From 2526e443781ba46f190c72a2f8ad72312a438774 Mon Sep 17 00:00:00 2001 From: matei jordache Date: Sun, 22 Oct 2023 09:32:07 -0400 Subject: [PATCH] firmware defines --- .pio/build/lemon-pepper/idedata.json | 1 + .pio/build/project.checksum | 1 + .../.github/ISSUE_TEMPLATE/bug_report.md | 29 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../Simple FOC/.github/workflows/ccpp.yml | 109 ++ .pio/libdeps/aioli-foc/Simple FOC/.piopm | 1 + .../libdeps/aioli-foc/Simple FOC/CITATION.cff | 48 + .../aioli-foc/Simple FOC/CODE_OF_CONDUCT.md | 128 +++ .pio/libdeps/aioli-foc/Simple FOC/LICENSE | 21 + .pio/libdeps/aioli-foc/Simple FOC/README.md | 223 ++++ .../B_G431B_ESC1/B_G431B_ESC1.ino | 109 ++ .../B_G431B_ESC1/build_opt.h | 1 + .../bluepill_position_control.ino | 115 +++ .../bluepill_position_control.ino | 115 +++ .../full_control_serial.ino | 130 +++ .../full_control_serial.ino | 129 +++ .../esp32_current_control_low_side.ino | 167 +++ .../stm32_current_control_low_side.ino | 167 +++ .../esp32_position_control.ino | 107 ++ .../esp32_position_control.ino | 112 +++ .../position_control/position_control.ino | 127 +++ .../voltage_control/voltage_control.ino | 110 ++ .../odrive_example_encoder.ino | 134 +++ .../odrive_example_spi/odrive_example_spi.ino | 132 +++ .../SAMD_examples/README.md | 62 ++ .../nano33IoT_velocity_control.ino | 64 ++ .../single_full_control_example.ino | 102 ++ .../angle_control/angle_control.ino | 125 +++ .../double_full_control_example.ino | 118 +++ .../single_full_control_example.ino | 89 ++ .../double_full_control_example.ino | 144 +++ .../single_full_control_example.ino | 101 ++ .../single_full_control_example.ino | 101 ++ .../smartstepper_control.ino | 61 ++ .../bldc_driver_6pwm_standalone.ino | 41 + .../open_loop_velocity_6pwm.ino | 65 ++ .../bldc_driver_6pwm_standalone.ino | 57 ++ .../open_loop_velocity_6pwm.ino | 80 ++ .../open_loop_position_example.ino | 70 ++ .../open_loop_velocity_example.ino | 68 ++ .../encoder/angle_control/angle_control.ino | 129 +++ .../angle_control/angle_control.ino | 133 +++ .../angle_control/angle_control.ino | 112 +++ .../current_control/current_control.ino | 107 ++ .../voltage_control/voltage_control.ino | 92 ++ .../voltage_control/voltage_control.ino | 95 ++ .../voltage_control/voltage_control.ino | 84 ++ .../velocity_control/velocity_control.ino | 137 +++ .../velocity_control/velocity_control.ino | 125 +++ .../velocity_control/velocity_control.ino | 106 ++ .../full_control_serial.ino | 106 ++ .../full_control_serial.ino | 114 +++ .../full_control_serial.ino | 104 ++ .../examples/osc_control_examples/README.md | 28 + .../osc_esp32_3pwm/layout1.touchosc | Bin 0 -> 511 bytes .../osc_esp32_3pwm/osc_esp32_3pwm.ino | 183 ++++ .../osc_esp32_fullcontrol.ino | 351 +++++++ .../osc_esp32_fullcontrol/osc_fullcontrol.pd | 384 +++++++ .../osc_esp32_fullcontrol/ssid.h_rename_me | 4 + .../alignment_and_cogging_test.ino | 125 +++ .../encoder/find_kv_rating/find_kv_rating.ino | 102 ++ .../find_kv_rating/find_kv_rating.ino | 99 ++ .../find_kv_rating/find_kv_rating.ino | 96 ++ .../find_pole_pairs_number.ino | 173 ++++ .../find_pole_pairs_number.ino | 173 ++++ .../find_sensor_offset_and_direction.ino | 84 ++ .../commander_extend_example.ino | 53 + .../commander_no_serial.ino | 51 + .../commander_tune_custom_loop.ino | 79 ++ .../step_dir_listener_simple.ino | 36 + .../step_dir_listener_software_interrupt.ino | 44 + .../step_dir_motor_example.ino | 98 ++ .../generic_current_sense.ino | 59 ++ .../inline_current_sense_test.ino | 36 + .../bldc_driver_3pwm_standalone.ino | 34 + .../bldc_driver_6pwm_standalone.ino | 35 + .../stepper_driver_2pwm_standalone.ino | 39 + .../stepper_driver_4pwm_standalone.ino | 34 + .../encoder_example/encoder_example.ino | 44 + .../encoder_software_interrupts_example.ino | 57 ++ .../generic_sensor/generic_sensor.ino | 51 + .../hall_sensor_example.ino | 48 + ...all_sensor_software_interrupts_example.ino | 59 ++ .../find_raw_centers/find_raw_centers.ino | 62 ++ .../find_raw_min_max/find_raw_min_max.ino | 56 ++ .../magnetic_sensor_analog_example.ino | 37 + .../esp32_i2c_dual_bus_example.ino | 40 + .../stm32_i2c_dual_bus_example.ino | 39 + .../magnetic_sensor_i2c_example.ino | 43 + .../find_raw_min_max/find_raw_min_max.ino | 49 + .../magnetic_sensor_pwm_example.ino | 38 + ...magnetic_sensor_pwm_software_interrupt.ino | 44 + .../esp32_spi_alt_example.ino | 41 + .../stm32_spi_alt_example.ino | 32 + .../magnetic_sensor_spi_example.ino | 32 + .../libdeps/aioli-foc/Simple FOC/keywords.txt | 251 +++++ .../aioli-foc/Simple FOC/library.properties | 10 + .../aioli-foc/Simple FOC/src/BLDCMotor.cpp | 736 ++++++++++++++ .../aioli-foc/Simple FOC/src/BLDCMotor.h | 115 +++ .../aioli-foc/Simple FOC/src/SimpleFOC.h | 119 +++ .../aioli-foc/Simple FOC/src/StepperMotor.cpp | 443 ++++++++ .../aioli-foc/Simple FOC/src/StepperMotor.h | 115 +++ .../src/common/base_classes/BLDCDriver.h | 56 ++ .../src/common/base_classes/CurrentSense.cpp | 99 ++ .../src/common/base_classes/CurrentSense.h | 75 ++ .../src/common/base_classes/FOCMotor.cpp | 155 +++ .../src/common/base_classes/FOCMotor.h | 251 +++++ .../src/common/base_classes/Sensor.cpp | 78 ++ .../src/common/base_classes/Sensor.h | 139 +++ .../src/common/base_classes/StepperDriver.h | 32 + .../Simple FOC/src/common/defaults.h | 49 + .../Simple FOC/src/common/foc_utils.cpp | 73 ++ .../Simple FOC/src/common/foc_utils.h | 106 ++ .../Simple FOC/src/common/lowpass_filter.cpp | 28 + .../Simple FOC/src/common/lowpass_filter.h | 28 + .../aioli-foc/Simple FOC/src/common/pid.cpp | 64 ++ .../aioli-foc/Simple FOC/src/common/pid.h | 41 + .../Simple FOC/src/common/time_utils.cpp | 31 + .../Simple FOC/src/common/time_utils.h | 22 + .../src/communication/Commander.cpp | 685 +++++++++++++ .../Simple FOC/src/communication/Commander.h | 301 ++++++ .../src/communication/SimpleFOCDebug.cpp | 104 ++ .../src/communication/SimpleFOCDebug.h | 79 ++ .../src/communication/StepDirListener.cpp | 40 + .../src/communication/StepDirListener.h | 65 ++ .../Simple FOC/src/communication/commands.h | 52 + .../src/current_sense/GenericCurrentSense.cpp | 163 +++ .../src/current_sense/GenericCurrentSense.h | 40 + .../src/current_sense/InlineCurrentSense.cpp | 253 +++++ .../src/current_sense/InlineCurrentSense.h | 73 ++ .../src/current_sense/LowsideCurrentSense.cpp | 254 +++++ .../src/current_sense/LowsideCurrentSense.h | 73 ++ .../src/current_sense/hardware_api.h | 65 ++ .../hardware_specific/atmega_mcu.cpp | 40 + .../hardware_specific/due_mcu.cpp | 27 + .../esp32/esp32_adc_driver.cpp | 259 +++++ .../esp32/esp32_adc_driver.h | 88 ++ .../esp32/esp32_ledc_mcu.cpp | 27 + .../hardware_specific/esp32/esp32_mcu.cpp | 162 +++ .../esp32/esp32s_adc_driver.cpp | 258 +++++ .../hardware_specific/generic_mcu.cpp | 41 + .../hardware_specific/rp2040/rp2040_mcu.cpp | 265 +++++ .../hardware_specific/rp2040/rp2040_mcu.h | 93 ++ .../hardware_specific/samd/samd21_mcu.cpp | 318 ++++++ .../hardware_specific/samd/samd21_mcu.h | 69 ++ .../hardware_specific/samd/samd_mcu.cpp | 23 + .../stm32/b_g431/b_g431_hal.cpp | 377 +++++++ .../stm32/b_g431/b_g431_hal.h | 18 + .../stm32/b_g431/b_g431_mcu.cpp | 171 ++++ .../hardware_specific/stm32/stm32_mcu.cpp | 33 + .../hardware_specific/stm32/stm32_mcu.h | 21 + .../stm32/stm32f1/stm32f1_hal.cpp | 162 +++ .../stm32/stm32f1/stm32f1_hal.h | 17 + .../stm32/stm32f1/stm32f1_mcu.cpp | 104 ++ .../stm32/stm32f4/stm32f4_hal.cpp | 170 ++++ .../stm32/stm32f4/stm32f4_hal.h | 18 + .../stm32/stm32f4/stm32f4_mcu.cpp | 96 ++ .../stm32/stm32f4/stm32f4_utils.cpp | 193 ++++ .../stm32/stm32f4/stm32f4_utils.h | 34 + .../stm32/stm32g4/stm32g4_hal.cpp | 267 +++++ .../stm32/stm32g4/stm32g4_hal.h | 19 + .../stm32/stm32g4/stm32g4_mcu.cpp | 98 ++ .../stm32/stm32g4/stm32g4_utils.cpp | 237 +++++ .../stm32/stm32g4/stm32g4_utils.h | 34 + .../stm32/stm32l4/stm32l4_hal.cpp | 266 +++++ .../stm32/stm32l4/stm32l4_hal.h | 19 + .../stm32/stm32l4/stm32l4_mcu.cpp | 98 ++ .../stm32/stm32l4/stm32l4_utils.cpp | 221 ++++ .../stm32/stm32l4/stm32l4_utils.h | 34 + .../hardware_specific/teensy_mcu.cpp | 24 + .../Simple FOC/src/drivers/BLDCDriver3PWM.cpp | 92 ++ .../Simple FOC/src/drivers/BLDCDriver3PWM.h | 64 ++ .../Simple FOC/src/drivers/BLDCDriver6PWM.cpp | 103 ++ .../Simple FOC/src/drivers/BLDCDriver6PWM.h | 70 ++ .../src/drivers/StepperDriver2PWM.cpp | 107 ++ .../src/drivers/StepperDriver2PWM.h | 68 ++ .../src/drivers/StepperDriver4PWM.cpp | 81 ++ .../src/drivers/StepperDriver4PWM.h | 55 + .../Simple FOC/src/drivers/hardware_api.h | 180 ++++ .../atmega/atmega2560_mcu.cpp | 278 +++++ .../atmega/atmega328_mcu.cpp | 255 +++++ .../atmega/atmega32u4_mcu.cpp | 222 ++++ .../src/drivers/hardware_specific/due_mcu.cpp | 449 +++++++++ .../esp32/esp32_driver_mcpwm.h | 96 ++ .../esp32/esp32_ledc_mcu.cpp | 185 ++++ .../hardware_specific/esp32/esp32_mcu.cpp | 431 ++++++++ .../drivers/hardware_specific/esp8266_mcu.cpp | 121 +++ .../drivers/hardware_specific/generic_mcu.cpp | 125 +++ .../drivers/hardware_specific/nrf52_mcu.cpp | 397 ++++++++ .../hardware_specific/portenta_h7_mcu.cpp | 545 ++++++++++ .../hardware_specific/renesas/renesas.cpp | 609 +++++++++++ .../hardware_specific/renesas/renesas.h | 28 + .../hardware_specific/rp2040/rp2040_mcu.cpp | 237 +++++ .../hardware_specific/rp2040/rp2040_mcu.h | 22 + .../hardware_specific/samd/samd21_mcu.cpp | 353 +++++++ .../hardware_specific/samd/samd51_mcu.cpp | 351 +++++++ .../hardware_specific/samd/samd_mcu.cpp | 914 +++++++++++++++++ .../drivers/hardware_specific/samd/samd_mcu.h | 127 +++ .../hardware_specific/stm32/stm32_mcu.cpp | 952 ++++++++++++++++++ .../hardware_specific/stm32/stm32_mcu.h | 32 + .../hardware_specific/teensy/teensy3_mcu.cpp | 228 +++++ .../hardware_specific/teensy/teensy4_mcu.cpp | 306 ++++++ .../hardware_specific/teensy/teensy4_mcu.h | 110 ++ .../hardware_specific/teensy/teensy_mcu.cpp | 116 +++ .../hardware_specific/teensy/teensy_mcu.h | 14 + .../Simple FOC/src/sensors/Encoder.cpp | 229 +++++ .../Simple FOC/src/sensors/Encoder.h | 91 ++ .../Simple FOC/src/sensors/GenericSensor.cpp | 26 + .../Simple FOC/src/sensors/GenericSensor.h | 31 + .../Simple FOC/src/sensors/HallSensor.cpp | 173 ++++ .../Simple FOC/src/sensors/HallSensor.h | 98 ++ .../src/sensors/MagneticSensorAnalog.cpp | 42 + .../src/sensors/MagneticSensorAnalog.h | 51 + .../src/sensors/MagneticSensorI2C.cpp | 156 +++ .../src/sensors/MagneticSensorI2C.h | 84 ++ .../src/sensors/MagneticSensorPWM.cpp | 117 +++ .../src/sensors/MagneticSensorPWM.h | 73 ++ .../src/sensors/MagneticSensorSPI.cpp | 161 +++ .../src/sensors/MagneticSensorSPI.h | 84 ++ .../.github/workflows/ccpp.yml | 76 ++ .../aioli-foc/SimpleFOCDrivers/.gitignore | 2 + .../libdeps/aioli-foc/SimpleFOCDrivers/.piopm | 1 + .../aioli-foc/SimpleFOCDrivers/LICENSE | 21 + .../aioli-foc/SimpleFOCDrivers/README.md | 112 +++ .../drv8316/drv8316_3pwm/drv8316_3pwm.ino | 123 +++ .../drv8316/drv8316_6pwm/drv8316_6pwm.ino | 111 ++ .../encoders/calibrated/calibrated.ino | 86 ++ .../encoders/mt6816/mt6816_spi/mt6816_spi.ino | 106 ++ .../examples/encoders/smoothing/smoothing.ino | 141 +++ .../aioli-foc/SimpleFOCDrivers/keywords.txt | 11 + .../SimpleFOCDrivers/library.properties | 11 + .../SimpleFOCDrivers/src/SimpleFOCDrivers.h | 11 + .../SimpleFOCDrivers/src/comms/README.md | 5 + .../src/comms/RegisterReceiver.cpp | 131 +++ .../src/comms/RegisterReceiver.h | 15 + .../src/comms/RegisterSender.cpp | 193 ++++ .../src/comms/RegisterSender.h | 18 + .../src/comms/SimpleFOCRegisters.h | 72 ++ .../src/comms/i2c/I2CCommander.cpp | 424 ++++++++ .../src/comms/i2c/I2CCommander.h | 49 + .../src/comms/i2c/I2CCommanderMaster.cpp | 59 ++ .../src/comms/i2c/I2CCommanderMaster.h | 42 + .../SimpleFOCDrivers/src/comms/i2c/README.md | 98 ++ .../src/comms/serial/README.md | 49 + .../src/comms/serial/SerialASCIITelemetry.cpp | 65 ++ .../src/comms/serial/SerialASCIITelemetry.h | 27 + .../comms/serial/SerialBinaryCommander.cpp | 153 +++ .../src/comms/serial/SerialBinaryCommander.h | 57 ++ .../src/comms/stm32speeddir/README.md | 58 ++ .../stm32speeddir/STM32SpeedDirInput.cpp | 29 + .../comms/stm32speeddir/STM32SpeedDirInput.h | 31 + .../src/comms/telemetry/README.md | 42 + .../src/comms/telemetry/Telemetry.cpp | 67 ++ .../src/comms/telemetry/Telemetry.h | 53 + .../src/drivers/drv8316/README.md | 175 ++++ .../src/drivers/drv8316/drv8316.cpp | 586 +++++++++++ .../src/drivers/drv8316/drv8316.h | 316 ++++++ .../src/drivers/drv8316/drv8316_registers.h | 184 ++++ .../src/drivers/tmc6200/README.md | 147 +++ .../src/drivers/tmc6200/TMC6200.cpp | 221 ++++ .../src/drivers/tmc6200/TMC6200.hpp | 171 ++++ .../src/drivers/tmc6200/TMC6200_Registers.hpp | 111 ++ .../src/encoders/a1334/A1334.cpp | 54 + .../src/encoders/a1334/A1334.h | 51 + .../encoders/a1334/MagneticSensorA1334.cpp | 27 + .../src/encoders/a1334/MagneticSensorA1334.h | 24 + .../src/encoders/aeat8800q24/AEAT8800Q24.cpp | 102 ++ .../src/encoders/aeat8800q24/AEAT8800Q24.h | 119 +++ .../aeat8800q24/MagneticSensorAEAT8800Q24.cpp | 24 + .../aeat8800q24/MagneticSensorAEAT8800Q24.h | 18 + .../src/encoders/aeat8800q24/README.md | 75 ++ .../src/encoders/as5047/AS5047.cpp | 245 +++++ .../src/encoders/as5047/AS5047.h | 167 +++ .../encoders/as5047/MagneticSensorAS5047.cpp | 29 + .../encoders/as5047/MagneticSensorAS5047.h | 20 + .../src/encoders/as5047/README.md | 82 ++ .../src/encoders/as5047u/AS5047U.cpp | 350 +++++++ .../src/encoders/as5047u/AS5047U.h | 225 +++++ .../as5047u/MagneticSensorAS5047U.cpp | 29 + .../encoders/as5047u/MagneticSensorAS5047U.h | 20 + .../src/encoders/as5047u/README.md | 81 ++ .../src/encoders/as5048a/AS5048A.cpp | 118 +++ .../src/encoders/as5048a/AS5048A.h | 89 ++ .../as5048a/MagneticSensorAS5048A.cpp | 26 + .../encoders/as5048a/MagneticSensorAS5048A.h | 20 + .../as5048a/PreciseMagneticSensorAS5048A.cpp | 50 + .../as5048a/PreciseMagneticSensorAS5048A.h | 33 + .../src/encoders/as5048a/README.md | 86 ++ .../encoders/as5145/MagneticSensorAS5145.cpp | 34 + .../encoders/as5145/MagneticSensorAS5145.h | 39 + .../src/encoders/as5145/README.md | 50 + .../src/encoders/as5600/AS5600.cpp | 175 ++++ .../src/encoders/as5600/AS5600.h | 104 ++ .../encoders/as5600/MagneticSensorAS5600.cpp | 18 + .../encoders/as5600/MagneticSensorAS5600.h | 22 + .../src/encoders/as5600/README.md | 131 +++ .../encoders/calibrated/CalibratedSensor.cpp | 257 +++++ .../encoders/calibrated/CalibratedSensor.h | 61 ++ .../src/encoders/calibrated/README.md | 91 ++ .../src/encoders/ma330/MA330.cpp | 146 +++ .../src/encoders/ma330/MA330.h | 82 ++ .../encoders/ma330/MagneticSensorMA330.cpp | 26 + .../src/encoders/ma330/MagneticSensorMA330.h | 19 + .../src/encoders/ma330/README.md | 66 ++ .../src/encoders/ma730/MA730.cpp | 125 +++ .../src/encoders/ma730/MA730.h | 76 ++ .../encoders/ma730/MagneticSensorMA730.cpp | 26 + .../src/encoders/ma730/MagneticSensorMA730.h | 19 + .../encoders/ma730/MagneticSensorMA730SSI.cpp | 34 + .../encoders/ma730/MagneticSensorMA730SSI.h | 25 + .../src/encoders/ma730/README.md | 74 ++ .../mt6701/MagneticSensorMT6701SSI.cpp | 42 + .../encoders/mt6701/MagneticSensorMT6701SSI.h | 43 + .../src/encoders/mt6701/README.md | 82 ++ .../src/encoders/mt6816/MT6816.cpp | 56 ++ .../src/encoders/mt6816/MT6816.h | 40 + .../encoders/mt6816/MagneticSensorMT6816.cpp | 26 + .../encoders/mt6816/MagneticSensorMT6816.h | 17 + .../src/encoders/mt6835/MT6835.cpp | 278 +++++ .../src/encoders/mt6835/MT6835.h | 215 ++++ .../encoders/mt6835/MagneticSensorMT6835.cpp | 24 + .../encoders/mt6835/MagneticSensorMT6835.h | 17 + .../src/encoders/mt6835/README.md | 64 ++ .../sc60228/MagneticSensorSC60228.cpp | 24 + .../encoders/sc60228/MagneticSensorSC60228.h | 20 + .../src/encoders/sc60228/README.md | 47 + .../src/encoders/sc60228/SC60228.cpp | 57 ++ .../src/encoders/sc60228/SC60228.h | 50 + .../src/encoders/smoothing/README.md | 69 ++ .../encoders/smoothing/SmoothingSensor.cpp | 57 ++ .../src/encoders/smoothing/SmoothingSensor.h | 46 + .../src/encoders/stm32hwencoder/README.md | 49 + .../stm32hwencoder/STM32HWEncoder.cpp | 78 ++ .../encoders/stm32hwencoder/STM32HWEncoder.h | 36 + .../src/encoders/stm32pwmsensor/README.md | 46 + .../stm32pwmsensor/STM32MagneticSensorPWM.cpp | 35 + .../stm32pwmsensor/STM32MagneticSensorPWM.h | 28 + .../tle5012b/MagneticSensorTLE5012B.cpp | 18 + .../tle5012b/MagneticSensorTLE5012B.h | 25 + .../src/encoders/tle5012b/README.md | 91 ++ .../src/encoders/tle5012b/STM32TLE5012B.cpp | 159 +++ .../src/encoders/tle5012b/STM32TLE5012B.h | 45 + .../HybridStepperMotor/HybridStepperMotor.cpp | 548 ++++++++++ .../HybridStepperMotor/HybridStepperMotor.h | 112 +++ .../src/motors/HybridStepperMotor/README.md | 45 + .../SimpleFOCDrivers/src/settings/README.md | 143 +++ .../src/settings/SettingsStorage.cpp | 120 +++ .../src/settings/SettingsStorage.h | 70 ++ .../i2c/CAT24I2CFlashSettingsStorage.cpp | 98 ++ .../i2c/CAT24I2CFlashSettingsStorage.h | 35 + .../src/settings/i2c/README.md | 26 + .../src/settings/samd/README.md | 49 + .../settings/samd/SAMDNVMSettingsStorage.cpp | 164 +++ .../settings/samd/SAMDNVMSettingsStorage.h | 85 ++ .../src/utilities/PreciseAngle.cpp | 113 +++ .../src/utilities/PreciseAngle.h | 57 ++ .../SimpleFOCDrivers/src/utilities/README.md | 25 + .../src/utilities/stm32pwm/README.md | 73 ++ .../src/utilities/stm32pwm/STM32PWMInput.cpp | 108 ++ .../src/utilities/stm32pwm/STM32PWMInput.h | 29 + .../src/voltage/GenericVoltageSense.cpp | 44 + .../src/voltage/GenericVoltageSense.h | 23 + .../src/voltage/VoltageSense.cpp | 23 + .../src/voltage/VoltageSense.h | 24 + .pio/libdeps/aioli-foc/integrity.dat | 2 + .../.github/ISSUE_TEMPLATE/bug_report.md | 29 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../Simple FOC/.github/workflows/ccpp.yml | 109 ++ .pio/libdeps/lemon-pepper/Simple FOC/.piopm | 1 + .../lemon-pepper/Simple FOC/CITATION.cff | 48 + .../Simple FOC/CODE_OF_CONDUCT.md | 128 +++ .pio/libdeps/lemon-pepper/Simple FOC/LICENSE | 21 + .../libdeps/lemon-pepper/Simple FOC/README.md | 223 ++++ .../B_G431B_ESC1/B_G431B_ESC1.ino | 109 ++ .../B_G431B_ESC1/build_opt.h | 1 + .../bluepill_position_control.ino | 115 +++ .../bluepill_position_control.ino | 115 +++ .../full_control_serial.ino | 130 +++ .../full_control_serial.ino | 129 +++ .../esp32_current_control_low_side.ino | 167 +++ .../stm32_current_control_low_side.ino | 167 +++ .../esp32_position_control.ino | 107 ++ .../esp32_position_control.ino | 112 +++ .../position_control/position_control.ino | 127 +++ .../voltage_control/voltage_control.ino | 110 ++ .../odrive_example_encoder.ino | 134 +++ .../odrive_example_spi/odrive_example_spi.ino | 132 +++ .../SAMD_examples/README.md | 62 ++ .../nano33IoT_velocity_control.ino | 64 ++ .../single_full_control_example.ino | 102 ++ .../angle_control/angle_control.ino | 125 +++ .../double_full_control_example.ino | 118 +++ .../single_full_control_example.ino | 89 ++ .../double_full_control_example.ino | 144 +++ .../single_full_control_example.ino | 101 ++ .../single_full_control_example.ino | 101 ++ .../smartstepper_control.ino | 61 ++ .../bldc_driver_6pwm_standalone.ino | 41 + .../open_loop_velocity_6pwm.ino | 65 ++ .../bldc_driver_6pwm_standalone.ino | 57 ++ .../open_loop_velocity_6pwm.ino | 80 ++ .../open_loop_position_example.ino | 70 ++ .../open_loop_velocity_example.ino | 68 ++ .../encoder/angle_control/angle_control.ino | 129 +++ .../angle_control/angle_control.ino | 133 +++ .../angle_control/angle_control.ino | 112 +++ .../current_control/current_control.ino | 107 ++ .../voltage_control/voltage_control.ino | 92 ++ .../voltage_control/voltage_control.ino | 95 ++ .../voltage_control/voltage_control.ino | 84 ++ .../velocity_control/velocity_control.ino | 137 +++ .../velocity_control/velocity_control.ino | 125 +++ .../velocity_control/velocity_control.ino | 106 ++ .../full_control_serial.ino | 106 ++ .../full_control_serial.ino | 114 +++ .../full_control_serial.ino | 104 ++ .../examples/osc_control_examples/README.md | 28 + .../osc_esp32_3pwm/layout1.touchosc | Bin 0 -> 511 bytes .../osc_esp32_3pwm/osc_esp32_3pwm.ino | 183 ++++ .../osc_esp32_fullcontrol.ino | 351 +++++++ .../osc_esp32_fullcontrol/osc_fullcontrol.pd | 384 +++++++ .../osc_esp32_fullcontrol/ssid.h_rename_me | 4 + .../alignment_and_cogging_test.ino | 125 +++ .../encoder/find_kv_rating/find_kv_rating.ino | 102 ++ .../find_kv_rating/find_kv_rating.ino | 99 ++ .../find_kv_rating/find_kv_rating.ino | 96 ++ .../find_pole_pairs_number.ino | 173 ++++ .../find_pole_pairs_number.ino | 173 ++++ .../find_sensor_offset_and_direction.ino | 84 ++ .../commander_extend_example.ino | 53 + .../commander_no_serial.ino | 51 + .../commander_tune_custom_loop.ino | 79 ++ .../step_dir_listener_simple.ino | 36 + .../step_dir_listener_software_interrupt.ino | 44 + .../step_dir_motor_example.ino | 98 ++ .../generic_current_sense.ino | 59 ++ .../inline_current_sense_test.ino | 36 + .../bldc_driver_3pwm_standalone.ino | 34 + .../bldc_driver_6pwm_standalone.ino | 35 + .../stepper_driver_2pwm_standalone.ino | 39 + .../stepper_driver_4pwm_standalone.ino | 34 + .../encoder_example/encoder_example.ino | 44 + .../encoder_software_interrupts_example.ino | 57 ++ .../generic_sensor/generic_sensor.ino | 51 + .../hall_sensor_example.ino | 48 + ...all_sensor_software_interrupts_example.ino | 59 ++ .../find_raw_centers/find_raw_centers.ino | 62 ++ .../find_raw_min_max/find_raw_min_max.ino | 56 ++ .../magnetic_sensor_analog_example.ino | 37 + .../esp32_i2c_dual_bus_example.ino | 40 + .../stm32_i2c_dual_bus_example.ino | 39 + .../magnetic_sensor_i2c_example.ino | 43 + .../find_raw_min_max/find_raw_min_max.ino | 49 + .../magnetic_sensor_pwm_example.ino | 38 + ...magnetic_sensor_pwm_software_interrupt.ino | 44 + .../esp32_spi_alt_example.ino | 41 + .../stm32_spi_alt_example.ino | 32 + .../magnetic_sensor_spi_example.ino | 32 + .../lemon-pepper/Simple FOC/keywords.txt | 251 +++++ .../Simple FOC/library.properties | 10 + .../lemon-pepper/Simple FOC/src/BLDCMotor.cpp | 736 ++++++++++++++ .../lemon-pepper/Simple FOC/src/BLDCMotor.h | 115 +++ .../lemon-pepper/Simple FOC/src/SimpleFOC.h | 119 +++ .../Simple FOC/src/StepperMotor.cpp | 443 ++++++++ .../Simple FOC/src/StepperMotor.h | 115 +++ .../src/common/base_classes/BLDCDriver.h | 56 ++ .../src/common/base_classes/CurrentSense.cpp | 99 ++ .../src/common/base_classes/CurrentSense.h | 75 ++ .../src/common/base_classes/FOCMotor.cpp | 155 +++ .../src/common/base_classes/FOCMotor.h | 251 +++++ .../src/common/base_classes/Sensor.cpp | 78 ++ .../src/common/base_classes/Sensor.h | 139 +++ .../src/common/base_classes/StepperDriver.h | 32 + .../Simple FOC/src/common/defaults.h | 49 + .../Simple FOC/src/common/foc_utils.cpp | 73 ++ .../Simple FOC/src/common/foc_utils.h | 106 ++ .../Simple FOC/src/common/lowpass_filter.cpp | 28 + .../Simple FOC/src/common/lowpass_filter.h | 28 + .../Simple FOC/src/common/pid.cpp | 64 ++ .../lemon-pepper/Simple FOC/src/common/pid.h | 41 + .../Simple FOC/src/common/time_utils.cpp | 31 + .../Simple FOC/src/common/time_utils.h | 22 + .../src/communication/Commander.cpp | 685 +++++++++++++ .../Simple FOC/src/communication/Commander.h | 301 ++++++ .../src/communication/SimpleFOCDebug.cpp | 104 ++ .../src/communication/SimpleFOCDebug.h | 79 ++ .../src/communication/StepDirListener.cpp | 40 + .../src/communication/StepDirListener.h | 65 ++ .../Simple FOC/src/communication/commands.h | 52 + .../src/current_sense/GenericCurrentSense.cpp | 163 +++ .../src/current_sense/GenericCurrentSense.h | 40 + .../src/current_sense/InlineCurrentSense.cpp | 253 +++++ .../src/current_sense/InlineCurrentSense.h | 73 ++ .../src/current_sense/LowsideCurrentSense.cpp | 254 +++++ .../src/current_sense/LowsideCurrentSense.h | 73 ++ .../src/current_sense/hardware_api.h | 65 ++ .../hardware_specific/atmega_mcu.cpp | 40 + .../hardware_specific/due_mcu.cpp | 27 + .../esp32/esp32_adc_driver.cpp | 259 +++++ .../esp32/esp32_adc_driver.h | 88 ++ .../esp32/esp32_ledc_mcu.cpp | 27 + .../hardware_specific/esp32/esp32_mcu.cpp | 162 +++ .../esp32/esp32s_adc_driver.cpp | 258 +++++ .../hardware_specific/generic_mcu.cpp | 41 + .../hardware_specific/rp2040/rp2040_mcu.cpp | 265 +++++ .../hardware_specific/rp2040/rp2040_mcu.h | 93 ++ .../hardware_specific/samd/samd21_mcu.cpp | 318 ++++++ .../hardware_specific/samd/samd21_mcu.h | 69 ++ .../hardware_specific/samd/samd_mcu.cpp | 23 + .../stm32/b_g431/b_g431_hal.cpp | 377 +++++++ .../stm32/b_g431/b_g431_hal.h | 18 + .../stm32/b_g431/b_g431_mcu.cpp | 171 ++++ .../hardware_specific/stm32/stm32_mcu.cpp | 33 + .../hardware_specific/stm32/stm32_mcu.h | 21 + .../stm32/stm32f1/stm32f1_hal.cpp | 162 +++ .../stm32/stm32f1/stm32f1_hal.h | 17 + .../stm32/stm32f1/stm32f1_mcu.cpp | 104 ++ .../stm32/stm32f4/stm32f4_hal.cpp | 170 ++++ .../stm32/stm32f4/stm32f4_hal.h | 18 + .../stm32/stm32f4/stm32f4_mcu.cpp | 96 ++ .../stm32/stm32f4/stm32f4_utils.cpp | 193 ++++ .../stm32/stm32f4/stm32f4_utils.h | 34 + .../stm32/stm32g4/stm32g4_hal.cpp | 267 +++++ .../stm32/stm32g4/stm32g4_hal.h | 19 + .../stm32/stm32g4/stm32g4_mcu.cpp | 98 ++ .../stm32/stm32g4/stm32g4_utils.cpp | 237 +++++ .../stm32/stm32g4/stm32g4_utils.h | 34 + .../stm32/stm32l4/stm32l4_hal.cpp | 266 +++++ .../stm32/stm32l4/stm32l4_hal.h | 19 + .../stm32/stm32l4/stm32l4_mcu.cpp | 98 ++ .../stm32/stm32l4/stm32l4_utils.cpp | 221 ++++ .../stm32/stm32l4/stm32l4_utils.h | 34 + .../hardware_specific/teensy_mcu.cpp | 24 + .../Simple FOC/src/drivers/BLDCDriver3PWM.cpp | 92 ++ .../Simple FOC/src/drivers/BLDCDriver3PWM.h | 64 ++ .../Simple FOC/src/drivers/BLDCDriver6PWM.cpp | 103 ++ .../Simple FOC/src/drivers/BLDCDriver6PWM.h | 70 ++ .../src/drivers/StepperDriver2PWM.cpp | 107 ++ .../src/drivers/StepperDriver2PWM.h | 68 ++ .../src/drivers/StepperDriver4PWM.cpp | 81 ++ .../src/drivers/StepperDriver4PWM.h | 55 + .../Simple FOC/src/drivers/hardware_api.h | 180 ++++ .../atmega/atmega2560_mcu.cpp | 278 +++++ .../atmega/atmega328_mcu.cpp | 255 +++++ .../atmega/atmega32u4_mcu.cpp | 222 ++++ .../src/drivers/hardware_specific/due_mcu.cpp | 449 +++++++++ .../esp32/esp32_driver_mcpwm.h | 96 ++ .../esp32/esp32_ledc_mcu.cpp | 185 ++++ .../hardware_specific/esp32/esp32_mcu.cpp | 431 ++++++++ .../drivers/hardware_specific/esp8266_mcu.cpp | 121 +++ .../drivers/hardware_specific/generic_mcu.cpp | 125 +++ .../drivers/hardware_specific/nrf52_mcu.cpp | 397 ++++++++ .../hardware_specific/portenta_h7_mcu.cpp | 545 ++++++++++ .../hardware_specific/renesas/renesas.cpp | 609 +++++++++++ .../hardware_specific/renesas/renesas.h | 28 + .../hardware_specific/rp2040/rp2040_mcu.cpp | 237 +++++ .../hardware_specific/rp2040/rp2040_mcu.h | 22 + .../hardware_specific/samd/samd21_mcu.cpp | 353 +++++++ .../hardware_specific/samd/samd51_mcu.cpp | 351 +++++++ .../hardware_specific/samd/samd_mcu.cpp | 914 +++++++++++++++++ .../drivers/hardware_specific/samd/samd_mcu.h | 127 +++ .../hardware_specific/stm32/stm32_mcu.cpp | 952 ++++++++++++++++++ .../hardware_specific/stm32/stm32_mcu.h | 32 + .../hardware_specific/teensy/teensy3_mcu.cpp | 228 +++++ .../hardware_specific/teensy/teensy4_mcu.cpp | 306 ++++++ .../hardware_specific/teensy/teensy4_mcu.h | 110 ++ .../hardware_specific/teensy/teensy_mcu.cpp | 116 +++ .../hardware_specific/teensy/teensy_mcu.h | 14 + .../Simple FOC/src/sensors/Encoder.cpp | 229 +++++ .../Simple FOC/src/sensors/Encoder.h | 91 ++ .../Simple FOC/src/sensors/GenericSensor.cpp | 26 + .../Simple FOC/src/sensors/GenericSensor.h | 31 + .../Simple FOC/src/sensors/HallSensor.cpp | 173 ++++ .../Simple FOC/src/sensors/HallSensor.h | 98 ++ .../src/sensors/MagneticSensorAnalog.cpp | 42 + .../src/sensors/MagneticSensorAnalog.h | 51 + .../src/sensors/MagneticSensorI2C.cpp | 156 +++ .../src/sensors/MagneticSensorI2C.h | 84 ++ .../src/sensors/MagneticSensorPWM.cpp | 117 +++ .../src/sensors/MagneticSensorPWM.h | 73 ++ .../src/sensors/MagneticSensorSPI.cpp | 161 +++ .../src/sensors/MagneticSensorSPI.h | 84 ++ .../.github/workflows/ccpp.yml | 76 ++ .../lemon-pepper/SimpleFOCDrivers/.gitignore | 2 + .../lemon-pepper/SimpleFOCDrivers/.piopm | 1 + .../lemon-pepper/SimpleFOCDrivers/LICENSE | 21 + .../lemon-pepper/SimpleFOCDrivers/README.md | 112 +++ .../drv8316/drv8316_3pwm/drv8316_3pwm.ino | 123 +++ .../drv8316/drv8316_6pwm/drv8316_6pwm.ino | 111 ++ .../encoders/calibrated/calibrated.ino | 86 ++ .../encoders/mt6816/mt6816_spi/mt6816_spi.ino | 106 ++ .../examples/encoders/smoothing/smoothing.ino | 141 +++ .../SimpleFOCDrivers/keywords.txt | 11 + .../SimpleFOCDrivers/library.properties | 11 + .../SimpleFOCDrivers/src/SimpleFOCDrivers.h | 11 + .../SimpleFOCDrivers/src/comms/README.md | 5 + .../src/comms/RegisterReceiver.cpp | 131 +++ .../src/comms/RegisterReceiver.h | 15 + .../src/comms/RegisterSender.cpp | 193 ++++ .../src/comms/RegisterSender.h | 18 + .../src/comms/SimpleFOCRegisters.h | 72 ++ .../src/comms/i2c/I2CCommander.cpp | 424 ++++++++ .../src/comms/i2c/I2CCommander.h | 49 + .../src/comms/i2c/I2CCommanderMaster.cpp | 59 ++ .../src/comms/i2c/I2CCommanderMaster.h | 42 + .../SimpleFOCDrivers/src/comms/i2c/README.md | 98 ++ .../src/comms/serial/README.md | 49 + .../src/comms/serial/SerialASCIITelemetry.cpp | 65 ++ .../src/comms/serial/SerialASCIITelemetry.h | 27 + .../comms/serial/SerialBinaryCommander.cpp | 153 +++ .../src/comms/serial/SerialBinaryCommander.h | 57 ++ .../src/comms/stm32speeddir/README.md | 58 ++ .../stm32speeddir/STM32SpeedDirInput.cpp | 29 + .../comms/stm32speeddir/STM32SpeedDirInput.h | 31 + .../src/comms/telemetry/README.md | 42 + .../src/comms/telemetry/Telemetry.cpp | 67 ++ .../src/comms/telemetry/Telemetry.h | 53 + .../src/drivers/drv8316/README.md | 175 ++++ .../src/drivers/drv8316/drv8316.cpp | 586 +++++++++++ .../src/drivers/drv8316/drv8316.h | 316 ++++++ .../src/drivers/drv8316/drv8316_registers.h | 184 ++++ .../src/drivers/tmc6200/README.md | 147 +++ .../src/drivers/tmc6200/TMC6200.cpp | 221 ++++ .../src/drivers/tmc6200/TMC6200.hpp | 171 ++++ .../src/drivers/tmc6200/TMC6200_Registers.hpp | 111 ++ .../src/encoders/a1334/A1334.cpp | 54 + .../src/encoders/a1334/A1334.h | 51 + .../encoders/a1334/MagneticSensorA1334.cpp | 27 + .../src/encoders/a1334/MagneticSensorA1334.h | 24 + .../src/encoders/aeat8800q24/AEAT8800Q24.cpp | 102 ++ .../src/encoders/aeat8800q24/AEAT8800Q24.h | 119 +++ .../aeat8800q24/MagneticSensorAEAT8800Q24.cpp | 24 + .../aeat8800q24/MagneticSensorAEAT8800Q24.h | 18 + .../src/encoders/aeat8800q24/README.md | 75 ++ .../src/encoders/as5047/AS5047.cpp | 245 +++++ .../src/encoders/as5047/AS5047.h | 167 +++ .../encoders/as5047/MagneticSensorAS5047.cpp | 29 + .../encoders/as5047/MagneticSensorAS5047.h | 20 + .../src/encoders/as5047/README.md | 82 ++ .../src/encoders/as5047u/AS5047U.cpp | 350 +++++++ .../src/encoders/as5047u/AS5047U.h | 225 +++++ .../as5047u/MagneticSensorAS5047U.cpp | 29 + .../encoders/as5047u/MagneticSensorAS5047U.h | 20 + .../src/encoders/as5047u/README.md | 81 ++ .../src/encoders/as5048a/AS5048A.cpp | 118 +++ .../src/encoders/as5048a/AS5048A.h | 89 ++ .../as5048a/MagneticSensorAS5048A.cpp | 26 + .../encoders/as5048a/MagneticSensorAS5048A.h | 20 + .../as5048a/PreciseMagneticSensorAS5048A.cpp | 50 + .../as5048a/PreciseMagneticSensorAS5048A.h | 33 + .../src/encoders/as5048a/README.md | 86 ++ .../encoders/as5145/MagneticSensorAS5145.cpp | 34 + .../encoders/as5145/MagneticSensorAS5145.h | 39 + .../src/encoders/as5145/README.md | 50 + .../src/encoders/as5600/AS5600.cpp | 175 ++++ .../src/encoders/as5600/AS5600.h | 104 ++ .../encoders/as5600/MagneticSensorAS5600.cpp | 18 + .../encoders/as5600/MagneticSensorAS5600.h | 22 + .../src/encoders/as5600/README.md | 131 +++ .../encoders/calibrated/CalibratedSensor.cpp | 257 +++++ .../encoders/calibrated/CalibratedSensor.h | 61 ++ .../src/encoders/calibrated/README.md | 91 ++ .../src/encoders/ma330/MA330.cpp | 146 +++ .../src/encoders/ma330/MA330.h | 82 ++ .../encoders/ma330/MagneticSensorMA330.cpp | 26 + .../src/encoders/ma330/MagneticSensorMA330.h | 19 + .../src/encoders/ma330/README.md | 66 ++ .../src/encoders/ma730/MA730.cpp | 125 +++ .../src/encoders/ma730/MA730.h | 76 ++ .../encoders/ma730/MagneticSensorMA730.cpp | 26 + .../src/encoders/ma730/MagneticSensorMA730.h | 19 + .../encoders/ma730/MagneticSensorMA730SSI.cpp | 34 + .../encoders/ma730/MagneticSensorMA730SSI.h | 25 + .../src/encoders/ma730/README.md | 74 ++ .../mt6701/MagneticSensorMT6701SSI.cpp | 42 + .../encoders/mt6701/MagneticSensorMT6701SSI.h | 43 + .../src/encoders/mt6701/README.md | 82 ++ .../src/encoders/mt6816/MT6816.cpp | 56 ++ .../src/encoders/mt6816/MT6816.h | 40 + .../encoders/mt6816/MagneticSensorMT6816.cpp | 26 + .../encoders/mt6816/MagneticSensorMT6816.h | 17 + .../src/encoders/mt6835/MT6835.cpp | 278 +++++ .../src/encoders/mt6835/MT6835.h | 215 ++++ .../encoders/mt6835/MagneticSensorMT6835.cpp | 24 + .../encoders/mt6835/MagneticSensorMT6835.h | 17 + .../src/encoders/mt6835/README.md | 64 ++ .../sc60228/MagneticSensorSC60228.cpp | 24 + .../encoders/sc60228/MagneticSensorSC60228.h | 20 + .../src/encoders/sc60228/README.md | 47 + .../src/encoders/sc60228/SC60228.cpp | 57 ++ .../src/encoders/sc60228/SC60228.h | 50 + .../src/encoders/smoothing/README.md | 69 ++ .../encoders/smoothing/SmoothingSensor.cpp | 57 ++ .../src/encoders/smoothing/SmoothingSensor.h | 46 + .../src/encoders/stm32hwencoder/README.md | 49 + .../stm32hwencoder/STM32HWEncoder.cpp | 78 ++ .../encoders/stm32hwencoder/STM32HWEncoder.h | 36 + .../src/encoders/stm32pwmsensor/README.md | 46 + .../stm32pwmsensor/STM32MagneticSensorPWM.cpp | 35 + .../stm32pwmsensor/STM32MagneticSensorPWM.h | 28 + .../tle5012b/MagneticSensorTLE5012B.cpp | 18 + .../tle5012b/MagneticSensorTLE5012B.h | 25 + .../src/encoders/tle5012b/README.md | 91 ++ .../src/encoders/tle5012b/STM32TLE5012B.cpp | 159 +++ .../src/encoders/tle5012b/STM32TLE5012B.h | 45 + .../HybridStepperMotor/HybridStepperMotor.cpp | 548 ++++++++++ .../HybridStepperMotor/HybridStepperMotor.h | 112 +++ .../src/motors/HybridStepperMotor/README.md | 45 + .../SimpleFOCDrivers/src/settings/README.md | 143 +++ .../src/settings/SettingsStorage.cpp | 120 +++ .../src/settings/SettingsStorage.h | 70 ++ .../i2c/CAT24I2CFlashSettingsStorage.cpp | 98 ++ .../i2c/CAT24I2CFlashSettingsStorage.h | 35 + .../src/settings/i2c/README.md | 26 + .../src/settings/samd/README.md | 49 + .../settings/samd/SAMDNVMSettingsStorage.cpp | 164 +++ .../settings/samd/SAMDNVMSettingsStorage.h | 85 ++ .../src/utilities/PreciseAngle.cpp | 113 +++ .../src/utilities/PreciseAngle.h | 57 ++ .../SimpleFOCDrivers/src/utilities/README.md | 25 + .../src/utilities/stm32pwm/README.md | 73 ++ .../src/utilities/stm32pwm/STM32PWMInput.cpp | 108 ++ .../src/utilities/stm32pwm/STM32PWMInput.h | 29 + .../src/voltage/GenericVoltageSense.cpp | 44 + .../src/voltage/GenericVoltageSense.h | 23 + .../src/voltage/VoltageSense.cpp | 23 + .../src/voltage/VoltageSense.h | 24 + .pio/libdeps/lemon-pepper/integrity.dat | 2 + .vscode/c_cpp_properties.json | 141 +++ .vscode/extensions.json | 10 + .vscode/launch.json | 47 + firmware/boards/genericSTM32G431CB.json | 44 + firmware/include/README | 39 + firmware/include/lemon-pepper.h | 38 + firmware/ldscript.ld | 185 ++++ firmware/lib/README | 46 + firmware/lib/can/can.cpp | 263 +++++ firmware/lib/can/can.h | 17 + firmware/lib/dfu/dfu.cpp | 26 + firmware/lib/dfu/dfu.h | 7 + firmware/src/clock.c | 50 + firmware/src/main.cpp | 163 +++ firmware/test/README | 11 + hardware/currentsense.kicad_sch | 24 +- hardware/lemon-pepper.kicad_pcb | 8 +- hardware/mcu.kicad_sch | 18 +- lemon-pepper.pdf | Bin 1006037 -> 800628 bytes lemon-pepper/include/README | 39 + lemon-pepper/lib/README | 46 + lemon-pepper/platformio.ini | 14 + lemon-pepper/test/README | 11 + pinout.png | Bin 353540 -> 342911 bytes platformio.ini | 41 + 753 files changed, 81108 insertions(+), 32 deletions(-) create mode 100644 .pio/build/lemon-pepper/idedata.json create mode 100644 .pio/build/project.checksum create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/.github/workflows/ccpp.yml create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/.piopm create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/CITATION.cff create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/CODE_OF_CONDUCT.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/LICENSE create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/README.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/README.md create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/keywords.txt create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/library.properties create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/SimpleFOC.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/BLDCDriver.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/StepperDriver.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/defaults.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/pid.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/pid.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/communication/commands.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_api.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_api.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.h create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.cpp create mode 100644 .pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/.github/workflows/ccpp.yml create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/.gitignore create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/.piopm create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/LICENSE create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/keywords.txt create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/library.properties create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/SimpleFOCDrivers.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/README.md create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.cpp create mode 100644 .pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.h create mode 100644 .pio/libdeps/aioli-foc/integrity.dat create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/.github/workflows/ccpp.yml create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/.piopm create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/CITATION.cff create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/CODE_OF_CONDUCT.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/LICENSE create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/README.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/README.md create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/keywords.txt create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/library.properties create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/SimpleFOC.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/BLDCDriver.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/StepperDriver.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/defaults.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/communication/commands.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_api.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_api.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.h create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.cpp create mode 100644 .pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/.github/workflows/ccpp.yml create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/.gitignore create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/.piopm create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/LICENSE create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/keywords.txt create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/library.properties create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/SimpleFOCDrivers.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/README.md create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.cpp create mode 100644 .pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.h create mode 100644 .pio/libdeps/lemon-pepper/integrity.dat create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 firmware/boards/genericSTM32G431CB.json create mode 100644 firmware/include/README create mode 100644 firmware/include/lemon-pepper.h create mode 100644 firmware/ldscript.ld create mode 100644 firmware/lib/README create mode 100644 firmware/lib/can/can.cpp create mode 100644 firmware/lib/can/can.h create mode 100644 firmware/lib/dfu/dfu.cpp create mode 100644 firmware/lib/dfu/dfu.h create mode 100644 firmware/src/clock.c create mode 100644 firmware/src/main.cpp create mode 100644 firmware/test/README create mode 100644 lemon-pepper/include/README create mode 100644 lemon-pepper/lib/README create mode 100644 lemon-pepper/platformio.ini create mode 100644 lemon-pepper/test/README create mode 100644 platformio.ini diff --git a/.pio/build/lemon-pepper/idedata.json b/.pio/build/lemon-pepper/idedata.json new file mode 100644 index 0000000..504b0dc --- /dev/null +++ b/.pio/build/lemon-pepper/idedata.json @@ -0,0 +1 @@ +{"build_type": "release", "env_name": "lemon-pepper", "libsource_dirs": ["/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper", "/Users/mateijordache/.platformio/lib", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/__cores__/arduino", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries"], "defines": ["PLATFORMIO=60111", "STM32G4xx", "STM32G431xx", "USBCON", "SIMPLEFOC_STM32_DEBUG", "PIO_FRAMEWORK_ARDUINO_ENABLE_CDC", "HAL_FDCAN_MODULE_ENABLED", "FDCAN_ALT1", "SN65HVD23x", "ARDUINO_GENERIC_G431CBUX", "STM32G4xx", "ARDUINO=10808", "ARDUINO_ARCH_STM32", "NDEBUG", "ARDUINO_GENERIC_G431CBUX", "BOARD_NAME=\"GENERIC_G431CBUX\"", "HAL_UART_MODULE_ENABLED", "USE_FULL_LL_DRIVER", "VARIANT_H=\"variant_generic.h\"", "USBD_USE_CDC", "USBCON", "USB_VID=0", "USB_PID=0", "HAL_PCD_MODULE_ENABLED"], "includes": {"build": ["/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/include", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/src", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/include", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/EEPROM/src", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/dfu", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/can", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/Simple FOC/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SPI/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Wire/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/avr", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/LL", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/OpenAMP", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/hid", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/cdc", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Inc", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/STM32G4xx", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/open-amp/lib/include", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/libmetal/lib/include", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/virtual_driver", "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/Core/Include", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Include", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Source/Templates/gcc", "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/Include", "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/PrivateInclude", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/variants/STM32G4xx/G431C\\(6-8-B\\)U_G441CBU"], "compatlib": ["/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/can", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/dfu", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/Simple FOC/src", "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/EEPROM/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SPI/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Wire/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/CMSIS_DSP/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/IWatchdog/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Keyboard/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Mouse/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/RGB_LED_TLC59731/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Servo/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SoftwareSerial/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SrcWrapper/src", "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SubGhz/src"], "toolchain": ["/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/arm-none-eabi/include/c++/10.3.1", "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/arm-none-eabi/include/c++/10.3.1/arm-none-eabi", "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/lib/gcc/arm-none-eabi/10.3.1/include", "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/lib/gcc/arm-none-eabi/10.3.1/include-fixed", "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/arm-none-eabi/include"]}, "cc_flags": ["-std=gnu11", "-mcpu=cortex-m4", "-mthumb", "-mfpu=fpv4-sp-d16", "-mfloat-abi=hard", "-Os", "-ffunction-sections", "-fdata-sections", "-nostdlib", "--param", "max-inline-insns-single=500"], "cxx_flags": ["-std=gnu++14", "-fno-threadsafe-statics", "-fno-rtti", "-fno-exceptions", "-fno-use-cxa-atexit", "-mcpu=cortex-m4", "-mthumb", "-mfpu=fpv4-sp-d16", "-mfloat-abi=hard", "-Os", "-ffunction-sections", "-fdata-sections", "-nostdlib", "--param", "max-inline-insns-single=500"], "cc_path": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-gcc", "cxx_path": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-g++", "gdb_path": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-gdb", "prog_path": "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/build/lemon-pepper/firmware.elf", "svd_path": "/Users/mateijordache/.platformio/platforms/ststm32/misc/svd/STM32G431xx.svd", "compiler_type": "gcc", "targets": [{"name": "upload", "group": "Platform", "title": "Upload"}], "extra": {"flash_images": []}} \ No newline at end of file diff --git a/.pio/build/project.checksum b/.pio/build/project.checksum new file mode 100644 index 0000000..b336cdd --- /dev/null +++ b/.pio/build/project.checksum @@ -0,0 +1 @@ +dbed697f8cd0b1a4acd5fb4f169d0a718938cac9 \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md b/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3d352a1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: possible bug +assignees: '' + +--- + +This is a simplified template, feel free to change it if it does not fit your case. + +**Describe the bug** +A clear and concise description of what the bug is. + +**Describe the hardware setup** +For us it is very important to know what is the hardware setup you're using in order to be able to help more directly +- Which motor +- Which driver +- Which microcontroller +- Which position sensor +- Current sensing used? + +**IDE you are using** +- Arduino IDE +- Platformio +- Something else + +**Tried the Getting started guide? - if applicable** +Have you tried the getting started guide and at which step are you blocked in diff --git a/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md b/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..df55847 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +Description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +Description of what you want to happen. + +**Describe alternatives you've considered** +Description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.pio/libdeps/aioli-foc/Simple FOC/.github/workflows/ccpp.yml b/.pio/libdeps/aioli-foc/Simple FOC/.github/workflows/ccpp.yml new file mode 100644 index 0000000..5c50bb1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/.github/workflows/ccpp.yml @@ -0,0 +1,109 @@ +name: Library Compile +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update + project-type: library + build: + name: Test compiling + runs-on: ubuntu-latest + + strategy: + matrix: + arduino-boards-fqbn: + - arduino:avr:uno # arudino uno + - arduino:sam:arduino_due_x # arduino due + - arduino:avr:mega # arduino mega2650 + - arduino:avr:leonardo # arduino leonardo + - arduino:samd:nano_33_iot # samd21 + - adafruit:samd:adafruit_metro_m4 # samd51 + - esp32:esp32:esp32doit-devkit-v1 # esp32 + - esp32:esp32:esp32s2 # esp32s2 + - esp32:esp32:esp32s3 # esp32s3 + - STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # stm32 bluepill + - STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # stm32 nucleo + - STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX # stm32f405 - odrive + - STMicroelectronics:stm32:GenL4:pnum=GENERIC_L475RGTX # stm32l475 + - STMicroelectronics:stm32:Disco:pnum=B_G431B_ESC1 # B-G431-ESC1 + - arduino:mbed_rp2040:pico # rpi pico + + include: + - arduino-boards-fqbn: arduino:avr:uno # arudino uno - compiling almost all examples + sketch-names: '**.ino' + required-libraries: PciManager + sketches-exclude: full_control_serial, angle_control, bluepill_position_control, esp32_position_control, esp32_i2c_dual_bus_example, stm32_i2c_dual_bus_example, magnetic_sensor_spi_alt_example, osc_esp32_3pwm, osc_esp32_fullcontrol, nano33IoT_velocity_control, smartstepper_control,esp32_current_control_low_side, stm32_spi_alt_example, esp32_spi_alt_example, B_G431B_ESC1, odrive_example_spi, odrive_example_encoder, single_full_control_example, double_full_control_example, stm32_current_control_low_side, open_loop_velocity_6pwm + + - arduino-boards-fqbn: arduino:sam:arduino_due_x # arduino due - one full example + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: arduino:avr:leonardo # arduino leonardo - one full example + sketch-names: open_loop_position_example.ino + + - arduino-boards-fqbn: arduino:avr:mega # arduino mega2660 - one full example + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: arduino:samd:nano_33_iot # samd21 + sketch-names: nano33IoT_velocity_control.ino, smartstepper_control.ino + + - arduino-boards-fqbn: arduino:mbed_rp2040:pico # raspberry pi pico - one example + sketch-names: open_loop_position_example.ino + + - arduino-boards-fqbn: adafruit:samd:adafruit_metro_m4 # samd51 - one full example + platform-url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: esp32:esp32:esp32s2 # esp32s2 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: bldc_driver_3pwm_standalone.ino, stepper_driver_2pwm_standalone.ino, stepper_driver_4pwm_standalone + + - arduino-boards-fqbn: esp32:esp32:esp32s3 # esp32s3 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: esp32_position_control.ino, esp32_i2c_dual_bus_example.ino + + - arduino-boards-fqbn: esp32:esp32:esp32doit-devkit-v1 # esp32 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: esp32_position_control.ino, esp32_i2c_dual_bus_example.ino, esp32_current_control_low_side.ino, esp32_spi_alt_example.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # bluepill - hs examples + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: bluepill_position_control.ino, stm32_i2c_dual_bus_example.ino, stm32_spi_alt_example.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:Disco:pnum=B_G431B_ESC1 # B-G431-ESC1 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: B_G431B_ESC1.ino + build-properties: + B_G431B_ESC1: + -DHAL_OPAMP_MODULE_ENABLED + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX # stm32f405 - odrive + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: odrive_example_encoder.ino, odrive_example_spi.ino, stm32_current_control_low_side.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenL4:pnum=GENERIC_L475RGTX # stm32l475 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: single_full_control_example.ino, stm32_spi_alt_example.ino, double_full_control_example.ino, stm32_current_control_low_side.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # nucleo one full example + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: single_full_control_example.ino, stm32_spi_alt_example.ino, double_full_control_example.ino, stm32_current_control_low_side.ino + + + # Do not cancel all jobs / architectures if one job fails + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@master + - name: Compile all examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} + required-libraries: ${{ matrix.required-libraries }} + platform-url: ${{ matrix.platform-url }} + sketch-names: ${{ matrix.sketch-names }} + sketches-exclude: ${{ matrix.sketches-exclude }} + build-properties: ${{ toJson(matrix.build-properties) }} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/.piopm b/.pio/libdeps/aioli-foc/Simple FOC/.piopm new file mode 100644 index 0000000..d89173e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "Simple FOC", "version": "2.3.1", "spec": {"owner": "askuric", "id": 7229, "name": "Simple FOC", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/CITATION.cff b/.pio/libdeps/aioli-foc/Simple FOC/CITATION.cff new file mode 100644 index 0000000..ec37686 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/CITATION.cff @@ -0,0 +1,48 @@ +cff-version: 1.0.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Skuric" + given-names: "Antun" + orcid: "https://orcid.org/0000-0002-3323-4482" +- family-names: "Bank" + given-names: "Hasan Sinan" + orcid: "https://orcid.org/0000-0002-0626-2664" +- family-names: "Unger" + given-names: "Richard" +- family-names: "Williams" + given-names: "Owen" +- family-names: "González-Reyes" + given-names: "David" + orcid: "https://orcid.org/0000-0002-1535-3007" +title: "SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors" +version: 2.2.2 +doi: 10.21105/joss.04232 +date-released: 2022-06-26 +url: "https://github.com/simplefoc/Arduino-FOC" + +preferred-citation: + type: article + authors: + - family-names: "Skuric" + given-names: "Antun" + orcid: "https://orcid.org/0000-0002-3323-4482" + - family-names: "Bank" + given-names: "Hasan Sinan" + orcid: "https://orcid.org/0000-0002-0626-2664" + - family-names: "Unger" + given-names: "Richard" + - family-names: "Williams" + given-names: "Owen" + - family-names: "González-Reyes" + given-names: "David" + orcid: "https://orcid.org/0000-0002-1535-3007" + doi: "10.21105/joss.04232" + journal: "Journal of Open Source Software" + url: "https://doi.org/10.21105/joss.04232" + month: 6 + start: 4232 # First page number + end: 4232 # Last page number + title: "SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors" + volume: 7 + issue: 74 + year: 2022 diff --git a/.pio/libdeps/aioli-foc/Simple FOC/CODE_OF_CONDUCT.md b/.pio/libdeps/aioli-foc/Simple FOC/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..88763e9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@simplefoc.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.pio/libdeps/aioli-foc/Simple FOC/LICENSE b/.pio/libdeps/aioli-foc/Simple FOC/LICENSE new file mode 100644 index 0000000..4a32ba6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Antun Skuric + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.pio/libdeps/aioli-foc/Simple FOC/README.md b/.pio/libdeps/aioli-foc/Simple FOC/README.md new file mode 100644 index 0000000..747b0f3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/README.md @@ -0,0 +1,223 @@ +# SimpleFOClibrary - **Simple** Field Oriented Control (FOC) **library**
+### A Cross-Platform FOC implementation for BLDC and Stepper motors
based on the Arduino IDE and PlatformIO + +![Library Compile](https://github.com/simplefoc/Arduino-FOC/workflows/Library%20Compile/badge.svg) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/simplefoc/arduino-foc) +![GitHub Release Date](https://img.shields.io/github/release-date/simplefoc/arduino-foc?color=blue) +![GitHub commits since tagged version](https://img.shields.io/github/commits-since/simplefoc/arduino-foc/latest/dev) +![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/simplefoc/arduino-foc/dev) + +[![arduino-library-badge](https://www.ardu-badge.com/badge/Simple%20FOC.svg?)](https://www.ardu-badge.com/badge/Simple%20FOC.svg) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![status](https://joss.theoj.org/papers/4382445f249e064e9f0a7f6c1bb06b1d/status.svg)](https://joss.theoj.org/papers/4382445f249e064e9f0a7f6c1bb06b1d) + + +We live in very exciting times 😃! BLDC motors are entering the hobby community more and more and many great projects have already emerged leveraging their far superior dynamics and power capabilities. BLDC motors have numerous advantages over regular DC motors but they have one big disadvantage, the complexity of control. Even though it has become relatively easy to design and manufacture PCBs and create our own hardware solutions for driving BLDC motors the proper low-cost solutions are yet to come. One of the reasons for this is the apparent complexity of writing the BLDC driving algorithms, Field oriented control (FOC) being an example of one of the most efficient ones. +The solutions that can be found on-line are almost exclusively very specific for certain hardware configuration and the microcontroller architecture used. +Additionally, most of the efforts at this moment are still channeled towards the high-power applications of the BLDC motors and proper low-cost and low-power FOC supporting boards are very hard to find today and even may not exist.
+Therefore this is an attempt to: +- 🎯 Demystify FOC algorithm and make a robust but simple Arduino library: [Arduino *SimpleFOClibrary*](https://docs.simplefoc.com/arduino_simplefoc_library_showcase) + - Support as many motor + sensor + driver + mcu combinations out there +- 🎯 Develop a modular FOC supporting BLDC driver boards: + - ***NEW*** 📢: *Minimalistic* BLDC driver (<3Amps) : [SimpleFOCMini ](https://github.com/simplefoc/SimpleFOCMini). + - *Low-power* gimbal driver (<5Amps) : [*Arduino Simple**FOC**Shield*](https://docs.simplefoc.com/arduino_simplefoc_shield_showcase). + - *Medium-power* BLDC driver (<30Amps): [Arduino SimpleFOCPowerShield ](https://github.com/simplefoc/Arduino-SimpleFOC-PowerShield). + - See also [@byDagor](https://github.com/byDagor)'s *fully-integrated* ESP32 based board: [Dagor Brushless Controller](https://github.com/byDagor/Dagor-Brushless-Controller) + +> NEW RELEASE 📢 : SimpleFOClibrary v2.3.1 +> - Support for Arduino UNO R4 Minima (Renesas R7FA4M1 MCU - note UNO R4 WiFi is not yet supported) +> - Support setting PWM polarity on ESP32 (thanks to [@mcells](https://github.com/mcells)) +> - Expose I2C errors in MagneticSensorI2C (thanks to [@padok](https://github.com/padok)) +> - Improved default trig functions (sine, cosine) - faster, smaller +> - Overridable trig functions - plug in your own optimized versions +> - Bugfix: microseconds overflow in velocity mode [#287](https://github.com/simplefoc/Arduino-FOC/issues/287) +> - Bugfix: KV initialization ([5fc3128](https://github.com/simplefoc/Arduino-FOC/commit/5fc3128d282b65c141ca486327c6235089999627)) +> - And more bugfixes - see the [complete list of 2.3.1 fixes here](https://github.com/simplefoc/Arduino-FOC/issues?q=is%3Aissue+milestone%3A2.3.1_Release) +> - Change: simplify initFOC() API ([d57d32d](https://github.com/simplefoc/Arduino-FOC/commit/d57d32dd8715dbed4e476469bc3de0c052f1d531). [5231e5e](https://github.com/simplefoc/Arduino-FOC/commit/5231e5e1d044b0cc33ede67664b6ef2f9d0a8cdf), [10c5b87](https://github.com/simplefoc/Arduino-FOC/commit/10c5b872672cab72df16ddd738bbf09bcce95d28)) +> - Change: check for linked driver in currentsense and exit gracefully ([5ef4d9d](https://github.com/simplefoc/Arduino-FOC/commit/5ef4d9d5a92e03da0dd5af7f624243ab30f1b688)) +> - Compatibility with newest versions of Arduino framework for STM32, Renesas, ESP32, Atmel SAM, Atmel AVR, nRF52 and RP2040 + +## Arduino *SimpleFOClibrary* v2.3.1 + +

+ + + +

+ +This video demonstrates the *Simple**FOC**library* basic usage, electronic connections and shows its capabilities. + +### Features +- **Easy install**: + - Arduino IDE: Arduino Library Manager integration + - PlatformIO +- **Open-Source**: Full code and documentation available on github +- **Goal**: + - Support as many [sensor](https://docs.simplefoc.com/position_sensors) + [motor](https://docs.simplefoc.com/motors) + [driver](https://docs.simplefoc.com/drivers) + [current sense](https://docs.simplefoc.com/current_sense) combination as possible. + - Provide the up-to-date and in-depth documentation with API references and the examples +- **Easy to setup and configure**: + - Easy hardware configuration + - Each hardware component is a C++ object (easy to understand) + - Easy [tuning the control loops](https://docs.simplefoc.com/motion_control) + - [*Simple**FOC**Studio*](https://docs.simplefoc.com/studio) configuration GUI tool + - Built-in communication and monitoring +- **Cross-platform**: + - Seamless code transfer from one microcontroller family to another + - Supports multiple [MCU architectures](https://docs.simplefoc.com/microcontrollers): + - Arduino: UNO R4, UNO, MEGA, DUE, Leonardo, Nano, Nano33 .... + - STM32 + - ESP32 + - Teensy + - many more ... + +

+ + +## Documentation +Full API code documentation as well as example projects and step by step guides can be found on our [docs website](https://docs.simplefoc.com/). + +![image](https://user-images.githubusercontent.com/36178713/168475410-105e4e3d-082a-4015-98ff-d380c7992dfd.png) + + +## Getting Started +Depending on if you want to use this library as the plug and play Arduino library or you want to get insight in the algorithm and make changes there are two ways to install this code. + +- Full library installation [Docs](https://docs.simplefoc.com/library_download) +- PlatformIO [Docs](https://docs.simplefoc.com/library_platformio) + +### Arduino *SimpleFOClibrary* installation to Arduino IDE +#### Arduino Library Manager +The simplest way to get hold of the library is directly by using Arduino IDE and its integrated Library Manager. +- Open Arduino IDE and start Arduino Library Manager by clicking: `Tools > Manage Libraries...`. +- Search for `Simple FOC` library and install the latest version. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +#### Using Github website +- Go to the [github repository](https://github.com/simplefoc/Arduino-FOC) +- Click first on `Clone or Download > Download ZIP`. +- Unzip it and place it in `Arduino Libraries` folder. Windows: `Documents > Arduino > libraries`. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +#### Using terminal +- Open terminal and run +```sh +cd #Arduino libraries folder +git clone https://github.com/simplefoc/Arduino-FOC.git +``` +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +## Community and contributing + +For all the questions regarding the potential implementation, applications, supported hardware and similar please visit our [community forum](https://community.simplefoc.com) or our [discord server](https://discord.gg/kWBwuzY32n). + +It is always helpful to hear the stories/problems/suggestions of people implementing the code and you might find a lot of answered questions there already! + +### Github Issues & Pull requests + +Please do not hesitate to leave an issue if you have problems/advices/suggestions regarding the code! + +Pull requests are welcome, but let's first discuss them in [community forum](https://community.simplefoc.com)! + +If you'd like to contribute to this project but you are not very familiar with github, don't worry, let us know either by posting at the community forum , by posting a github issue or at our discord server. + +If you are familiar, we accept pull requests to the dev branch! + +## Arduino code example +This is a simple Arduino code example implementing the velocity control program of a BLDC motor with encoder. + +NOTE: This program uses all the default control parameters. + +```cpp +#include + +// BLDCMotor( pole_pairs ) +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver( pin_pwmA, pin_pwmB, pin_pwmC, enable (optional) ) +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +// Encoder(pin_A, pin_B, CPR) +Encoder encoder = Encoder(2, 3, 2048); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +void setup() { + // initialize encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage [V] + driver.voltage_power_supply = 12; + // initialise driver hardware + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::velocity; + // initialize motor + motor.init(); + + // align encoder and start FOC + motor.initFOC(); +} + +void loop() { + // FOC algorithm function + motor.loopFOC(); + + // velocity control loop function + // setting the target velocity or 2rad/s + motor.move(2); +} +``` +You can find more details in the [SimpleFOC documentation](https://docs.simplefoc.com/). + +## Example projects +Here are some of the *Simple**FOC**library* and *Simple**FOC**Shield* application examples. +

+ + + + + + + + + + + + +

+ + +## Citing the *SimpleFOC* + +We are very happy that *Simple**FOC**library* has been used as a component of several research project and has made its way to several scientific papers. We are hoping that this trend is going to continue as the project matures and becomes more robust! +A short resume paper about *Simple**FOC*** has been published in the Journal of Open Source Software: +

+ SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors.
+ A. Skuric, HS. Bank, R. Unger, O. Williams, D. González-Reyes
+Journal of Open Source Software, 7(74), 4232, https://doi.org/10.21105/joss.04232 +

+ +If you are interested in citing *Simple**FOC**library* or some other component of *Simple**FOC**project* in your research, we suggest you to cite our paper: + +```bib +@article{simplefoc2022, + doi = {10.21105/joss.04232}, + url = {https://doi.org/10.21105/joss.04232}, + year = {2022}, + publisher = {The Open Journal}, + volume = {7}, + number = {74}, + pages = {4232}, + author = {Antun Skuric and Hasan Sinan Bank and Richard Unger and Owen Williams and David González-Reyes}, + title = {SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors}, + journal = {Journal of Open Source Software} +} + +``` diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino new file mode 100644 index 0000000..51ac990 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino @@ -0,0 +1,109 @@ +/** + * B-G431B-ESC1 position motion control example with encoder + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(A_PHASE_UH, A_PHASE_UL, A_PHASE_VH, A_PHASE_VL, A_PHASE_WH, A_PHASE_WL); +LowsideCurrentSense currentSense = LowsideCurrentSense(0.003f, -64.0f/7.0f, A_OP1_OUT, A_OP2_OUT, A_OP3_OUT); + + +// encoder instance +Encoder encoder = Encoder(A_HALL2, A_HALL3, 2048, A_HALL1); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doIndex(){encoder.handleIndex();} + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.motion(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + currentSense.linkDriver(&driver); + + // current sensing + currentSense.init(); + // no need for aligning + currentSense.skip_align = true; + motor.linkCurrentSense(¤tSense); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + + // Motion control function + motor.move(); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h new file mode 100644 index 0000000..6f547ec --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h @@ -0,0 +1 @@ +-DHAL_OPAMP_MODULE_ENABLED \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino new file mode 100644 index 0000000..be853f0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino @@ -0,0 +1,115 @@ +/** + * + * STM32 Bluepill position motion control example with encoder + * + * The same example can be ran with any STM32 board - just make sure that put right pin numbers. + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM(IN1, IN2, IN3, enable(optional)) +BLDCDriver3PWM driver = BLDCDriver3PWM(PB6, PB7, PB8, PB5); +// BLDCDriver6PWM(IN1_H, IN1_L, IN2_H, IN2_L, IN3_H, IN3_L, enable(optional)) +//BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, PB12); + +// encoder instance +Encoder encoder = Encoder(PA8, PA9, 8192, PA10); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doI(){encoder.handleIndex();} + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB, doI); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino new file mode 100644 index 0000000..e27f1e7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino @@ -0,0 +1,115 @@ +/** + * + * STM32 Bluepill position motion control example with magnetic sensor + * + * The same example can be ran with any STM32 board - just make sure that put right pin numbers. + * + */ +#include + +// SPI Magnetic sensor instance (AS5047U example) +// MISO PA7 +// MOSI PA6 +// SCK PA5 +MagneticSensorSPI sensor = MagneticSensorSPI(PA4, 14, 0x3FFF); + +// I2C Magnetic sensor instance (AS5600 example) +// make sure to use the pull-ups!! +// SDA PB7 +// SCL PB6 +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM(IN1, IN2, IN3, enable(optional)) +BLDCDriver3PWM driver = BLDCDriver3PWM(PB6, PB7, PB8, PB5); +// BLDCDriver6PWM(IN1_H, IN1_L, IN2_H, IN2_L, IN3_H, IN3_L, enable(optional)) +//BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, PB12); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 40; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..842f383 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,130 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 9 +#define INH_B 10 +#define INH_C 11 + +#define EN_GATE 7 +#define M_PWM A1 +#define M_OC A2 +#define OC_ADJ A3 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..a5b72f8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,129 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 3 +#define INH_B 5 +#define INH_C 9 +#define INL_A 11 +#define INL_B 6 +#define INL_C 10 + +#define EN_GATE 7 +#define M_PWM A1 +#define M_OC A2 +#define OC_ADJ A3 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(INH_A,INH_A, INH_B,INH_B, INH_C,INL_C, EN_GATE); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - disable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM, LOW); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino new file mode 100644 index 0000000..84033b4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino @@ -0,0 +1,167 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 21 +#define INH_B 19 +#define INH_C 18 + +#define EN_GATE 5 +#define M_PWM 25 +#define M_OC 26 +#define OC_ADJ 12 +#define OC_GAIN 14 + +#define IOUTA 34 +#define IOUTB 35 +#define IOUTC 32 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// DRV8302 board has 0.005Ohm shunt resistors and the gain of 12.22 V/V +LowsideCurrentSense cs = LowsideCurrentSense(0.005f, 12.22f, IOUTA, IOUTB, IOUTC); + +// encoder instance +Encoder encoder = Encoder(22, 23, 500); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + pinMode(OC_GAIN,OUTPUT); + digitalWrite(OC_GAIN,LOW); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.pwm_frequency = 15000; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + cs.linkDriver(&driver); + + // align voltage + motor.voltage_sensor_align = 0.5; + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + motor.motion_downsample = 0.0; + + // velocity loop PID + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 5.0; + // Low pass filtering time constant + motor.LPF_velocity.Tf = 0.02; + // angle loop PID + motor.P_angle.P = 20.0; + // Low pass filtering time constant + motor.LPF_angle.Tf = 0.0; + // current q loop PID + motor.PID_current_q.P = 3.0; + motor.PID_current_q.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_q.Tf = 0.02; + // current d loop PID + motor.PID_current_d.P = 3.0; + motor.PID_current_d.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_d.Tf = 0.02; + + // Limits + motor.velocity_limit = 100.0; // 100 rad/s velocity limit + motor.voltage_limit = 12.0; // 12 Volt limit + motor.current_limit = 2.0; // 2 Amp current limit + + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; // monitor the two currents d and q + motor.monitor_downsample = 1000; + + // initialise motor + motor.init(); + + cs.init(); + // driver 8302 has inverted gains on all channels + cs.gain_a *=-1; + cs.gain_b *=-1; + cs.gain_c *=-1; + motor.linkCurrentSense(&cs); + + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // monitoring the state variables + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino new file mode 100644 index 0000000..3e3f04a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino @@ -0,0 +1,167 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A PA8 +#define INH_B PA9 +#define INH_C PA10 + +#define EN_GATE PB7 +#define M_PWM PB4 +#define M_OC PB3 +#define OC_ADJ PB6 +#define OC_GAIN PB5 + +#define IOUTA PA0 +#define IOUTB PA1 +#define IOUTC PA2 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// DRV8302 board has 0.005Ohm shunt resistors and the gain of 12.22 V/V +LowsideCurrentSense cs = LowsideCurrentSense(0.005f, 12.22f, IOUTA, IOUTB, IOUTC); + +// encoder instance +Encoder encoder = Encoder(PB14, PB15, 2048); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + pinMode(OC_GAIN,OUTPUT); + digitalWrite(OC_GAIN,LOW); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 19; + driver.pwm_frequency = 15000; // suggested under 18khz + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + cs.linkDriver(&driver); + + // align voltage + motor.voltage_sensor_align = 0.5; + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + motor.motion_downsample = 0.0; + + // velocity loop PID + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 5.0; + // Low pass filtering time constant + motor.LPF_velocity.Tf = 0.02; + // angle loop PID + motor.P_angle.P = 20.0; + // Low pass filtering time constant + motor.LPF_angle.Tf = 0.0; + // current q loop PID + motor.PID_current_q.P = 3.0; + motor.PID_current_q.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_q.Tf = 0.02; + // current d loop PID + motor.PID_current_d.P = 3.0; + motor.PID_current_d.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_d.Tf = 0.02; + + // Limits + motor.velocity_limit = 100.0; // 100 rad/s velocity limit + motor.voltage_limit = 12.0; // 12 Volt limit + motor.current_limit = 2.0; // 2 Amp current limit + + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; // monitor the two currents d and q + motor.monitor_downsample = 0; + + // initialise motor + motor.init(); + + cs.init(); + // driver 8302 has inverted gains on all channels + cs.gain_a *=-1; + cs.gain_b *=-1; + cs.gain_c *=-1; + motor.linkCurrentSense(&cs); + + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // monitoring the state variables + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino new file mode 100644 index 0000000..f6dac94 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino @@ -0,0 +1,107 @@ +/** + * ESP32 position motion control example with encoder + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27, 7); + +// encoder instance +Encoder encoder = Encoder(4, 2, 1024); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino new file mode 100644 index 0000000..f025895 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino @@ -0,0 +1,112 @@ +/** + * ESP32 position motion control example with magnetic sensor + */ +#include + +// SPI Magnetic sensor instance (AS5047U example) +// MISO 12 +// MOSI 9 +// SCK 14 +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 15); + +// I2C Magnetic sensor instance (AS5600 example) +// make sure to use the pull-ups!! +// SDA 21 +// SCL 22 +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); + +// Analog output Magnetic sensor instance (AS5600) +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27, 7); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 40; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino new file mode 100644 index 0000000..5b5c8e4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino @@ -0,0 +1,127 @@ +/** + * + * HMBGC position motion control example with encoder + * + * - Motor is connected the MOT1 connector (MOT1 9,10,11; MOT2 3,5,6) + * - Encoder is connected to A0 and A1 + * + * This board doesn't have any interrupt pins so we need to run all the encoder channels with the software interrupt library + * - For this example we use: PciManager library : https://github.com/prampec/arduino-pcimanager + * + * See docs.simplefoc.com for more info. + * + */ +#include +// software interrupt library +#include +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11); + +// encoder instance +Encoder encoder = Encoder(A0, A1, 2048); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise encoder hardware + encoder.init(); + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino new file mode 100644 index 0000000..9c9655f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino @@ -0,0 +1,110 @@ +/** + * + * HMBGC torque control example using voltage control loop. + * + * - Motor is connected the MOT1 connector (MOT1 9,10,11; MOT2 3,5,6) + * - Encoder is connected to A0 and A1 + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. position motion control example with encoder + * + * NOTE: + * > HMBGC doesn't have any interrupt pins so we need to run all the encoder channels with the software interrupt library + * > - For this example we use: PciManager library : https://github.com/prampec/arduino-pcimanager + * + * See docs.simplefoc.com for more info. + * + */ +#include +// software interrupt library +#include +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11); + +// encoder instance +Encoder encoder = Encoder(A0, A1, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial for motor init + // comment out if not needed + motor.useMonitoring(Serial); + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino new file mode 100644 index 0000000..b89c215 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino @@ -0,0 +1,134 @@ +/* + Odrive robotics' hardware is one of the best BLDC motor foc supporting hardware out there. + + This is an example code that can be directly uploaded to the Odrive using the SWD programmer. + This code uses an encoder with 500 cpr and a BLDC motor with 7 pole pairs connected to the M0 interface of the Odrive. + + This is a short template code and the idea is that you are able to adapt to your needs not to be a complete solution. :D +*/ +#include + +// Odrive M0 motor pinout +#define M0_INH_A PA8 +#define M0_INH_B PA9 +#define M0_INH_C PA10 +#define M0_INL_A PB13 +#define M0_INL_B PB14 +#define M0_INL_C PB15 +// M0 currnets +#define M0_IB PC0 +#define M0_IC PC1 +// Odrive M0 encoder pinout +#define M0_ENC_A PB4 +#define M0_ENC_B PB5 +#define M0_ENC_Z PC9 + + +// Odrive M1 motor pinout +#define M1_INH_A PC6 +#define M1_INH_B PC7 +#define M1_INH_C PC8 +#define M1_INL_A PA7 +#define M1_INL_B PB0 +#define M1_INL_C PB1 +// M0 currnets +#define M1_IB PC2 +#define M1_IC PC3 +// Odrive M1 encoder pinout +#define M1_ENC_A PB6 +#define M1_ENC_B PB7 +#define M1_ENC_Z PC15 + +// M1 & M2 common enable pin +#define EN_GATE PB12 + +// SPI pinout +#define SPI3_SCL PC10 +#define SPI3_MISO PC11 +#define SPI3_MOSO PC12 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver6PWM driver = BLDCDriver6PWM(M0_INH_A,M0_INL_A, M0_INH_B,M0_INL_B, M0_INH_C,M0_INL_C, EN_GATE); + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +// low side current sensing define +// 0.0005 Ohm resistor +// gain of 10x +// current sensing on B and C phases, phase A not connected +LowsideCurrentSense current_sense = LowsideCurrentSense(0.0005f, 10.0f, _NC, M0_IB, M0_IC); + +Encoder encoder = Encoder(M0_ENC_A, M0_ENC_B, 500,M0_ENC_Z); +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doI(){encoder.handleIndex();} + +void setup(){ + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 20000; + // power supply voltage [V] + driver.voltage_power_supply = 20; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 20; + // driver init + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB, doI); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // max voltage allowed for motion control + motor.voltage_limit = 8.0; + // alignment voltage limit + motor.voltage_sensor_align = 0.5; + + + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; + motor.monitor_downsample = 1000; + + // add target command T + command.add('M', doMotor, "motor M0"); + + // initialise motor + motor.init(); + + // link the driver + current_sense.linkDriver(&driver); + // init the current sense + current_sense.init(); + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // init FOC + motor.initFOC(); + delay(1000); +} + +void loop(){ + + // foc loop + motor.loopFOC(); + // motion control + motor.move(); + // monitoring + motor.monitor(); + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino new file mode 100644 index 0000000..2aed6bf --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino @@ -0,0 +1,132 @@ +/* + Odrive robotics' hardware is one of the best BLDC motor foc supporting hardware out there. + + This is an example code that can be directly uploaded to the Odrive using the SWD programmer. + This code uses an magnetic spi sensor AS5047 and a BLDC motor with 11 pole pairs connected to the M0 interface of the Odrive. + + This is a short template code and the idea is that you are able to adapt to your needs not to be a complete solution. :D +*/ +#include + +// Odrive M0 motor pinout +#define M0_INH_A PA8 +#define M0_INH_B PA9 +#define M0_INH_C PA10 +#define M0_INL_A PB13 +#define M0_INL_B PB14 +#define M0_INL_C PB15 +// M0 currnets +#define M0_IB PC0 +#define M0_IC PC1 +// Odrive M0 encoder pinout +#define M0_ENC_A PB4 +#define M0_ENC_B PB5 +#define M0_ENC_Z PC9 + + +// Odrive M1 motor pinout +#define M1_INH_A PC6 +#define M1_INH_B PC7 +#define M1_INH_C PC8 +#define M1_INL_A PA7 +#define M1_INL_B PB0 +#define M1_INL_C PB1 +// M0 currnets +#define M1_IB PC2 +#define M1_IC PC3 +// Odrive M1 encoder pinout +#define M1_ENC_A PB6 +#define M1_ENC_B PB7 +#define M1_ENC_Z PC15 + +// M1 & M2 common enable pin +#define EN_GATE PB12 + +// SPI pinout +#define SPI3_SCL PC10 +#define SPI3_MISO PC11 +#define SPI3_MOSO PC12 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(M0_INH_A,M0_INL_A, M0_INH_B,M0_INL_B, M0_INH_C,M0_INL_C, EN_GATE); + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +// low side current sensing define +// 0.0005 Ohm resistor +// gain of 10x +// current sensing on B and C phases, phase A not connected +LowsideCurrentSense current_sense = LowsideCurrentSense(0.0005f, 10.0f, _NC, M0_IB, M0_IC); + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, M0_ENC_A); +SPIClass SPI_3(SPI3_MOSO, SPI3_MISO, SPI3_SCL); + +void setup(){ + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 20000; + // power supply voltage [V] + driver.voltage_power_supply = 20; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 20; + // driver init + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // initialise magnetic sensor hardware + sensor.init(&SPI_3); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // max voltage allowed for motion control + motor.voltage_limit = 8.0; + // alignment voltage limit + motor.voltage_sensor_align = 0.5; + + + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; + motor.monitor_downsample = 1000; + + // add target command T + command.add('M', doMotor, "motor M0"); + + // initialise motor + motor.init(); + + // link the driver + current_sense.linkDriver(&driver); + // init the current sense + current_sense.init(); + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // init FOC + motor.initFOC(); + delay(1000); +} + +void loop(){ + + // foc loop + motor.loopFOC(); + // motion control + motor.move(); + // monitoring + motor.monitor(); + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md new file mode 100644 index 0000000..d1a5258 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md @@ -0,0 +1,62 @@ + +# SAMD Support + +SimpleFOC supports many SAMD21 MCUs, really any SAMD21 supported by Arduino core should work. + +## Pin assignments + +The SAMD chips have some very powerful PWM features, but do not have flexible pin assignments. + +You should be able to use *most* (but not all!), pin combinations for attaching your motor's PWM pins. Please ignore the board descriptions and pinout diagrammes regarding PWM-pins on SAMD boards. They are pretty much all incorrect to varying degrees of awfulness. + +On SAMD we use TCC and TC timer peripherals (built into the SAMD chip) to control the PWM. Depending on the chip there are various timer units, whose PWM outputs are attached to various different pins, and it is all very complicated. Luckily SimpleFOC sets it all up automatically *if* there is a compatible configuration for those pins. + +Not all timers are created equal. The TCC timers are pretty awesome for PWM motor control, while the TC timers are just ok for the job. So to get best performance, you want to use just TCC timer pins if you can. + +By enabling + +``` + #define SIMPLEFOC_SAMD_DEBUG +``` + +in drivers/hardware_specific/samd_mcu.cpp
+you will see a table of pin assignments printed on the serial console, as well as the timers SimpleFOC was able to find and configure on the pins you specified. You can use this to optimize your choice of pins if you want. + +You can configure up to 12 pins for PWM motor control, i.e. 6x 2-PWM motors, 4x 3-PWM motors, 3x 4-PWM motors or 2x 6-PWM motors. + +## PWM control modes + +All modes (3-PWM, 6-PWM, Stepper 2-PWM and Stepper 4-PWM) are supported. + +For 2-, 3- amd 4- PWM, any valid pin-combinations can be used. If you stick to TCC timers rather than using TC timers, then you'll get getter PWM waveforms. If you use pins which are all on the same TCC unit, you'll get the best result, with the PWM signals all perfectly aligned as well. + +For 6-PWM, the situation is much more complicated:
+TC timers cannot be used for 6-PWM, only TCC timers. + +For Hardware Dead-Time insertion, you must use H and L pins for one phase from the same TCC unit, and on the same channel, but using complementary WOs (Waveform Outputs, i.e. PWM output pins). Check the table to find pins on the same channel (like TCC0-0) but complementary WOs (like TCC0-0[0] and TCC0-0[4] or TCC1-0[0] and TCC1-0[2]). + +For Software Dead-Time insertion, you must use the same TCC and different channels for the H and L pins of the same phase. + +Note: in all of the above note that you *cannot* set the timers or WOs used - they are fixed, and determined by the pins you selected. SimpleFOC will find the best combination of timers given the pins, trying to use TCC timers before TC, and trying to keep things on the same timers as much as possible. If you configure multiple motors, it will take into account the pins already assigned to other motors. +So it is matter of choosing the right pins, nothing else. + +Note also: Unfortunately you can't set the PWM frequency. It is currently fixed at 24KHz. This is a tradeoff between limiting PWM resolution vs +increasing frequency, and also due to keeping the pin assignemts flexible, which would not be possible if we ran the timers at different rates. + +## Status + +Currently, SAMD21 is supported, and SAMD51 is unsupported. SAMD51 support is in progress. + +Boards tested: + + * Arduino Nano 33 IoT + * Arduino MKR1000 + * Arduino MKR1010 Wifi + * Seeduino XIAO + * Feather M0 Basic + +Environments tested: + + * Arduino IDE + * Arduino Pro IDE + * Sloeber diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino new file mode 100644 index 0000000..70ec28b --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino @@ -0,0 +1,64 @@ + +// show the infos for SAMD pin assignment on serial console +// set this #define SIMPLEFOC_SAMD_DEBUG in drivers/hardware_specific/samd21_mcu.h + + +#include "Arduino.h" +#include +#include + +// this is for an AS5048B absolute magnetic encoder on I2C address 0x41 +MagneticSensorI2C sensor = MagneticSensorI2C(0x41, 14, 0xFE, 8); + +// small BLDC gimbal motor, 7 pole-pairs +BLDCMotor motor = BLDCMotor(7); +// 3-PWM driving on pins 6, 5 and 8 - these are all on the same timer unit (TCC0), but different channels +BLDCDriver3PWM driver = BLDCDriver3PWM(6,5,8); + +// velocity set point variable +float target_velocity = 2.0f; +// instantiate the commander +Commander command = Commander(SerialUSB); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("Initializing..."); + + sensor.init(); + Wire.setClock(400000); + motor.linkSensor(&sensor); + driver.voltage_power_supply = 9; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity; + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0.001f; + motor.PID_velocity.output_ramp = 1000; + motor.LPF_velocity.Tf = 0.01f; + motor.voltage_limit = 9; + //motor.P_angle.P = 20; + motor.init(); + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + delay(100); +} + + + +void loop() { +// Serial.print("Sensor: "); +// Serial.println(sensor.getAngle()); + motor.loopFOC(); + motor.move(target_velocity); + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..eb52f51 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,102 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8, 4, 7); + +// encoder instance +Encoder encoder = Encoder(10, 11, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +InlineCurrentSense current_sense = InlineCurrentSense(0.001f, 50.0f, A0, A1); + +// commander communication instance +Commander command = Commander(Serial); +// void doMotor(char* cmd){ command.motor(&motor, cmd); } +void doTarget(char* cmd){ command.scalar(&motor.target, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 20; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + motor.voltage_sensor_align = 1; + // set control loop type to be used + motor.torque_controller = TorqueControlType::foc_current; + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // subscribe motor to the commander + // command.add('M', doMotor, "motor"); + command.add('T', doTarget, "target"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino new file mode 100644 index 0000000..9102c89 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino @@ -0,0 +1,125 @@ +/** + * + * SimpleFOCMini motor control example + * + * For Arduino UNO, the most convenient way to use the board is to stack it to the pins: + * - 12 - GND + * - 11 - IN1 + * - 10 - IN2 + * - 9 - IN3 + * - 8 - EN + * + * For other boards with UNO headers but more PWM channles such as esp32, nucleo-64, samd51 metro etc, the best way to most convenient pinout is: + * - GND - GND + * - 13 - IN1 + * - 12 - IN2 + * - 11 - IN3 + * - 9 - EN + * + * For the boards without arduino uno headers, the choice of pinout is a lot less constrained. + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(11, 10, 9, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +void setup() { + // if SimpleFOCMini is stacked in arduino headers + // on pins 12,11,10,9,8 + // pin 12 is used as ground + pinMode(12,OUTPUT); + pinMode(12,LOW); + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command M + command.add('M', doMotor, "motor"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino new file mode 100644 index 0000000..c048956 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino @@ -0,0 +1,118 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor1 = BLDCMotor(11); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(9, 5, 6, 8); + +// BLDC motor & driver instance +BLDCMotor motor2 = BLDCMotor(11); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(3, 10, 11, 7); + +// encoder instance +Encoder encoder1 = Encoder(2, A3, 500); +// channel A and B callbacks +void doA1(){encoder1.handleA();} +void doB1(){encoder1.handleB();} + +// encoder instance +Encoder encoder2 = Encoder(A1, A2, 500); +// channel A and B callbacks +void doA2(){encoder2.handleA();} +void doB2(){encoder2.handleB();} + +// commander communication instance +Commander command = Commander(Serial); +void doMotor1(char* cmd){ command.motor(&motor1, cmd); } +void doMotor2(char* cmd){ command.motor(&motor2, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder1.init(); + encoder1.enableInterrupts(doA1, doB1); + // initialize encoder sensor hardware + encoder2.init(); + encoder2.enableInterrupts(doA2, doB2); + // link the motor to the sensor + motor1.linkSensor(&encoder1); + motor2.linkSensor(&encoder2); + + + // driver config + // power supply voltage [V] + driver1.voltage_power_supply = 12; + driver1.init(); + // link driver + motor1.linkDriver(&driver1); + // power supply voltage [V] + driver2.voltage_power_supply = 12; + driver2.init(); + // link driver + motor2.linkDriver(&driver2); + + // set control loop type to be used + motor1.controller = MotionControlType::torque; + motor2.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor1.PID_velocity.P = 0.05f; + motor1.PID_velocity.I = 1; + motor1.PID_velocity.D = 0; + // default voltage_power_supply + motor1.voltage_limit = 12; + // contoller configuration based on the controll type + motor2.PID_velocity.P = 0.05f; + motor2.PID_velocity.I = 1; + motor2.PID_velocity.D = 0; + // default voltage_power_supply + motor2.voltage_limit = 12; + + // angle loop velocity limit + motor1.velocity_limit = 20; + motor2.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor1.useMonitoring(Serial); + motor2.useMonitoring(Serial); + + // initialise motor + motor1.init(); + // align encoder and start FOC + motor1.initFOC(); + + // initialise motor + motor2.init(); + // align encoder and start FOC + motor2.initFOC(); + + // set the inital target value + motor1.target = 2; + motor2.target = 2; + + // subscribe motor to the commander + command.add('A', doMotor1, "motor 1"); + command.add('B', doMotor2, "motor 2"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Double motor sketch ready.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor1.loopFOC(); + motor2.loopFOC(); + + // iterative function setting the outter loop target + motor1.move(); + motor2.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..10e26c9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,89 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// commander communication instance +Commander command = Commander(Serial); +void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino new file mode 100644 index 0000000..b40ab62 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino @@ -0,0 +1,144 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor1 = BLDCMotor(11); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(5, 10, 6, 8); + +// BLDC motor & driver instance +BLDCMotor motor2 = BLDCMotor(11); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(3, 9, 11, 7); + +// encoder instance +Encoder encoder1 = Encoder(12, 2, 500); +// channel A and B callbacks +void doA1(){encoder1.handleA();} +void doB1(){encoder1.handleB();} + +// encoder instance +Encoder encoder2 = Encoder(A5, A4, 500); +// channel A and B callbacks +void doA2(){encoder2.handleA();} +void doB2(){encoder2.handleB();} + + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense1 = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense2 = InlineCurrentSense(0.01f, 50.0f, A1, A3); + +// commander communication instance +Commander command = Commander(Serial); +// void doMotor1(char* cmd){ command.motor(&motor1, cmd); } +// void doMotor2(char* cmd){ command.motor(&motor2, cmd); } +void doTarget1(char* cmd){ command.scalar(&motor1.target, cmd); } +void doTarget2(char* cmd){ command.scalar(&motor2.target, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder1.init(); + encoder1.enableInterrupts(doA1, doB1); + // initialize encoder sensor hardware + encoder2.init(); + encoder2.enableInterrupts(doA2, doB2); + // link the motor to the sensor + motor1.linkSensor(&encoder1); + motor2.linkSensor(&encoder2); + + + // driver config + // power supply voltage [V] + driver1.voltage_power_supply = 12; + driver1.init(); + // link driver + motor1.linkDriver(&driver1); + // link current sense and the driver + current_sense1.linkDriver(&driver1); + + // power supply voltage [V] + driver2.voltage_power_supply = 12; + driver2.init(); + // link driver + motor2.linkDriver(&driver2); + // link current sense and the driver + current_sense2.linkDriver(&driver2); + + // set control loop type to be used + motor1.controller = MotionControlType::torque; + motor2.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor1.PID_velocity.P = 0.05f; + motor1.PID_velocity.I = 1; + motor1.PID_velocity.D = 0; + // default voltage_power_supply + motor1.voltage_limit = 12; + // contoller configuration based on the controll type + motor2.PID_velocity.P = 0.05f; + motor2.PID_velocity.I = 1; + motor2.PID_velocity.D = 0; + // default voltage_power_supply + motor2.voltage_limit = 12; + + // angle loop velocity limit + motor1.velocity_limit = 20; + motor2.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor1.useMonitoring(Serial); + motor2.useMonitoring(Serial); + + + // current sense init and linking + current_sense1.init(); + motor1.linkCurrentSense(¤t_sense1); + // current sense init and linking + current_sense2.init(); + motor2.linkCurrentSense(¤t_sense2); + + // initialise motor + motor1.init(); + // align encoder and start FOC + motor1.initFOC(); + + // initialise motor + motor2.init(); + // align encoder and start FOC + motor2.initFOC(); + + // set the inital target value + motor1.target = 2; + motor2.target = 2; + + // subscribe motor to the commander + // command.add('A', doMotor1, "motor 1"); + // command.add('B', doMotor2, "motor 2"); + command.add('A', doTarget1, "target 1"); + command.add('B', doTarget2, "target 2"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motors ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor1.loopFOC(); + motor2.loopFOC(); + + // iterative function setting the outter loop target + motor1.move(); + motor2.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..76a7296 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,101 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// commander communication instance +Commander command = Commander(Serial); +void doMotion(char* cmd){ command.motion(&motor, cmd); } +// void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('T', doMotion, "motion control"); + // command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..0259328 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,101 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(6, 10, 5, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +// ACS712-05B has the resolution of 0.185mV per Amp +InlineCurrentSense current_sense = InlineCurrentSense(1.0f, 0.185f, A0, A2); + +// commander communication instance +Commander command = Commander(Serial); +void doMotion(char* cmd){ command.motion(&motor, cmd); } +// void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // controller configuration based on the control type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('T', doMotion, "motion control"); + // command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino new file mode 100644 index 0000000..80f2fe6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino @@ -0,0 +1,61 @@ +/** + * Smart Stepper support with SimpleFOClibrary + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, A2); + +// Stepper motor & driver instance +StepperMotor motor = StepperMotor(50); +int in1[2] = {5, 6}; +int in2[2] = {A4, 7}; +StepperDriver2PWM driver = StepperDriver2PWM(4, in1, 9, in2); + +// instantiate the commander +Commander command = Commander(SerialUSB); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with SerialUSB + SerialUSB.begin(115200); + // comment out if not needed + motor.useMonitoring(SerialUSB); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command M + command.add('M', doMotor, "my motor"); + + SerialUSB.println(F("Motor ready.")); + SerialUSB.println(F("Set the target voltage using Serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..7f59551 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,41 @@ +// 6pwm standalone example code for Teensy 3.x boards +#include + + +// BLDC driver instance +// using FTM0 timer +BLDCDriver6PWM driver = BLDCDriver6PWM(22,23, 9,10, 6,20, 8); +// using FTM3 timer - available on Teensy3.5 and Teensy3.6 +// BLDCDriver6PWM driver = BLDCDriver6PWM(2,14, 7,8, 35,36, 8); + +void setup() { + Serial.begin(115200); + // Enable debugging + // Driver init will show debugging output + SimpleFOCDebug::enable(&Serial); + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 30000; + // dead zone percentage of the duty cycle - default 0.02 - 2% + driver.dead_zone=0.02; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino new file mode 100644 index 0000000..40924ee --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino @@ -0,0 +1,65 @@ +// 6pwm openloop velocity example +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// using FTM0 timer +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +BLDCDriver6PWM driver = BLDCDriver6PWM(22,23, 9,10, 6,20, 8); +// using FTM3 timer - available on Teensy3.5 and Teensy3.6 +// BLDCDriver6PWM driver = BLDCDriver6PWM(2,14, 7,8, 35,36, 8); + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&motor.target, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + //initial motor target + motor.target=0; + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..f3df3db --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,57 @@ +// 6pwm standalone example code for Teensy 4.x boards +// +// Teensy4.x 6pwm driver generates a 6pwm signal using FlexTimers and it doesn't support the QuadTimers +// - Each high-low pair of the 6pwm has to be A and B channels of the same FlexTimer and to the same submodule +// +// List of available teensy 4.1 pins with their respective submodules and channels +// FlexPWM(timer number)_(submodule)_(channel) +// FlexPWM4_2_A pin 2 +// FlexPWM4_2_B pin 3 +// FlexPWM1_3_B pin 7 +// FlexPWM1_3_A pin 8 +// FlexPWM2_2_A pin 6 +// FlexPWM2_2_B pin 9 +// FlexPWM3_1_B pin 28 +// FlexPWM3_1_A pin 29 +// FlexPWM2_3_A pin 36 +// FlexPWM2_3_B pin 37 +#include + + +// BLDC driver instance +// make sure to provide channel A for high side and channel B for low side +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +// Example configuration +BLDCDriver6PWM driver = BLDCDriver6PWM(2,3, 6,9, 8,7); + +void setup() { + Serial.begin(115200); + // Enable debugging + // Driver init will show debugging output + SimpleFOCDebug::enable(&Serial); + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 30000; + // dead zone percentage of the duty cycle - default 0.02 - 2% + driver.dead_zone=0.02; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino new file mode 100644 index 0000000..9c6eea0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino @@ -0,0 +1,80 @@ +// 6pwm openloop velocity example +// +// Teensy4.x 6pwm driver generates a 6pwm signal using FlexTimers and it doesn't support the QuadTimers +// - Each high-low pair of the 6pwm has to be A and B channels of the same FlexTimer and to the same submodule +// +// List of available teensy 4.1 pins with their respective submodules and channels +// FlexPWM(timer number)_(submodule)_(channel) +// FlexPWM4_2_A pin 2 +// FlexPWM4_2_B pin 3 +// FlexPWM1_3_B pin 7 +// FlexPWM1_3_A pin 8 +// FlexPWM2_2_A pin 6 +// FlexPWM2_2_B pin 9 +// FlexPWM3_1_B pin 28 +// FlexPWM3_1_A pin 29 +// FlexPWM2_3_A pin 36 +// FlexPWM2_3_B pin 37 +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// make sure to provide channel A for high side and channel B for low side +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +// Example configuration +BLDCDriver6PWM driver = BLDCDriver6PWM(2,3, 6,9, 8,7); + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&motor.target, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + //initial motor target + motor.target=0; + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino new file mode 100644 index 0000000..5a915dd --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino @@ -0,0 +1,70 @@ +// Open loop motor control example + #include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM driver = BLDCDriver3PWM(pwmA, pwmB, pwmC, Enable(optional)); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +//target variable +float target_position = 0; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_position, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } +void doVelocity(char* cmd) { command.scalar(&motor.velocity_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + // limit/set the velocity of the transition in between + // target angles + motor.velocity_limit = 5; // [rad/s] cca 50rpm + // open loop control config + motor.controller = MotionControlType::angle_openloop; + + // init motor hardware + motor.init(); + + // add target command T + command.add('T', doTarget, "target angle"); + command.add('L', doLimit, "voltage limit"); + command.add('V', doLimit, "movement velocity"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target position [rad]"); + _delay(1000); +} + +void loop() { + // open loop angle movements + // using motor.voltage_limit and motor.velocity_limit + // angles can be positive or negative, negative angles correspond to opposite motor direction + motor.move(target_position); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino new file mode 100644 index 0000000..43fc6f7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino @@ -0,0 +1,68 @@ +// Open loop motor control example +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM driver = BLDCDriver3PWM(pwmA, pwmB, pwmC, Enable(optional)); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + + +//target variable +float target_velocity = 0; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // current = voltage / resistance, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit and motor.velocity_limit + // to turn the motor "backwards", just set a negative target_velocity + motor.move(target_velocity); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino new file mode 100644 index 0000000..ccb3980 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino @@ -0,0 +1,129 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and encoder + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + * + * NOTE : + * > Arduino UNO example code for running velocity motion control using an encoder with index significantly + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the encoder.enableInterrupts(doA,doB,doIndex) and avoid using PciManger + * + * > If you don't want to use index pin initialize the encoder class without index pin number: + * > For example: + * > - Encoder encoder = Encoder(2, 3, 8192); + * > and initialize interrupts like this: + * > - encoder.enableInterrupts(doA,doB) + * + * Check the docs.simplefoc.com for more info about the possible encoder configuration. + * + */ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino new file mode 100644 index 0000000..4bd7163 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino @@ -0,0 +1,133 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and hall sensor + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + * + * NOTE : + * > This is for Arduino UNO example code for running angle motion control specifically + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the sensr.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenC(sensor.pinC, doC); + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino new file mode 100644 index 0000000..752030c --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino @@ -0,0 +1,112 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and magnetic sensor + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 20; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino new file mode 100644 index 0000000..c912340 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino @@ -0,0 +1,107 @@ +/** + * + * Torque control example using current control loop. + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// current sensor +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// current set point variable +float target_current = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_current, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // current sense init hardware + current_sense.init(); + // link the current sense to the motor + motor.linkCurrentSense(¤t_sense); + + // set torque mode: + // TorqueControlType::dc_current + // TorqueControlType::voltage + // TorqueControlType::foc_current + motor.torque_controller = TorqueControlType::foc_current; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // foc currnet control parameters (Arduino UNO/Mega) + motor.PID_current_q.P = 5; + motor.PID_current_q.I= 300; + motor.PID_current_d.P= 5; + motor.PID_current_d.I = 300; + motor.LPF_current_q.Tf = 0.01f; + motor.LPF_current_d.Tf = 0.01f; + // foc currnet control parameters (stm/esp/due/teensy) + // motor.PID_current_q.P = 5; + // motor.PID_current_q.I= 1000; + // motor.PID_current_d.P= 5; + // motor.PID_current_d.I = 1000; + // motor.LPF_current_q.Tf = 0.002f; // 1ms default + // motor.LPF_current_d.Tf = 0.002f; // 1ms default + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target current"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target current using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or torque (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_current); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino new file mode 100644 index 0000000..66ff456 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino @@ -0,0 +1,92 @@ +/** + * + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // aligning voltage + motor.voltage_sensor_align = 5; + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino new file mode 100644 index 0000000..7273d15 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino @@ -0,0 +1,95 @@ +/** + * + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead of the current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB, doC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino new file mode 100644 index 0000000..d869f4d --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino @@ -0,0 +1,84 @@ +/** + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - I2C +// MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 5; + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino new file mode 100644 index 0000000..f0f1e3e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino @@ -0,0 +1,137 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and encoder + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * + * NOTE : + * > Arduino UNO example code for running velocity motion control using an encoder with index significantly + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the encoder.enableInterrupts(doA,doB,doIndex) and avoid using PciManger + * + * > If you don't want to use index pin initialize the encoder class without index pin number: + * > For example: + * > - Encoder encoder = Encoder(2, 3, 8192); + * > and initialize interrupts like this: + * > - encoder.enableInterrupts(doA,doB) + * + * Check the docs.simplefoc.com for more info about the possible encoder configuration. + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192, A0); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doIndex(){encoder.handleIndex();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(encoder.index_pin, doIndex); + + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // software interrupts + PciManager.registerListener(&listenerIndex); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino new file mode 100644 index 0000000..1d39173 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino @@ -0,0 +1,125 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * + * NOTE : + * > Specifically for Arduino UNO example code for running velocity motion control using a hall sensor + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doC directly to the sensor.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(sensor.pinC, doC); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void setup() { + + // initialize sensor sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenerIndex); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino new file mode 100644 index 0000000..98e0942 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino @@ -0,0 +1,106 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and magnetic sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * By using the serial terminal set the velocity value you want to motor to obtain + * + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering + // default 5ms - try different values to see what is the best. + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..186d257 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,106 @@ +/** + * Comprehensive BLDC motor control example using encoder + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..df7b76a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino @@ -0,0 +1,114 @@ +/** + * Comprehensive BLDC motor control example using Hall sensor + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A, B and C callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenC(sensor.pinC, doC); + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set control loop type to be used + motor.controller = MotionControlType::torque; + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..098f688 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino @@ -0,0 +1,104 @@ +/** + * Comprehensive BLDC motor control example using magnetic sensor + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the control type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/README.md b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/README.md new file mode 100644 index 0000000..bd6f776 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/README.md @@ -0,0 +1,28 @@ + +# OSC control examples + +OSC - opensoundcontrol.org is a simple message exchange protocol. Widely used in the Audio world it is being hailed as the successor to MIDI. But MIDI itself, and now OSC, has always been about sending and receiving fairly simple control messages, at medium speed, and that makes it well suited for the same kind of task in robotics or other control applications. + +As a protocol it is simple, making implementation quite easy, and while binary, simple enough to be fairly "human readable", which aids in debugging and reduces errors. + +The main advantage of using it is that there is a bunch of ready made software, and also libraries to use in your own programs, neaning you don't have to re-invent these things from scratch. + +## TouchOSC + +The first example shows how to set up control of a motor from TouchOSC. It's a super-fast way to set up a customizable GUI for your motor-project, that runs on your phone... + +## purr-Data + +The second example, "simplefoc\_osc\_esp32\_fullcontrol.ino" allows setting the variables and tuning the motor parameters, in the same way as the serial control but via OSC. The file "osc\_fullcontrol.pd" contains a sample GUI to go with that sketch, allowing the control and tuning of 2 BLDC motors. + +Here a screenshot of what it looks like in purr-Data: + +![Screenshot from pD](osc_fullcontrol_screenshot.png?raw=true "pD controlling 2 BLDC motors") + + +## Links to software used in examples + +- OSC - opensoundcontrol.org +- pD - https://puredata.info +- TouchOSC - https://hexler.net/products/touchosc + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc new file mode 100644 index 0000000000000000000000000000000000000000..ba4d2afca30fa1bd295d514e07167c165edd5323 GIT binary patch literal 511 zcmWIWW@Zs#;Nak3sATmCWIzI(Kz3$cN@|5(MQ+Z~$-eoA40zi9dp)&Y-1<>f@Pgjt zvKx$?+a5pJwk(9((ChH?_48*>JAW{B`rJq68z&#nfAX+m`;~)jITJSr3fQ&^Oz^Zg z>$@z$tTy*cqK#D6ozJ&opL|t_DDvV`nV;)-Y~g2h?m%|2m5C-L66*~XA9xTH^!U8v z54%9FCM8GTVokmIpAVg!oA_eux&1D_>CHDys%}@#)i|Lu@#CTiSJLG-Hd(%E+S4Iw zv0E>B&&uPct8A`5PPLu)+WoN058c;N)!TF@mUCGNyb;~9dsAL2@6qPgrabwk7Sm|f zoQo+XdFu;vUT$;U9v}F#%&UyCacR=gWfNq3xUXfahd*)KQ#NHDhw#Q3IXerNxLZ4lzWC#CHU8*~n)GScpMBZo zGw&SlUDNfE{WDyy_Vm>;1bDM^1mvEnsb*wgxWvo=Cur~n>=0p6@^ RASp&5GzZd~fW|Q}002w5+d=>U literal 0 HcmV?d00001 diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino new file mode 100644 index 0000000..afb9529 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino @@ -0,0 +1,183 @@ +/** + * Arduino SimpleFOC + OSC control example + * + * Simple example to show how you can control SimpleFOC via OSC. + * + * It's a nice way to work, easier than changing speeds via Seerial text input. It could also be the basis for + * a functional UI, for example in a lab, art-gallery or similar situation. + * + * For this example we used an ESP32 to run the code, a AS5048B I2C absolute encoder + * and a generic L298 driver board to drive a Emax 4114 gimbal motor. But there is no reason the OSC part will + * not work with any other setup. + * + * You will need: + * + * - a working SimpleFOC setup - motor, driver, encoder + * - a MCU with WiFi and UDP support, for example an ESP32, or an Arduino with Ethernet Shield + * - a device to run an OSC UI + * - a configured OSC UI + * - a WiFi network + * + * To do the OSC UI I used TouchOSC from https://hexler.net/products/touchosc + * There is an example UI file that works with this sketch, see "layout1.touchosc" + * You can open the UI file in 'TouchOSC Editor' (free program) and transfer it to the TouchOSC app on your device. + * + * Alternatively, there are other OSC UIs which may work, e.g. http://opensoundcontrol.org/implementations + * + * Using: + * + * Change the values below to match the WiFi ssid/password of your network. + * Load and run the code on your ESP32. Take a note of the IP address of your ESP32. + * Load and run the UI in TouchOSC. + * Configure TouchOSC to connect to your ESP32. + * The first command you send will cause the ESP32 to start sending responses to your TouchOSC device. + * After this you will see motor position and speed in the UI. + * Have fun controlling your SimpleFOC motors from your smartphone! + * + */ + + +#include "Arduino.h" +#include + +#include +#include + +#include +#include +#include +#include + + +const char ssid[] = "myssid"; // your network SSID (name) +const char pass[] = "mypassword"; // your network password +WiFiUDP Udp; +IPAddress outIp(192,168,1,17); // remote IP (not needed for receive) +const unsigned int outPort = 8000; // remote port (not needed for receive) +const unsigned int inPort = 8000; // local port to listen for UDP packets (here's where we send the packets) + + +OSCErrorCode error; + +MagneticSensorI2C sensor = MagneticSensorI2C(0x40, 14, 0xFE, 8); +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27); + +// commander interface +Commander command = Commander(Serial); + +void setup() { + Serial.begin(115200); + + WiFi.begin(ssid, pass); + + Serial.print("Connecting WiFi "); + Serial.println(ssid); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Udp.begin(inPort); + Serial.println(); + Serial.print("WiFi connected. Local OSC address: "); + Serial.print(WiFi.localIP()); + Serial.print(":"); + Serial.println(inPort); + + delay(2000); + Serial.println("Initializing motor."); + + sensor.init(); + motor.linkSensor(&sensor); + driver.voltage_power_supply = 9; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity; + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0.001f; + motor.PID_velocity.output_ramp = 1000; + motor.LPF_velocity.Tf = 0.01f; + motor.voltage_limit = 8; + //motor.P_angle.P = 20; + motor.init(); + motor.initFOC(); + + Serial.println("All initialization complete."); +} + +// velocity set point variable +float target_velocity = 2.0f; +// angle set point variable +float target_angle = 1.0f; + + +void motorControl(OSCMessage &msg){ + if (msg.isInt(0)) + target_velocity = radians(msg.getInt(0)); + else if (msg.isFloat(0)) + target_velocity = radians(msg.getFloat(0)); + else if (msg.isDouble(0)) + target_velocity = radians(msg.getDouble(0)); + Serial.print("Velocity set to "); + Serial.println(target_velocity); +} + +void cmdControl(OSCMessage &msg){ + char cmdStr[16]; + if (msg.isString(0)) { + msg.getString(0,cmdStr,16); + command.motor(&motor,cmdStr); + } +} + +long lastSend = 0; +OSCMessage bundleIN; + +void loop() { + OSCBundle bundleOUT; + + // FOC algorithm function + motor.move(target_velocity); + motor.loopFOC(); + + + int size = Udp.parsePacket(); + if (size > 0) { + while (size--) { + bundleIN.fill(Udp.read()); + } + if (!bundleIN.hasError()) { + bundleIN.dispatch("/mot1/S", motorControl); + bundleIN.dispatch("/mot1/C", cmdControl); + IPAddress ip = Udp.remoteIP(); + if (!( ip==outIp )) { + Serial.print("New connection from "); + Serial.println(ip); + outIp = ip; + } + } + else { + error = bundleIN.getError(); + Serial.print("error: "); + Serial.println(error); + } + bundleIN.empty(); + } + else { // don't receive and send in the same loop... + long now = millis(); + if (now - lastSend > 100) { + int ang = (int)degrees(motor.shaftAngle()) % 360; + if (ang<0) ang = 360-abs(ang); + //BOSCBundle's add' returns the OSCMessage so the message's 'add' can be composed together + bundleOUT.add("/mot1/A").add((int)ang); + bundleOUT.add("/mot1/V").add((int)degrees(motor.shaftVelocity())); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one + lastSend = now; + } + } + +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino new file mode 100644 index 0000000..7c6aa4f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino @@ -0,0 +1,351 @@ +/** + * + * Control 2 motors on ESP32 using OSC + * + * In this example, all the commands available on the serial command interface are also available via OSC. + * Also, the example is for 2 motors. If you have only 1 motor, comment out the lines for the second motor. + * + * + * + * + */ + + +#include "Arduino.h" +#include +#include +#include "ssid.h" // create this file, which defines the constants MYSSID and MYPASS +#include +#include +#include + +#include +#include +#include + + +const char ssid[] = MYSSID; // your network SSID (name) +const char pass[] = MYPASS; // your network password +WiFiUDP Udp; +IPAddress outIp(192,168,1,13); // remote IP (not needed for receive) +const unsigned int outPort = 8000; // remote port (not needed for receive) +const unsigned int inPort = 8000; // local port to listen for UDP packets (here's where we send the packets) +#define POWER_SUPPLY 7.4f + + + +MagneticSensorI2C sensor2 = MagneticSensorI2C(0x40, 14, 0xFE, 8); +MagneticSensorI2C sensor1 = MagneticSensorI2C(0x41, 14, 0xFE, 8); +BLDCMotor motor1 = BLDCMotor(7); +BLDCMotor motor2 = BLDCMotor(7); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(25, 26, 27); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(0, 4, 16); + +String m1Prefix("/M1"); +String m2Prefix("/M2"); + + +// we store seperate set-points for each motor, of course +float set_point1 = 0.0f; +float set_point2 = 0.0f; + + +OSCErrorCode error; +OSCBundle bundleOUT; // outgoing message, gets re-used + + + +void setupOTA(const char* hostname) { + ArduinoOTA + .setPort(8266) + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }) + .onEnd([]() { + Serial.println("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\n", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }) + .setHostname(hostname) + .setMdnsEnabled(true); + ArduinoOTA.begin(); +} + + +void setup() { + // set pins low - motor initialization can take some time, + // and you don't want current flowing through the other motor while it happens... + pinMode(0,OUTPUT); + pinMode(4,OUTPUT); + pinMode(16,OUTPUT); + pinMode(25,OUTPUT); + pinMode(26,OUTPUT); + pinMode(27,OUTPUT); + digitalWrite(0, 0); + digitalWrite(4, 0); + digitalWrite(16, 0); + digitalWrite(25, 0); + digitalWrite(26, 0); + digitalWrite(27, 0); + + Serial.begin(115200); + delay(200); + + WiFi.begin(ssid, pass); + + Serial.print("Connecting WiFi "); + Serial.println(ssid); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Udp.begin(inPort); + Serial.println(); + Serial.print("WiFi connected. Local OSC address: "); + Serial.print(WiFi.localIP()); + Serial.print(":"); + Serial.println(inPort); + + setupOTA("smallrobot1"); + + Serial.println("Initializing motors."); + + Wire.setClock(400000); + + sensor1.init(); + motor1.linkSensor(&sensor1); + driver1.voltage_power_supply = POWER_SUPPLY; + driver1.init(); + motor1.linkDriver(&driver1); + motor1.foc_modulation = FOCModulationType::SpaceVectorPWM; + motor1.controller = MotionControlType::velocity; + motor1.PID_velocity.P = 0.2f; + motor1.PID_velocity.I = 20; + motor1.PID_velocity.D = 0.001f; + motor1.PID_velocity.output_ramp = 1000; + motor1.LPF_velocity.Tf = 0.01f; + motor1.voltage_limit = POWER_SUPPLY; + motor1.P_angle.P = 20; + motor1.init(); + motor1.initFOC(); + + sensor2.init(); + motor2.linkSensor(&sensor2); + driver2.voltage_power_supply = POWER_SUPPLY; + driver2.init(); + motor2.linkDriver(&driver2); + motor2.foc_modulation = FOCModulationType::SpaceVectorPWM; + motor2.controller = MotionControlType::velocity; + motor2.PID_velocity.P = 0.2f; + motor2.PID_velocity.I = 20; + motor2.PID_velocity.D = 0.001f; + motor2.PID_velocity.output_ramp = 1000; + motor2.LPF_velocity.Tf = 0.01f; + motor2.voltage_limit = POWER_SUPPLY; + motor2.P_angle.P = 20; + motor2.init(); + motor2.initFOC(); + + sendMotorParams(motor1, m1Prefix); + sendMotorParams(motor2, m2Prefix); + Serial.println("All initialization complete."); + _delay(1000); +} + + + + +template +void sendMessage(String& addr, T datum) { + bundleOUT.add(addr.c_str()).add(datum); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + + +float getNumber(OSCMessage &msg, int index) { + if (msg.getType(index)=='i') + return msg.getInt(index); + if (msg.getType(index)=='f') + return msg.getFloat(index); + if (msg.getType(index)=='d') + return msg.getDouble(index); + return 0; +} + + + +void motorCmd(OSCMessage &msg, int offset, BLDCMotor& motor, float* set_point, String& prefix){ + Serial.print("Command for "); + Serial.println(prefix); + if (msg.fullMatch("/P", offset)) { + motor.PID_velocity.P = getNumber(msg,0); + sendMessage(prefix+"/P", motor.PID_velocity.P); + } + else if (msg.fullMatch("/I", offset)) { + motor.PID_velocity.I = getNumber(msg,0); + sendMessage(prefix+"/I", motor.PID_velocity.I); + } + else if (msg.fullMatch("/D", offset)) { + motor.PID_velocity.D = getNumber(msg,0); + sendMessage(prefix+"/D", motor.PID_velocity.D); + } + else if (msg.fullMatch("/R", offset)) { + motor.PID_velocity.output_ramp = getNumber(msg,0); + sendMessage(prefix+"/R", motor.PID_velocity.output_ramp); + } + else if (msg.fullMatch("/F", offset)) { + motor.LPF_velocity.Tf = getNumber(msg,0); + sendMessage(prefix+"/F", motor.LPF_velocity.Tf); + } + else if (msg.fullMatch("/K", offset)) { + motor.P_angle.P = getNumber(msg,0); + sendMessage(prefix+"/K", motor.P_angle.P); + } + else if (msg.fullMatch("/N", offset)) { + motor.velocity_limit = getNumber(msg,0); + sendMessage(prefix+"/N", motor.velocity_limit); + } + else if (msg.fullMatch("/L", offset)) { + motor.voltage_limit = getNumber(msg,0); + sendMessage(prefix+"/L", motor.voltage_limit); + } + else if (msg.fullMatch("/C", offset)) { + motor.controller = (MotionControlType)msg.getInt(0); + sendMessage(prefix+"/C", motor.controller); + } + else if (msg.fullMatch("/t", offset)) { + *set_point = getNumber(msg,0); + sendMessage(prefix+"/3", *set_point); // TODO is the set-point the same as motor.target? + Serial.print("Setting set-point to "); + Serial.println(*set_point); + } + else if (msg.fullMatch("/o", offset)) { + motor.disable(); + } + else if (msg.fullMatch("/e", offset)) { + motor.enable(); + } + else if (msg.fullMatch("/params", offset)) { + sendMotorParams(motor, prefix); + } + else if (msg.fullMatch("/reinit", offset)) { + motor.disable(); + motor.init(); + motor.initFOC(); + } + +} + + + + + + +void sendMotorMonitoring() { + //Serial.println("Sending monitoring..."); + bundleOUT.add("/M1/0").add(motor1.voltage.q); + bundleOUT.add("/M1/1").add(motor1.shaft_velocity); + bundleOUT.add("/M1/2").add(motor1.shaft_angle); + bundleOUT.add("/M1/3").add(motor1.target); + bundleOUT.add("/M2/0").add(motor2.voltage.q); + bundleOUT.add("/M2/1").add(motor2.shaft_velocity); + bundleOUT.add("/M2/2").add(motor2.shaft_angle); + bundleOUT.add("/M2/3").add(motor2.target); + // TODO pack it into one message bundleOUT.add("/M2/i").add(motor2.voltage_q).add(motor2.shaft_velocity).add(motor2.shaft_angle).add(motor2.target); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + +void sendMotorParams(BLDCMotor& motor, String& prefix) { + bundleOUT.add((prefix+"/P").c_str()).add(motor.PID_velocity.P); + bundleOUT.add((prefix+"/I").c_str()).add(motor.PID_velocity.I); + bundleOUT.add((prefix+"/D").c_str()).add(motor.PID_velocity.D); + bundleOUT.add((prefix+"/R").c_str()).add(motor.PID_velocity.output_ramp); + bundleOUT.add((prefix+"/F").c_str()).add(motor.LPF_velocity.Tf); + bundleOUT.add((prefix+"/K").c_str()).add(motor.P_angle.P); + bundleOUT.add((prefix+"/N").c_str()).add(motor.velocity_limit); + bundleOUT.add((prefix+"/L").c_str()).add(motor.voltage_limit); + bundleOUT.add((prefix+"/C").c_str()).add(motor.controller); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + + + +long lastSend = 0; +OSCMessage bundleIN; +int size; + + +void loop() { + + // FOC algorithm function + motor1.loopFOC(); + motor1.move(set_point1); + motor2.loopFOC(); + motor2.move(set_point2); + + + int size = Udp.parsePacket(); + if (size > 0) { + while (size--) { + bundleIN.fill(Udp.read()); + } + if (!bundleIN.hasError()) { + bundleIN.route("/M1", [](OSCMessage& msg, int offset){ motorCmd(msg, offset, motor1, &set_point1, m1Prefix); }, 0); + bundleIN.route("/M2", [](OSCMessage& msg, int offset){ motorCmd(msg, offset, motor2, &set_point2, m2Prefix); }, 0); + IPAddress ip = Udp.remoteIP(); + if (!( ip==outIp )) { + Serial.print("New connection from "); + Serial.println(ip); + outIp = ip; + sendMotorParams(motor1, m1Prefix); + sendMotorParams(motor2, m2Prefix); + } + } + else { + error = bundleIN.getError(); + Serial.print("error: "); + Serial.println(error); + } + bundleIN.empty(); + } + else { // don't receive and send in the same loop... + long now = millis(); + if (now - lastSend > 100) { + sendMotorMonitoring(); + lastSend = now; + } + } + + ArduinoOTA.handle(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd new file mode 100644 index 0000000..ddbf2f9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd @@ -0,0 +1,384 @@ +#N struct text float x float y text t; +#N canvas 1842 146 1519 1052 12; +#X obj 501 697 cnv 15 193 209 empty empty Tuning\ M1 20 12 0 14 #e0e0e0 +#404040 0; +#X obj 787 477 mrpeach/unpackOSC; +#X obj 132 586 print oscin; +#X obj 787 504 print oscout; +#X obj 723 449 spigot; +#X obj 774 452 tgl 15 1 empty empty Debug 17 7 0 10 #fcfcfc #000000 +#000000 1 1; +#X msg 591 503 disconnect; +#X obj 132 558 spigot; +#X obj 114 562 tgl 15 1 empty empty Debug -34 6 0 10 #fcfcfc #000000 +#000000 0 1; +#X obj 132 531 mrpeach/unpackOSC; +#X obj 673 477 mrpeach/udpsend; +#X obj 132 496 mrpeach/udpreceive 8000; +#X obj 673 422 mrpeach/packOSC; +#X obj 1043 150 hsl 249 25 -5000 5000 0 0 empty empty Set\ Point\ M1 +-2 -8 0 10 #fcfcfc #000000 #000000 0 1; +#X obj 1044 197 hsl 247 25 -15 15 0 0 empty empty Set\ Point\ M2 -2 +-8 0 10 #fcfcfc #000000 #000000 12300 1; +#X obj 120 153 bng 53 250 50 0 empty empty STOP 14 26 0 10 #fc1204 +#000000 #ffffff; +#X obj 200 102 * 0.10472; +#X obj 202 169 hsl 235 30 -520 520 0 0 empty empty Set\ point\ (Velocity) +-2 -8 0 10 #fcfcfc #000000 #000000 11500 1; +#X obj 673 449 spigot; +#X obj 653 452 tgl 15 1 empty empty Enable\ send -71 6 0 10 #fcfcfc +#000000 #000000 1 1; +#X msg 484 478 connect 192.168.1.43 8000; +#X obj 673 395 speedlim 100; +#X obj 231 573 mrpeach/routeOSC /M1 /M2; +#X obj 231 610 mrpeach/routeOSC /0 /1 /2 /3 /P /I /D /R /F /K /N /L +/C; +#X obj 326 844 hsl 101 29 0 6.3 0 0 empty empty rad -2 -8 0 10 #fcfcfc +#000000 #000000 0 1; +#X obj 326 812 % 6.28319; +#X obj 326 704 nbx 7 27 -1e+37 1e+37 0 0 empty empty V -5 0 0 18 #fcfcfc +#000000 #000000 0.137548 256 3; +#X obj 113 804 nbx 7 27 -1e+37 1e+37 0 0 empty empty Set\ point 0 -15 +0 18 #fcfcfc #000000 #000000 0 256 3; +#X obj 326 776 nbx 7 27 -1e+37 1e+37 0 0 empty empty Position -80 0 +0 18 #fcfcfc #000000 #000000 -348.637 256 3; +#X obj 326 740 nbx 7 27 -1e+37 1e+37 0 0 empty empty Velocity -80 0 +0 18 #fcfcfc #000000 #000000 0.0649328 256 3; +#X obj 538 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty P\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 0.2 256 3; +#X obj 537 776 nbx 5 14 -1e+37 1e+37 0 0 empty empty I\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 20 256 3; +#X obj 538 808 nbx 5 14 -1e+37 1e+37 0 0 empty empty D\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 0.0001 256 3; +#X obj 539 838 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ Ramp 0 -8 0 +10 #fcfcfc #000000 #000000 1000 256 3; +#X obj 539 868 nbx 5 14 -1e+37 1e+37 0 0 empty empty LP\ time 0 -8 +0 10 #fcfcfc #000000 #000000 0.01 256 3; +#X obj 605 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 608 779 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ lim 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 609 836 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ limit 0 -8 +0 10 #fcfcfc #000000 #000000 8 256 3; +#X obj 122 278 hradio 53 0 1 3 motorselect empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 0; +#X scalar text 172 305 \; \;; +#X obj 122 372 select 0 1 2; +#X msg 122 399 prefix /M?; +#X msg 149 423 prefix /M1; +#X msg 178 445 prefix /M2; +#X obj 789 422 mrpeach/packOSC; +#X obj 789 395 speedlim 100; +#X msg 592 531 typetags \$1; +#X obj 571 535 tgl 15 1 empty empty OSC\ type\ tags -80 7 0 10 #ffffff +#000000 #000000 1 1; +#X text 63 286 Choose Motor, f 7; +#X text 137 339 All, f 4; +#X text 191 339 M1, f 4; +#X text 247 339 M2, f 4; +#X text 152 77 RPM; +#X obj 1008 148 bng 29 250 50 0 empty empty STOP 2 13 0 10 #fc1204 +#000000 #ffffff; +#X obj 1009 195 bng 29 250 50 0 empty empty STOP 2 13 0 10 #fc1204 +#000000 #ffffff; +#X obj 74 696 hradio 53 0 1 3 empty empty empty 0 -8 0 10 #fcfcfc #000000 +#000000 1; +#X text 8 711 Control; +#X text 67 752 Voltage; +#X text 124 753 Velocity; +#X text 189 754 Position; +#X obj 312 101 /; +#X obj 312 129 * 6.28319; +#X text 424 75 cm, f 4; +#X text 393 51 Wheel diameter; +#X obj 394 100 * 0.0314159; +#X msg 348 636 set \$1; +#X msg 376 637 set \$1; +#X msg 407 636 set \$1; +#X msg 435 636 set \$1; +#X msg 466 636 set \$1; +#X msg 495 636 set \$1; +#X msg 524 637 set \$1; +#X msg 554 637 set \$1; +#X msg 583 637 set \$1; +#X obj 75 898 s osctargetedout; +#X obj 75 866 prepend /M1/C; +#X obj 773 304 r osctargetedout; +#X obj 593 912 prepend /M1/K; +#X obj 602 925 prepend /M1/N; +#X obj 609 936 prepend /M1/L; +#X obj 564 976 s osctargetedout; +#X obj 1271 697 cnv 15 193 209 empty empty Tuning\ M2 20 12 0 14 #e0e0e0 +#404040 0; +#X obj 1001 610 mrpeach/routeOSC /0 /1 /2 /3 /P /I /D /R /F /K /N /L +/C; +#X obj 1096 844 hsl 101 29 0 6.3 0 0 empty empty rad -2 -8 0 10 #fcfcfc +#000000 #000000 0 1; +#X obj 1096 812 % 6.28319; +#X obj 1096 704 nbx 7 27 -1e+37 1e+37 0 0 empty empty V -5 0 0 18 #fcfcfc +#000000 #000000 -0.13018 256 3; +#X obj 883 804 nbx 7 27 -1e+37 1e+37 0 0 setpointin2 empty Set\ point +0 -15 0 18 #fcfcfc #000000 #000000 0 256 3; +#X obj 1096 776 nbx 7 27 -1e+37 1e+37 0 0 empty empty Position -80 +0 0 18 #fcfcfc #000000 #000000 -346.273 256 3; +#X obj 1096 740 nbx 7 27 -1e+37 1e+37 0 0 empty empty Velocity -80 +0 0 18 #fcfcfc #000000 #000000 0.0657255 256 3; +#X obj 1308 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty P\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 0.2 256 3; +#X obj 1308 778 nbx 5 14 -1e+37 1e+37 0 0 empty empty I\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1308 808 nbx 5 14 -1e+37 1e+37 0 0 empty empty D\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 0.001 256 3; +#X obj 1309 838 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ Ramp 0 -8 +0 10 #fcfcfc #000000 #000000 1000 256 3; +#X obj 1309 868 nbx 5 14 -1e+37 1e+37 0 0 empty empty LP\ time 0 -8 +0 10 #fcfcfc #000000 #000000 0.01 256 3; +#X obj 1375 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ gain 0 +-8 0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1378 779 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ lim 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1379 836 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ limit 0 -8 +0 10 #fcfcfc #000000 #000000 8 256 3; +#X obj 844 696 hradio 53 0 1 3 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 1; +#X text 778 711 Control; +#X text 837 752 Voltage; +#X text 894 753 Velocity; +#X text 959 754 Position; +#X msg 1118 636 set \$1; +#X msg 1146 637 set \$1; +#X msg 1177 636 set \$1; +#X msg 1205 636 set \$1; +#X msg 1236 636 set \$1; +#X msg 1265 636 set \$1; +#X msg 1294 637 set \$1; +#X msg 1324 637 set \$1; +#X msg 1353 637 set \$1; +#X obj 845 898 s osctargetedout; +#X obj 1325 976 s osctargetedout; +#X obj 1379 936 prepend /M2/L; +#X obj 1372 925 prepend /M2/N; +#X obj 1364 912 prepend /M2/K; +#X obj 1296 947 prepend /M2/F; +#X obj 1291 940 prepend /M2/R; +#X obj 1287 933 prepend /M2/D; +#X obj 1281 925 prepend /M2/I; +#X obj 1276 917 prepend /M2/P; +#X obj 526 947 prepend /M1/F; +#X obj 521 940 prepend /M1/R; +#X obj 517 933 prepend /M1/D; +#X obj 511 925 prepend /M1/I; +#X obj 506 917 prepend /M1/P; +#X obj 393 78 nbx 2 14 0 50 0 1 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 6 256 2; +#X obj 299 71 nbx 5 24 -20 20 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 -0.266666 256 2; +#X obj 179 74 nbx 7 23 -5000 5000 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 -84.8824 256 2; +#X obj 577 169 hsl 104 30 -3.1415 3.1415 0 0 empty empty Set\ point\ (Position) +-2 -8 0 10 #fcfcfc #000000 #000000 10300 1; +#X obj 708 68 vsl 31 122 0 12 0 0 empty empty Set\ point\ (Voltage) +0 -9 0 10 #fcfcfc #000000 #000000 0 1; +#X obj 246 13 / 0.10472; +#X msg 318 13 set \$1; +#X obj 457 11 *; +#X obj 384 11 / 6.28319; +#X obj 547 96 /; +#X obj 547 125 * 6.28319; +#X obj 547 66 nbx 5 24 -100 100 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 0.0599999 256 2; +#X text 534 68 m; +#X obj 20 6 r setpointin; +#X obj 17 36 r setpointin2; +#X obj 120 6 spigot; +#X obj 120 42 spigot; +#X obj 16 63 r motorselect; +#X obj 1339 99 r setpointin; +#X obj 1345 150 r setpointin2; +#X msg 493 11 set \$1; +#X obj 22 103 > 1; +#X obj 59 103 <= 1; +#X msg 1110 529 /M?/params; +#X obj 1110 502 loadbang; +#X msg 1339 126 set \$1; +#X msg 1343 174 set \$1; +#X obj 639 9 *; +#X obj 566 9 / 6.28319; +#X msg 675 9 set \$1; +#X obj 483 448 loadbang; +#X text 284 74 m/s; +#X obj 845 866 prepend /M2/C; +#X msg 163 226 sendtyped /M?/t f 0; +#X obj 458 224 prepend sendtyped /t f; +#X msg 991 301 sendtyped /M2/t f 0; +#X msg 983 274 sendtyped /M1/t f 0; +#X obj 1051 286 prepend sendtyped /M1/t f; +#X obj 1047 322 prepend sendtyped /M2/t f; +#X obj 475 171 nbx 7 21 -1e+37 1e+37 0 0 empty empty rad 0 -8 0 10 +#fcfcfc #000000 #000000 2 256 2; +#X connect 1 0 3 0; +#X connect 4 0 1 0; +#X connect 5 0 4 1; +#X connect 6 0 10 0; +#X connect 7 0 2 0; +#X connect 8 0 7 1; +#X connect 9 0 7 0; +#X connect 9 0 22 0; +#X connect 11 0 9 0; +#X connect 12 0 18 0; +#X connect 12 0 4 0; +#X connect 13 0 163 0; +#X connect 14 0 164 0; +#X connect 15 0 159 0; +#X connect 16 0 17 0; +#X connect 17 0 131 0; +#X connect 17 0 134 0; +#X connect 17 0 160 0; +#X connect 18 0 10 0; +#X connect 19 0 18 1; +#X connect 20 0 10 0; +#X connect 21 0 12 0; +#X connect 22 0 23 0; +#X connect 22 1 82 0; +#X connect 23 0 26 0; +#X connect 23 1 29 0; +#X connect 23 2 28 0; +#X connect 23 3 27 0; +#X connect 23 4 65 0; +#X connect 23 5 66 0; +#X connect 23 6 67 0; +#X connect 23 7 68 0; +#X connect 23 8 69 0; +#X connect 23 9 70 0; +#X connect 23 10 71 0; +#X connect 23 11 72 0; +#X connect 23 12 73 0; +#X connect 25 0 24 0; +#X connect 28 0 25 0; +#X connect 30 0 125 0; +#X connect 31 0 124 0; +#X connect 32 0 123 0; +#X connect 33 0 122 0; +#X connect 34 0 121 0; +#X connect 35 0 77 0; +#X connect 36 0 78 0; +#X connect 37 0 79 0; +#X connect 38 0 40 0; +#X connect 40 0 41 0; +#X connect 40 1 42 0; +#X connect 40 2 43 0; +#X connect 41 0 12 0; +#X connect 42 0 12 0; +#X connect 43 0 12 0; +#X connect 44 0 18 0; +#X connect 44 0 4 0; +#X connect 45 0 44 0; +#X connect 46 0 12 0; +#X connect 47 0 46 0; +#X connect 53 0 162 0; +#X connect 54 0 161 0; +#X connect 55 0 75 0; +#X connect 60 0 61 0; +#X connect 61 0 17 0; +#X connect 64 0 60 1; +#X connect 64 0 133 1; +#X connect 64 0 135 1; +#X connect 64 0 153 1; +#X connect 65 0 30 0; +#X connect 66 0 31 0; +#X connect 67 0 32 0; +#X connect 68 0 33 0; +#X connect 69 0 34 0; +#X connect 70 0 35 0; +#X connect 71 0 36 0; +#X connect 72 0 37 0; +#X connect 73 0 55 0; +#X connect 75 0 74 0; +#X connect 76 0 45 0; +#X connect 77 0 80 0; +#X connect 78 0 80 0; +#X connect 79 0 80 0; +#X connect 82 0 85 0; +#X connect 82 1 88 0; +#X connect 82 2 87 0; +#X connect 82 3 86 0; +#X connect 82 4 102 0; +#X connect 82 5 103 0; +#X connect 82 6 104 0; +#X connect 82 7 105 0; +#X connect 82 8 106 0; +#X connect 82 9 107 0; +#X connect 82 10 108 0; +#X connect 82 11 109 0; +#X connect 82 12 110 0; +#X connect 84 0 83 0; +#X connect 87 0 84 0; +#X connect 89 0 120 0; +#X connect 90 0 119 0; +#X connect 91 0 118 0; +#X connect 92 0 117 0; +#X connect 93 0 116 0; +#X connect 94 0 115 0; +#X connect 95 0 114 0; +#X connect 96 0 113 0; +#X connect 97 0 158 0; +#X connect 102 0 89 0; +#X connect 103 0 90 0; +#X connect 104 0 91 0; +#X connect 105 0 92 0; +#X connect 106 0 93 0; +#X connect 107 0 94 0; +#X connect 108 0 95 0; +#X connect 109 0 96 0; +#X connect 110 0 97 0; +#X connect 113 0 112 0; +#X connect 114 0 112 0; +#X connect 115 0 112 0; +#X connect 116 0 112 0; +#X connect 117 0 112 0; +#X connect 118 0 112 0; +#X connect 119 0 112 0; +#X connect 120 0 112 0; +#X connect 121 0 80 0; +#X connect 122 0 80 0; +#X connect 123 0 80 0; +#X connect 124 0 80 0; +#X connect 125 0 80 0; +#X connect 126 0 64 0; +#X connect 127 0 60 0; +#X connect 128 0 16 0; +#X connect 129 0 165 0; +#X connect 130 0 160 0; +#X connect 131 0 132 0; +#X connect 132 0 128 0; +#X connect 133 0 146 0; +#X connect 134 0 133 0; +#X connect 135 0 136 0; +#X connect 136 0 165 0; +#X connect 137 0 135 0; +#X connect 139 0 141 0; +#X connect 140 0 142 0; +#X connect 143 0 147 0; +#X connect 143 0 148 0; +#X connect 144 0 151 0; +#X connect 145 0 152 0; +#X connect 146 0 127 0; +#X connect 147 0 142 1; +#X connect 148 0 141 1; +#X connect 149 0 44 0; +#X connect 150 0 149 0; +#X connect 151 0 13 0; +#X connect 152 0 14 0; +#X connect 153 0 155 0; +#X connect 154 0 153 0; +#X connect 155 0 137 0; +#X connect 156 0 20 0; +#X connect 158 0 111 0; +#X connect 159 0 44 0; +#X connect 160 0 21 0; +#X connect 161 0 44 0; +#X connect 162 0 44 0; +#X connect 163 0 45 0; +#X connect 164 0 45 0; +#X connect 165 0 160 0; +#X connect 165 0 154 0; diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me new file mode 100644 index 0000000..9d3b03c --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me @@ -0,0 +1,4 @@ + +#define MYSSID "yourssid" +#define MYPASS "yourpassword" + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino new file mode 100644 index 0000000..7324edf --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino @@ -0,0 +1,125 @@ +#include +#include + +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); +MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); + + +/** + * This measures how closely sensor and electrical angle agree and how much your motor is affected by 'cogging'. + * It can be used to investigate how much non linearity there is between what we set (electrical angle) and what we read (sensor angle) + * This non linearity could be down to magnet placement, coil winding differences or simply that the magnetic field when travelling through a pole pair is not linear + * An alignment error of ~10 degrees and cogging of ~4 degrees is normal for small gimbal. + * The following article is an interesting read + * https://hackaday.com/2016/02/23/anti-cogging-algorithm-brings-out-the-best-in-your-hobby-brushless-motors/ + */ +void testAlignmentAndCogging(int direction) { + + motor.move(0); + _delay(200); + + sensor.update(); + float initialAngle = sensor.getAngle(); + + const int shaft_rotation = 720; // 720 deg test - useful to see repeating cog pattern + int sample_count = int(shaft_rotation * motor.pole_pairs); // test every electrical degree + + float stDevSum = 0; + + float mean = 0.0f; + float prev_mean = 0.0f; + + + for (int i = 0; i < sample_count; i++) { + + float shaftAngle = (float) direction * i * shaft_rotation / sample_count; + float electricAngle = (float) shaftAngle * motor.pole_pairs; + // move and wait + motor.move(shaftAngle * PI / 180); + _delay(5); + + // measure + sensor.update(); + float sensorAngle = (sensor.getAngle() - initialAngle) * 180 / PI; + float sensorElectricAngle = sensorAngle * motor.pole_pairs; + float electricAngleError = electricAngle - sensorElectricAngle; + + // plot this - especially electricAngleError + Serial.print(electricAngle); + Serial.print("\t"); + Serial.print(sensorElectricAngle ); + Serial.print("\t"); + Serial.println(electricAngleError); + + // use knuth standard deviation algorithm so that we don't need an array too big for an Uno + prev_mean = mean; + mean = mean + (electricAngleError-mean)/(i+1); + stDevSum = stDevSum + (electricAngleError-mean)*(electricAngleError-prev_mean); + + } + + Serial.println(); + Serial.println(F("ALIGNMENT AND COGGING REPORT")); + Serial.println(); + Serial.print(F("Direction: ")); + Serial.println(direction); + Serial.print(F("Mean error (alignment): ")); + Serial.print(mean); + Serial.println(" deg (electrical)"); + Serial.print(F("Standard Deviation (cogging): ")); + Serial.print(sqrt(stDevSum/sample_count)); + Serial.println(F(" deg (electrical)")); + Serial.println(); + Serial.println(F("Plotting 3rd column of data (electricAngleError) will likely show sinusoidal cogging pattern with a frequency of 4xpole_pairs per rotation")); + Serial.println(); + +} + +void setup() { + + Serial.begin(115200); + while (!Serial) ; + + // driver config + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + motor.voltage_sensor_align = 3; + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=motor.voltage_sensor_align; + + sensor.init(); + motor.linkSensor(&sensor); + + motor.useMonitoring(Serial); + motor.init(); + motor.initFOC(); + + testAlignmentAndCogging(1); + + motor.move(0); + Serial.println(F("Press any key to test in CCW direction")); + while (!Serial.available()) { } + + testAlignmentAndCogging(-1); + + Serial.println(F("Complete")); + + motor.voltage_limit = 0; + motor.move(0); + while (true) ; //do nothing; + +} + + + + +void loop() { + +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..76fb46b --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,102 @@ +/** + * + * Find KV rating for motor with encoder + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder sensor = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} + + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..3abef46 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,99 @@ +/** + * + * Find KV rating for motor with Hall sensors + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB, doC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..f3dd74a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,96 @@ +/** + * Find KV rating for motor with magnetic sensors + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - I2C +// MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino new file mode 100644 index 0000000..aac879c --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino @@ -0,0 +1,173 @@ +/** + * Utility arduino sketch which finds pole pair number of the motor + * + * To run it just set the correct pin numbers for the BLDC driver and encoder A and B channel as well as the encoder PPR value. + * + * The program will rotate your motor a specific amount and check how much it moved, and by doing a simple calculation calculate your pole pair number. + * The pole pair number will be outputted to the serial terminal. + * + * If the pole pair number is well estimated your motor will start to spin in voltage mode with 2V target. + * + * If the code calculates negative pole pair number please invert your encoder A and B channel pins or motor connector. + * + * Try running this code several times to avoid statistical errors. + * > But in general if your motor spins, you have a good pole pairs number. + */ +#include + +// BLDC motor instance +// its important to put pole pairs number as 1!!! +BLDCMotor motor = BLDCMotor(1); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +// its important to put pole pairs number as 1!!! +//StepperMotor motor = StepperMotor(1); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// Encoder(int encA, int encB , int cpr, int index) +Encoder encoder = Encoder(2, 3, 2048); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +void setup() { + + // initialise encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage + // default 12V + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + + // initialize motor + motor.init(); + // monitoring port + Serial.begin(115200); + + // pole pairs calculation routine + Serial.println("Pole pairs (PP) estimator"); + Serial.println("-\n"); + + float pp_search_voltage = 4; // maximum power_supply_voltage/2 + float pp_search_angle = 6*_PI; // search electrical angle to turn + + // move motor to the electrical angle 0 + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=pp_search_voltage; + motor.move(0); + _delay(1000); + // read the encoder angle + encoder.update(); + float angle_begin = encoder.getAngle(); + _delay(50); + + // move the motor slowly to the electrical angle pp_search_angle + float motor_angle = 0; + while(motor_angle <= pp_search_angle){ + motor_angle += 0.01f; + motor.move(motor_angle); + _delay(1); + } + _delay(1000); + // read the encoder value for 180 + encoder.update(); + float angle_end = encoder.getAngle(); + _delay(50); + // turn off the motor + motor.move(0); + _delay(1000); + + // calculate the pole pair number + int pp = round((pp_search_angle)/(angle_end-angle_begin)); + + Serial.print(F("Estimated PP : ")); + Serial.println(pp); + Serial.println(F("PP = Electrical angle / Encoder angle ")); + Serial.print(pp_search_angle*180/_PI); + Serial.print("/"); + Serial.print((angle_end-angle_begin)*180/_PI); + Serial.print(" = "); + Serial.println((pp_search_angle)/(angle_end-angle_begin)); + Serial.println(); + + + // a bit of monitoring the result + if(pp <= 0 ){ + Serial.println(F("PP number cannot be negative")); + Serial.println(F(" - Try changing the search_voltage value or motor/encoder configuration.")); + return; + }else if(pp > 30){ + Serial.println(F("PP number very high, possible error.")); + }else{ + Serial.println(F("If PP is estimated well your motor should turn now!")); + Serial.println(F(" - If it is not moving try to relaunch the program!")); + Serial.println(F(" - You can also try to adjust the target voltage using serial terminal!")); + } + + + // set FOC loop to be used + motor.controller = MotionControlType::torque; + // set the pole pair number to the motor + motor.pole_pairs = pp; + //align encoder and start FOC + motor.initFOC(); + _delay(1000); + + Serial.println(F("\n Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); +} + +// uq voltage +float target_voltage = 2; + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // communicate with the user + serialReceiveUserCommand(); +} + + +// utility function enabling serial communication with the user to set the target values +// this function can be implemented in serialEvent function as well +void serialReceiveUserCommand() { + + // a string to hold incoming data + static String received_chars; + + while (Serial.available()) { + // get the new byte: + char inChar = (char)Serial.read(); + // add it to the string buffer: + received_chars += inChar; + // end of user input + if (inChar == '\n') { + + // change the motor target + target_voltage = received_chars.toFloat(); + Serial.print("Target voltage: "); + Serial.println(target_voltage); + + // reset the command buffer + received_chars = ""; + } + } +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino new file mode 100644 index 0000000..e98a452 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino @@ -0,0 +1,173 @@ +/** + * Utility arduino sketch which finds pole pair number of the motor + * + * To run it just set the correct pin numbers for the BLDC driver and sensor CPR value and chip select pin. + * + * The program will rotate your motor a specific amount and check how much it moved, and by doing a simple calculation calculate your pole pair number. + * The pole pair number will be outputted to the serial terminal. + * + * If the pole pair number is well estimated your motor will start to spin in voltage mode with 2V target. + * + * If the code calculates negative pole pair number please invert your motor connector. + * + * Try running this code several times to avoid statistical errors. + * > But in general if your motor spins, you have a good pole pairs number. + */ +#include + +// BLDC motor instance +// its important to put pole pairs number as 1!!! +BLDCMotor motor = BLDCMotor(1); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +// its important to put pole pairs number as 1!!! +//StepperMotor motor = StepperMotor(1); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + // default 12V + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // initialize motor hardware + motor.init(); + + // monitoring port + Serial.begin(115200); + + // pole pairs calculation routine + Serial.println("Pole pairs (PP) estimator"); + Serial.println("-\n"); + + float pp_search_voltage = 4; // maximum power_supply_voltage/2 + float pp_search_angle = 6*_PI; // search electrical angle to turn + + // move motor to the electrical angle 0 + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=pp_search_voltage; + motor.move(0); + _delay(1000); + // read the sensor angle + sensor.update(); + float angle_begin = sensor.getAngle(); + _delay(50); + + // move the motor slowly to the electrical angle pp_search_angle + float motor_angle = 0; + while(motor_angle <= pp_search_angle){ + motor_angle += 0.01f; + sensor.update(); // keep track of the overflow + motor.move(motor_angle); + _delay(1); + } + _delay(1000); + // read the sensor value for 180 + sensor.update(); + float angle_end = sensor.getAngle(); + _delay(50); + // turn off the motor + motor.move(0); + _delay(1000); + + // calculate the pole pair number + int pp = round((pp_search_angle)/(angle_end-angle_begin)); + + Serial.print(F("Estimated PP : ")); + Serial.println(pp); + Serial.println(F("PP = Electrical angle / Encoder angle ")); + Serial.print(pp_search_angle*180/_PI); + Serial.print(F("/")); + Serial.print((angle_end-angle_begin)*180/_PI); + Serial.print(F(" = ")); + Serial.println((pp_search_angle)/(angle_end-angle_begin)); + Serial.println(); + + + // a bit of monitoring the result + if(pp <= 0 ){ + Serial.println(F("PP number cannot be negative")); + Serial.println(F(" - Try changing the search_voltage value or motor/sensor configuration.")); + return; + }else if(pp > 30){ + Serial.println(F("PP number very high, possible error.")); + }else{ + Serial.println(F("If PP is estimated well your motor should turn now!")); + Serial.println(F(" - If it is not moving try to relaunch the program!")); + Serial.println(F(" - You can also try to adjust the target voltage using serial terminal!")); + } + + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + // set the pole pair number to the motor + motor.pole_pairs = pp; + //align sensor and start FOC + motor.initFOC(); + _delay(1000); + + Serial.println(F("\n Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); +} + +// uq voltage +float target_voltage = 2; + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // communicate with the user + serialReceiveUserCommand(); +} + + +// utility function enabling serial communication with the user to set the target values +// this function can be implemented in serialEvent function as well +void serialReceiveUserCommand() { + + // a string to hold incoming data + static String received_chars; + + while (Serial.available()) { + // get the new byte: + char inChar = (char)Serial.read(); + // add it to the string buffer: + received_chars += inChar; + // end of user input + if (inChar == '\n') { + + // change the motor target + target_voltage = received_chars.toFloat(); + Serial.print("Target voltage: "); + Serial.println(target_voltage); + + // reset the command buffer + received_chars = ""; + } + } +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino new file mode 100644 index 0000000..27163df --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino @@ -0,0 +1,84 @@ +/** + * Simple example intended to help users find the zero offset and natural direction of the sensor. + * + * These values can further be used to avoid motor and sensor alignment procedure. + * To use these values add them to the code:"); + * motor.sensor_direction=Direction::CW; // or Direction::CCW + * motor.zero_electric_angle=1.2345; // use the real value! + * + * This will only work for abosolute value sensors - magnetic sensors. + * Bypassing the alignment procedure is not possible for the encoders and for the current implementation of the Hall sensors. + * library version 1.4.2. + * + */ +#include + +// magnetic sensor instance - SPI +//MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); +// magnetic sensor instance - analog output +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +void setup() { + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // aligning voltage + motor.voltage_sensor_align = 7; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // force direction search - because default is CW + motor.sensor_direction = Direction::UNKNOWN; + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + + Serial.begin(115200); + Serial.println("Sensor zero offset is:"); + Serial.println(motor.zero_electric_angle, 4); + Serial.println("Sensor natural direction is: "); + Serial.println(motor.sensor_direction == Direction::CW ? "Direction::CW" : "Direction::CCW"); + + Serial.println("To use these values add them to the code:"); + Serial.print(" motor.sensor_direction="); + Serial.print(motor.sensor_direction == Direction::CW ? "Direction::CW" : "Direction::CCW"); + Serial.println(";"); + Serial.print(" motor.zero_electric_angle="); + Serial.print(motor.zero_electric_angle, 4); + Serial.println(";"); + + _delay(1000); + Serial.println("If motor is not moving the alignment procedure was not successfull!!"); +} + + +void loop() { + + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(2); +} + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino new file mode 100644 index 0000000..60bbcf1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino @@ -0,0 +1,53 @@ +/** + * Simple example of custom commands that have nothing to do with the simple foc library + */ + +#include + +// instantiate the commander +Commander command = Commander(Serial); + +// led control function +void doLed(char* cmd){ + if(atoi(cmd)) digitalWrite(LED_BUILTIN, HIGH); + else digitalWrite(LED_BUILTIN, LOW); +}; +// get analog input +void doAnalog(char* cmd){ + if (cmd[0] == '0') Serial.println(analogRead(A0)); + else if (cmd[0] == '1') Serial.println(analogRead(A1)); + else if (cmd[0] == '2') Serial.println(analogRead(A2)); + else if (cmd[0] == '3') Serial.println(analogRead(A3)); + else if (cmd[0] == '4') Serial.println(analogRead(A4)); +}; + +void setup() { + // define pins + pinMode(LED_BUILTIN, OUTPUT); + pinMode(A0, INPUT); + pinMode(A1, INPUT); + pinMode(A2, INPUT); + pinMode(A3, INPUT); + pinMode(A4, INPUT); + + // Serial port to be used + Serial.begin(115200); + + // add new commands + command.add('L', doLed, "led on/off"); + command.add('A', doAnalog, "analog read A0-A4"); + + Serial.println(F("Commander listening")); + Serial.println(F(" - Send ? to see the node list...")); + Serial.println(F(" - Send L0 to turn the led off and L1 to turn it off")); + Serial.println(F(" - Send A0-A4 to read the analog pins")); + _delay(1000); +} + + +void loop() { + + // user communication + command.run(); + _delay(10); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino new file mode 100644 index 0000000..1381515 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino @@ -0,0 +1,51 @@ +/** + * Simple example of how to use the commander without serial - using just strings + */ + +#include + +// instantiate the commander +Commander command = Commander(); + +// led control function +void doLed(char* cmd){ + if(atoi(cmd)) digitalWrite(LED_BUILTIN, HIGH); + else digitalWrite(LED_BUILTIN, LOW); +}; +// get analog input +void doAnalog(char* cmd){ + if (cmd[0] == '0') Serial.println(analogRead(A0)); + else if (cmd[0] == '1') Serial.println(analogRead(A1)); +}; + +void setup() { + // define pins + pinMode(LED_BUILTIN, OUTPUT); + pinMode(A0, INPUT); + pinMode(A1, INPUT); + + // Serial port to be used + Serial.begin(115200); + + // add new commands + command.add('L', doLed, "led control"); + command.add('A', doAnalog, "analog read A0-A1"); + + Serial.println(F("Commander running")); + _delay(1000); +} + + +void loop() { + // user communication + command.run("?"); + _delay(2000); + command.run("L0"); + _delay(1000); + command.run("A0"); + _delay(1000); + command.run("A1"); + _delay(1000); + command.run("L1"); + _delay(1000); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino new file mode 100644 index 0000000..cbeb6ec --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino @@ -0,0 +1,79 @@ +/** + * A simple example to show how to use the commander with the control loops outside of the scope of the SimpleFOC library +*/ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA() { encoder.handleA(); } +void doB() { encoder.handleB(); } + +// target voltage to be set to the motor +float target_velocity = 0; + +// PID controllers and low pass filters +PIDController PIDv{0.05, 1, 0, 100000000, 12}; +LowPassFilter LPFv{0.01}; + +//add communication +Commander command = Commander(Serial); +void doController(char* cmd) { command.pid(&PIDv, cmd); } +void doFilter(char* cmd) { command.lpf(&LPFv, cmd); } +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set motion control loop to be used ( doing nothing ) + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + motor.useMonitoring(Serial); + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // subscribe the new commands + command.add('C', doController, "tune velocity pid"); + command.add('F', doFilter, "tune velocity LPF"); + command.add('T', doTarget, "motor target"); + + _delay(1000); + Serial.println(F("Commander listening")); + Serial.println(F(" - Send ? to see the node list...")); +} + + + +void loop() { + // looping foc + motor.loopFOC(); + + // calculate voltage + float target_voltage = PIDv(target_velocity - LPFv(motor.shaft_velocity)); + // set the voltage + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino new file mode 100644 index 0000000..a0bebc3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino @@ -0,0 +1,36 @@ +/** + * A simple example of reading step/dir communication + * - this example uses hadware interrupts +*/ + +#include + +// angle +float received_angle = 0; + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(2, 3, 360.0/200.0); // receive the angle in degrees +void onStep() { step_dir.handle(); } + +void setup() { + + Serial.begin(115200); + + // init step and dir pins + step_dir.init(); + // enable interrupts + step_dir.enableInterrupt(onStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling step_dir.getValue(); + step_dir.attach(&received_angle); + + Serial.println(F("Step/Dir listenning.")); + _delay(1000); +} + +void loop() { + Serial.print(received_angle); + Serial.print("\t"); + Serial.println(step_dir.getValue()); + _delay(500); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino new file mode 100644 index 0000000..b6f0ea5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino @@ -0,0 +1,44 @@ +/** + * A simple example of reading step/dir communication + * - this example uses software interrupts - this code is intended primarily + * for Arduino UNO/Mega and similar boards with very limited number of interrupt pins +*/ + +#include +// software interrupt library +#include +#include + + +// angle +float received_angle = 0; + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(4, 5, 2.0f*_PI/200.0); // receive the angle in radians +void onStep() { step_dir.handle(); } + +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenStep(step_dir.pin_step, onStep); + +void setup() { + + Serial.begin(115200); + + // init step and dir pins + step_dir.init(); + // enable software interrupts + PciManager.registerListener(&listenStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling step_dir.getValue(); + step_dir.attach(&received_angle); + + Serial.println(F("Step/Dir listenning.")); + _delay(1000); +} + +void loop() { + Serial.print(received_angle); + Serial.print("\t"); + Serial.println(step_dir.getValue()); + _delay(500); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino new file mode 100644 index 0000000..f897857 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino @@ -0,0 +1,98 @@ +/** + * A position control example using step/dir interface to update the motor position + */ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(10, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA() { encoder.handleA(); } +void doB() { encoder.handleB(); } + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(A4, A5, 2.0f*_PI/200.0); +void onStep() { step_dir.handle(); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 10; + // maximal velocity of the position control + motor.velocity_limit = 100; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // init step and dir pins + step_dir.init(); + // enable interrupts + step_dir.enableInterrupt(onStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling motor.move(step_dir.getValue()); + step_dir.attach(&motor.target); + + Serial.println(F("Motor ready.")); + Serial.println(F("Listening to step/dir commands!")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino new file mode 100644 index 0000000..e999de2 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino @@ -0,0 +1,59 @@ +/** + * An example code for the generic current sensing implementation +*/ +#include + + +// user defined function for reading the phase currents +// returning the value per phase in amps +PhaseCurrent_s readCurrentSense(){ + PhaseCurrent_s c; + // dummy example only reading analog pins + c.a = analogRead(A0); + c.b = analogRead(A1); + c.c = analogRead(A2); // if no 3rd current sense set it to 0 + return(c); +} + +// user defined function for intialising the current sense +// it is optional and if provided it will be called in current_sense.init() +void initCurrentSense(){ + pinMode(A0,INPUT); + pinMode(A1,INPUT); + pinMode(A2,INPUT); +} + + +// GenericCurrentSense class constructor +// it receives the user defined callback for reading the current sense +// and optionally the user defined callback for current sense initialisation +GenericCurrentSense current_sense = GenericCurrentSense(readCurrentSense, initCurrentSense); + + +void setup() { + // if callbacks are not provided in the constructor + // they can be assigned directly: + //current_sense.readCallback = readCurrentSense; + //current_sense.initCallback = initCurrentSense; + + // initialise the current sensing + current_sense.init(); + + + Serial.begin(115200); + Serial.println("Current sense ready."); +} + +void loop() { + + PhaseCurrent_s currents = current_sense.getPhaseCurrents(); + float current_magnitude = current_sense.getDCCurrent(); + + Serial.print(currents.a); // milli Amps + Serial.print("\t"); + Serial.print(currents.b); // milli Amps + Serial.print("\t"); + Serial.print(currents.c); // milli Amps + Serial.print("\t"); + Serial.println(current_magnitude); // milli Amps +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino new file mode 100644 index 0000000..c1a5139 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino @@ -0,0 +1,36 @@ +/** + * Testing example code for the Inline current sensing class +*/ +#include + +// current sensor +// shunt resistor value +// gain value +// pins phase A,B, (C optional) +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + + +void setup() { + // initialise the current sensing + current_sense.init(); + + // for SimpleFOCShield v2.01/v2.0.2 + current_sense.gain_b *= -1; + + Serial.begin(115200); + Serial.println("Current sense ready."); +} + +void loop() { + + PhaseCurrent_s currents = current_sense.getPhaseCurrents(); + float current_magnitude = current_sense.getDCCurrent(); + + Serial.print(currents.a*1000); // milli Amps + Serial.print("\t"); + Serial.print(currents.b*1000); // milli Amps + Serial.print("\t"); + Serial.print(currents.c*1000); // milli Amps + Serial.print("\t"); + Serial.println(current_magnitude*1000); // milli Amps +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino new file mode 100644 index 0000000..1235fcc --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino @@ -0,0 +1,34 @@ +// BLDC driver standalone example +#include + + +// BLDC driver instance +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 50000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..478d397 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,35 @@ +// BLDC driver standalone example +#include + +// BLDC driver instance +BLDCDriver6PWM driver = BLDCDriver6PWM(5, 6, 9,10, 3, 11, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 50000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + // daad_zone [0,1] - default 0.02f - 2% + driver.dead_zone = 0.05f; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino new file mode 100644 index 0000000..4627efa --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino @@ -0,0 +1,39 @@ +// Stepper driver standalone example +#include + + +// Stepper driver instance +// StepperDriver2PWM(pwm1, in1, pwm2, in2, (en1, en2 optional)) +int in1[] = {4,5}; +int in2[] = {9,8}; +StepperDriver2PWM driver = StepperDriver2PWM(3, in1, 10 , in2, 11, 12); + +// StepperDriver2PWM(pwm1, dir1, pwm2, dir2,(en1, en2 optional)) +// StepperDriver2PWM driver = StepperDriver2PWM(3, 4, 5, 6, 11, 12); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 30000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + driver.setPwm(3,6); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino new file mode 100644 index 0000000..e028a73 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino @@ -0,0 +1,34 @@ +// Stepper driver standalone example +#include + + +// Stepper driver instance +// StepperDriver4PWM(ph1A, ph1B, ph2A, ph2B, (en1, en2 optional)) +StepperDriver4PWM driver = StepperDriver4PWM(5, 6, 9,10, 7, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 30000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + driver.setPwm(3,6); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino new file mode 100644 index 0000000..f105f5b --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino @@ -0,0 +1,44 @@ +/** + * Encoder example code + * + * This is a code intended to test the encoder connections and to demonstrate the encoder setup. + * + */ + +#include + + +Encoder encoder = Encoder(2, 3, 8192); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // enable/disable quadrature mode + encoder.quadrature = Quadrature::ON; + + // check if you need internal pullups + encoder.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + + Serial.println("Encoder ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // not doing much for the encoder though + encoder.update(); + // display the angle and the angular velocity to the terminal + Serial.print(encoder.getAngle()); + Serial.print("\t"); + Serial.println(encoder.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino new file mode 100644 index 0000000..ebf2dd4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino @@ -0,0 +1,57 @@ +/** + * Encoder example code using only software interrupts + * + * This is a code intended to test the encoder connections and to + * demonstrate the encoder setup fully using software interrupts. + * - We use PciManager library: https://github.com/prampec/arduino-pcimanager + * + * This code will work on Arduino devices but not on STM32 devices + * + */ + +#include +// software interrupt library +#include +#include + +// encoder instance +Encoder encoder = Encoder(A0, A1, 2048); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + +void setup() { + // monitoring port + Serial.begin(115200); + + // enable/disable quadrature mode + encoder.quadrature = Quadrature::ON; + + // check if you need internal pullups + encoder.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + encoder.init(); + + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + + Serial.println("Encoder ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // not doing much for the encoder though + encoder.update(); + // display the angle and the angular velocity to the terminal + Serial.print(encoder.getAngle()); + Serial.print("\t"); + Serial.println(encoder.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino new file mode 100644 index 0000000..4a470e5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino @@ -0,0 +1,51 @@ +/** + * Generic sensor example code + * + * This is a code intended to demonstrate how to implement the generic sensor class + * + */ + +#include + +// sensor reading function example +// for the magnetic sensor with analog communication +// returning an angle in radians in between 0 and 2PI +float readSensor(){ + return analogRead(A0)*_2PI/1024.0; +} + +// sensor intialising function +void initSensor(){ + pinMode(A0,INPUT); +} + +// generic sensor class contructor +// - read sensor callback +// - init sensor callback (optional) +GenericSensor sensor = GenericSensor(readSensor, initSensor); + +void setup() { + // monitoring port + Serial.begin(115200); + + // if callbacks are not provided in the constructor + // they can be assigned directly: + //sensor.readCallback = readSensor; + //sensor.initCallback = initSensor; + + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino new file mode 100644 index 0000000..c4777ba --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino @@ -0,0 +1,48 @@ +/** + * Hall sensor example code + * + * This is a code intended to test the hall sensors connections and to demonstrate the hall sensor setup. + * + */ + +#include + +// Hall sensor instance +// HallSensor(int hallA, int hallB , int cpr, int index) +// - hallA, hallB, hallC - HallSensor A, B and C pins +// - pp - pole pairs +HallSensor sensor = HallSensor(2, 3, 4, 14); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +void setup() { + // monitoring port + Serial.begin(115200); + + // check if you need internal pullups + sensor.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + sensor.init(); + // hardware interrupt enable + sensor.enableInterrupts(doA, doB, doC); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); + delay(100); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino new file mode 100644 index 0000000..238eb2c --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino @@ -0,0 +1,59 @@ +/** + * Hall sensors example code using only software interrupts + * + * This is a code intended to test the hall sensor connections and to + * demonstrate the hall sensor setup fully using software interrupts. + * - We use PciManager library: https://github.com/prampec/arduino-pcimanager + * + * This code will work on Arduino devices but not on STM32 devices + */ + +#include +// software interrupt library +#include +#include + +// Hall sensor instance +// HallSensor(int hallA, int hallB , int cpr, int index) +// - hallA, hallB, hallC - HallSensor A, B and C pins +// - pp - pole pairs +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenA(sensor.pinA, doA); +PciListenerImp listenB(sensor.pinB, doB); +PciListenerImp listenC(sensor.pinC, doC); + +void setup() { + // monitoring port + Serial.begin(115200); + + // check if you need internal pullups + sensor.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + sensor.init(); + + // software interrupts + PciManager.registerListener(&listenA); + PciManager.registerListener(&listenB); + PciManager.registerListener(&listenC); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino new file mode 100644 index 0000000..5fda5f5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino @@ -0,0 +1,62 @@ +/** + * An example to find the center offsets for both ADC channels used in the LinearHall sensor constructor + * Spin your motor through at least one full revolution to average out all of the variations in magnet strength. + */ + +//Change these defines to match the analog input pins that your hall sensors are connected to +#define LINEAR_HALL_CHANNEL_A 39 +#define LINEAR_HALL_CHANNEL_B 33 + + +//program variables +int minA, maxA, minB, maxB, centerA, centerB; +unsigned long timestamp; + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + pinMode(LINEAR_HALL_CHANNEL_A, INPUT); + pinMode(LINEAR_HALL_CHANNEL_B, INPUT); + + minA = analogRead(LINEAR_HALL_CHANNEL_A); + maxA = minA; + centerA = (minA + maxA) / 2; + minB = analogRead(LINEAR_HALL_CHANNEL_B); + maxB = minB; + centerB = (minB + maxB) / 2; + + Serial.println("Sensor ready"); + delay(1000); + timestamp = millis(); +} + +void loop() { + //read sensors and update variables + int tempA = analogRead(LINEAR_HALL_CHANNEL_A); + if (tempA < minA) minA = tempA; + if (tempA > maxA) maxA = tempA; + centerA = (minA + maxA) / 2; + int tempB = analogRead(LINEAR_HALL_CHANNEL_B); + if (tempB < minB) minB = tempB; + if (tempB > maxB) maxB = tempB; + centerB = (minB + maxB) / 2; + + if (millis() > timestamp + 100) { + timestamp = millis(); + // display the center counts, and max and min count + Serial.print("A:"); + Serial.print(centerA); + Serial.print("\t, B:"); + Serial.print(centerB); + Serial.print("\t, min A:"); + Serial.print(minA); + Serial.print("\t, max A:"); + Serial.print(maxA); + Serial.print("\t, min B:"); + Serial.print(minB); + Serial.print("\t, max B:"); + Serial.println(maxB); + } +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino new file mode 100644 index 0000000..34e68a1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino @@ -0,0 +1,56 @@ +#include + +/** + * An example to find out the raw max and min count to be provided to the constructor + * Spin your motor/sensor/magnet to see what is the maximum output of the sensor and what is the minimum value + * And replace values 14 and 1020 with new values. Once when you replace them make sure there is no jump in the angle reading sensor.getAngle(). + * If there is a jump that means you can still find better values. + */ + +/** + * Magnetic sensor reading analog voltage on pin A1. This voltage is proportional to rotation position. + * Tested on AS5600 magnetic sensor running in 'analog mode'. Note AS5600 works better in 'i2C mode' (less noise) but only supports one sensor per i2c bus. + * + * MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * - pinAnalog - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note ESP32 will be closer to 4096 with its 12bit ADC + */ +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +int max_count = 0; +int min_count = 100000; + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // keep track of min and max + if(sensor.raw_count > max_count) max_count = sensor.raw_count; + else if(sensor.raw_count < min_count) min_count = sensor.raw_count; + + // display the raw count, and max and min raw count + Serial.print("angle:"); + Serial.print(sensor.getAngle()); + Serial.print("\t, raw:"); + Serial.print(sensor.raw_count); + Serial.print("\t, min:"); + Serial.print(min_count); + Serial.print("\t, max:"); + Serial.println(max_count); + delay(100); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino new file mode 100644 index 0000000..2496151 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino @@ -0,0 +1,37 @@ +#include + + + +/** + * Magnetic sensor reading analog voltage on pin A1. This voltage is proportional to rotation position. + * Tested on AS5600 magnetic sensor running in 'analog mode'. Note AS5600 works better in 'i2C mode' (less noise) but only supports one sensor per i2c bus. + * + * MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * - pinAnalog - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note ESP32 will be closer to 4096 with its 12bit ADC + */ +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino new file mode 100644 index 0000000..0516ede --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino @@ -0,0 +1,40 @@ +#include + +/** Annoyingly some i2c sensors (e.g. AS5600) have a fixed chip address. This means only one of these devices can be addressed on a single bus + * This example shows how a second i2c bus can be used to communicate with a second sensor. + */ + +MagneticSensorI2C sensor0 = MagneticSensorI2C(AS5600_I2C); +MagneticSensorI2C sensor1 = MagneticSensorI2C(AS5600_I2C); + + +void setup() { + + Serial.begin(115200); + _delay(750); + + Wire.setClock(400000); + Wire1.setClock(400000); + + // Normally SimpleFOC will call begin for i2c but with esp32 begin() is the only way to set pins! + // It seems safe to call begin multiple times + Wire1.begin(19, 23, (uint32_t)400000); + + sensor0.init(); + sensor1.init(&Wire1); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor0.update(); + sensor1.update(); + + _delay(200); + Serial.print(sensor0.getAngle()); + Serial.print(" - "); + Serial.print(sensor1.getAngle()); + Serial.println(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino new file mode 100644 index 0000000..08fb145 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino @@ -0,0 +1,39 @@ +#include + +/** Annoyingly some i2c sensors (e.g. AS5600) have a fixed chip address. This means only one of these devices can be addressed on a single bus + * This example shows how a second i2c bus can be used to communicate with a second sensor. + */ + +MagneticSensorI2C sensor0 = MagneticSensorI2C(AS5600_I2C); +MagneticSensorI2C sensor1 = MagneticSensorI2C(AS5600_I2C); + +// example of stm32 defining 2nd bus +TwoWire Wire1(PB11, PB10); + + +void setup() { + + Serial.begin(115200); + _delay(750); + + Wire.setClock(400000); + Wire1.setClock(400000); + + sensor0.init(); + sensor1.init(&Wire1); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor0.update(); + sensor1.update(); + + _delay(200); + Serial.print(sensor0.getAngle()); + Serial.print(" - "); + Serial.print(sensor1.getAngle()); + Serial.println(); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino new file mode 100644 index 0000000..4e060c0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino @@ -0,0 +1,43 @@ +#include + +// MagneticSensorI2C(uint8_t _chip_address, float _cpr, uint8_t _angle_register_msb) +// chip_address I2C chip address +// bit_resolution resolution of the sensor +// angle_register_msb angle read register msb +// bits_used_msb number of used bits in msb register +// +// make sure to read the chip address and the chip angle register msb value from the datasheet +// also in most cases you will need external pull-ups on SDA and SCL lines!!!!! +// +// For AS5058B +// MagneticSensorI2C sensor = MagneticSensorI2C(0x40, 14, 0xFE, 8); + +// Example of AS5600 configuration +MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); + + +void setup() { + // monitoring port + Serial.begin(115200); + + // configure i2C + Wire.setClock(400000); + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino new file mode 100644 index 0000000..ac9bf04 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino @@ -0,0 +1,49 @@ +#include + + +/** + * An example to find out the raw max and min count to be provided to the constructor + * SPin your motor/sensor/magnet to see what is the maximum output of the sensor and what is the minimum value + * And replace values 4 and 904 with new values. Once when you replace them make sure there is no jump in the angle reading sensor.getAngle(). + * If there is a jump that means you can still find better values. + */ +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +void doPWM(){sensor.handlePWM();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + sensor.enableInterrupt(doPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +int max_pulse= 0; +int min_pulse = 10000; + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // keep track of min and max + if(sensor.pulse_length_us > max_pulse) max_pulse = sensor.pulse_length_us; + else if(sensor.pulse_length_us < min_pulse) min_pulse = sensor.pulse_length_us; + + // display the raw count, and max and min raw count + Serial.print("angle:"); + Serial.print(sensor.getAngle()); + Serial.print("\t, raw:"); + Serial.print(sensor.pulse_length_us); + Serial.print("\t, min:"); + Serial.print(min_pulse); + Serial.print("\t, max:"); + Serial.println(max_pulse); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino new file mode 100644 index 0000000..6ae0a3e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino @@ -0,0 +1,38 @@ +#include + + +/** + * Magnetic sensor reading pwm signal on pin 2. The pwm duty cycle is proportional to the sensor angle. + * + * MagneticSensorPWM(uint8_t MagneticSensorPWM, int _min, int _max) + * - pinPWM - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~5. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 1kHz = 1000 it is often ~910. depending on the exact frequency and saturation + */ +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +void doPWM(){sensor.handlePWM();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + sensor.enableInterrupt(doPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino new file mode 100644 index 0000000..10dc8a6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino @@ -0,0 +1,44 @@ +#include + +// software interrupt library +#include +#include + +/** + * Magnetic sensor reading analog voltage on pin which does not have hardware interrupt support. Such as A0. + * + * MagneticSensorPWM(uint8_t MagneticSensorPWM, int _min, int _max) + * - pinPWM - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~5. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 1kHz = 1000 it is often ~910. depending on the exact frequency and saturation + */ +MagneticSensorPWM sensor = MagneticSensorPWM(A0, 4, 904); +void doPWM(){sensor.handlePWM();} + +// encoder interrupt init +PciListenerImp listenerPWM(sensor.pinPWM, doPWM); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + PciManager.registerListener(&listenerPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino new file mode 100644 index 0000000..cbb72af --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino @@ -0,0 +1,41 @@ +#include + +// alternative pinout +#define HSPI_MISO 12 +#define HSPI_MOSI 13 +#define HSPI_SCLK 14 +#define HSPI_SS 15 + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, HSPI_SS); + +// for esp 32, it has 2 spi interfaces VSPI (default) and HPSI as the second one +// to enable it instatiate the object +SPIClass SPI_2(HSPI); + +void setup() { + // monitoring port + Serial.begin(115200); + + // start the newly defined spi communication + SPI_2.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_SS); //SCLK, MISO, MOSI, SS + // initialise magnetic sensor hardware + sensor.init(&SPI_2); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino new file mode 100644 index 0000000..713a19b --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino @@ -0,0 +1,32 @@ +#include + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, PA15); + +// these are valid pins (mosi, miso, sclk) for 2nd SPI bus on storm32 board (stm32f107rc) +SPIClass SPI_2(PB15, PB14, PB13); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(&SPI_2); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino new file mode 100644 index 0000000..048620a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino @@ -0,0 +1,32 @@ +#include + +// MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs) +// config - SPI config +// cs - SPI chip select pin +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// alternative constructor (chipselsect, bit_resolution, angle_read_register, ) +// MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/keywords.txt b/.pio/libdeps/aioli-foc/Simple FOC/keywords.txt new file mode 100644 index 0000000..1f200e5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/keywords.txt @@ -0,0 +1,251 @@ +ArduinoFOC KEYWORD1 +SimpleFOC KEYWORD1 +BLDCMotor KEYWORD1 +FOCMotor KEYWORD1 +StepperMotor KEYWORD1 +Encoder KEYWORD1 +HallSensors KEYWORD1 +MagneticSensorSPI KEYWORD1 +MagneticSensorI2C KEYWORD1 +MagneticSensorAnalog KEYWORD1 +MagneticSensorPWM KEYWORD1 +BLDCDriver3PWM KEYWORD1 +BLDCDriver6PWM KEYWORD1 +BLDCDriver KEYWORD1 +StepperDriver4PWM KEYWORD1 +StepperDriver2PWM KEYWORD1 +StepperDriver KEYWORD1 +PIDController KEYWORD1 +LowPassFilter KEYWORD1 +InlineCurrentSense KEYWORD1 +LowsideCurrentSense KEYWORD1 +CurrentSense KEYWORD1 +Commander KEYWORD1 +StepDirListener KEYWORD1 +GenericCurrentSense KEYWORD1 +GenericSensor KEYWORD1 +SimpleFOCDebug KEYWORD1 + +initFOC KEYWORD2 +loopFOC KEYWORD2 +disable KEYWORD2 + +_delay KEYWORD3 +_sqrt KEYWORD3 +_micros KEYWORD3 +_sin KEYWORD3 +_cos KEYWORD3 +_setPwmFrequency KEYWORD3 +_writeDutyCycle KEYWORD3 +_round KEYWORD3 +_sign KEYWORD3 +_constrain KEYWORD3 +monitor KEYWORD3 +command KEYWORD3 + +PID_velocity KEYWORD2 +PID_current_q KEYWORD2 +PID_current_d KEYWORD2 +LPF_velocity KEYWORD2 +LPF_current_q KEYWORD2 +LPF_current_d KEYWORD2 +P_angle KEYWORD2 +LPF_angle KEYWORD2 +lpf_a KEYWORD2 +lpf_b KEYWORD2 +lpf_c KEYWORD2 + +MotionControlType KEYWORD1 +TorqueControlType KEYWORD1 +FOCModulationType KEYWORD2 +Quadrature KEYWORD1 +Pullup KEYWORD1 +Direction KEYWORD1 +MagneticSensorI2CConfig_s KEYWORD1 +MagneticSensorSPIConfig_s KEYWORD1 +DQVoltage_s KEYWORD1 +DQCurrent_s KEYWORD1 +PhaseCurrent_s KEYWORD1 + +linkDriver KEYWORD2 +linkSensor KEYWORD2 +linkCurrentSense KEYWORD2 +handleA KEYWORD2 +handleB KEYWORD2 +handleIndex KEYWORD2 +handleC KEYWORD2 +enableInterrupts KEYWORD2 +ISR KEYWORD2 +getVelocity KEYWORD2 +setPhaseVoltage KEYWORD2 +getAngle KEYWORD2 +getMechanicalAngle KEYWORD2 +getSensorAngle KEYWORD2 +update KEYWORD2 +needsSearch KEYWORD2 +useMonitoring KEYWORD2 +angleOpenloop KEYWORD2 +velocityOpenloop KEYWORD2 +getPhaseCurrents KEYWORD2 +getFOCCurrents KEYWORD2 +getDCCurrent KEYWORD2 +setPwm KEYWORD2 +driverAlign KEYWORD2 +linkDriver KEYWORD2 +add KEYWORD2 +run KEYWORD2 +attach KEYWORD2 +enableInterrupt KEYWORD2 +getValue KEYWORD2 +handle KEYWORD2 +scalar KEYWORD2 +pid KEYWORD2 +lpf KEYWORD2 +motor KEYWORD2 +handlePWM KEYWORD2 +enableInterrupt KEYWORD2 +readCallback KEYWORD2 +initCallback KEYWORD2 + + + +current KEYWORD2 +current_measured KEYWORD2 +shaft_angle_sp KEYWORD2 +electrical_angle KEYWORD2 +shaft_velocity_sp KEYWORD2 +shaft_angle KEYWORD2 +shaft_velocity KEYWORD2 +torque_controller KEYWORD2 +controller KEYWORD2 +pullup KEYWORD2 +quadrature KEYWORD2 +foc_modulation KEYWORD2 +target KEYWORD2 +motion KEYWORD2 +pwm_frequency KEYWORD2 +dead_zone KEYWORD2 +gain_a KEYWORD2 +gain_b KEYWORD2 +gain_c KEYWORD2 +skip_align KEYWORD2 +sensor_direction KEYWORD2 +sensor_offset KEYWORD2 +zero_electric_angle KEYWORD2 +verbose KEYWORD2 +decimal_places KEYWORD2 +phase_resistance KEYWORD2 +phase_inductance KEYWORD2 +modulation_centered KEYWORD2 + + + +voltage KEYWORD2 +velocity KEYWORD2 +velocity_openloop KEYWORD2 +angle KEYWORD2 +angle_openloop KEYWORD2 +torque KEYWORD2 +dc_current KEYWORD2 +foc_current KEYWORD2 + + +ON KEYWORD2 +OFF KEYWORD2 +CW KEYWORD2 +CCW KEYWORD2 +UNKNOWN KEYWORD2 + +P KEYWORD2 +I KEYWORD2 +D KEYWORD2 +Tf KEYWORD2 +voltage_limit KEYWORD2 +current_limit KEYWORD2 +output_ramp KEYWORD2 +limit KEYWORD2 +velocity_limit KEYWORD2 +voltage_power_supply KEYWORD2 +voltage_sensor_align KEYWORD2 +velocity_index_search KEYWORD2 +monitor_downsample KEYWORD2 +monitor_start_char KEYWORD2 +monitor_end_char KEYWORD2 +monitor_separator KEYWORD2 +monitor_decimals KEYWORD2 +monitor_variables KEYWORD2 +motion_downsample KEYWORD2 + +pinA KEYWORD2 +pinB KEYWORD2 +pinC KEYWORD2 +index_pin KEYWORD2 + +USE_INTERN KEYWORD2 +USE_EXTERN KEYWORD2 +DISABLE KEYWORD2 +ENABLE KEYWORD2 +SpaceVectorPWM KEYWORD2 +SinePWM KEYWORD2 +Trapezoid_120 KEYWORD2 +Trapezoid_150 KEYWORD2 + +pwmA KEYWORD2 +pwmB KEYWORD2 +pwmC KEYWORD2 +pwm1A KEYWORD2 +pwm1B KEYWORD2 +pwm2A KEYWORD2 +pwm2B KEYWORD2 +Ualpha KEYWORD2 +Ubeta KEYWORD2 +Ua KEYWORD2 +Ub KEYWORD2 +Uc KEYWORD2 +enable_pin KEYWORD2 +enable_pin1 KEYWORD2 +enable_pin2 KEYWORD2 +pole_pairs KEYWORD2 +dc_a KEYWORD2 +dc_b KEYWORD2 +dc_c KEYWORD2 + +DEF_POWER_SUPPLY LITERAL1 +DEF_PID_VEL_P LITERAL1 +DEF_PID_VEL_I LITERAL1 +DEF_PID_VEL_D LITERAL1 +DEF_PID_VEL_RAMP LITERAL1 +DEF_P_ANGLE_P LITERAL1 +DEF_PID_VEL_LIMIT LITERAL1 +DEF_INDEX_SEARCH_TARGET_VELOCITY LITERAL1 +DEF_VOLTAGE_SENSOR_ALIGN LITERAL1 +DEF_VEL_FILTER_Tf LITERAL1 +DEF_CURRENT_LIM LITERAL1 +DEF_CURR_FILTER_Tf LITERAL1 +DEF_PID_CURR_LIMIT LITERAL1 +DEF_PID_CURR_RAMP LITERAL1 +DEF_PID_CURR_D LITERAL1 +DEF_PID_CURR_I LITERAL1 +DEF_PID_CURR_P LITERAL1 +_2_SQRT3 LITERAL1 +_1_SQRT3 LITERAL1 +_SQRT3_2 LITERAL1 +_SQRT2 LITERAL1 +_120_D2R LITERAL1 +_PI_2 LITERAL1 +_PI_6 LITERAL1 +_2PI LITERAL1 +_3PI_2 LITERAL1 +_PI_3 LITERAL1 +_SQRT3 LITERAL1 +_PI LITERAL1 +_HIGH_Z LITERAL1 +_NC LITERAL1 + +_MON_TARGET LITERAL1 +_MON_VOLT_Q LITERAL1 +_MON_VOLT_D LITERAL1 +_MON_CURR_Q LITERAL1 +_MON_CURR_D LITERAL1 +_MON_VEL LITERAL1 +_MON_ANGLE LITERAL1 \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/library.properties b/.pio/libdeps/aioli-foc/Simple FOC/library.properties new file mode 100644 index 0000000..6904387 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/library.properties @@ -0,0 +1,10 @@ +name=Simple FOC +version=2.3.1 +author=Simplefoc +maintainer=Simplefoc +sentence=A library demistifying FOC for BLDC motors +paragraph=Simple library intended for hobby comunity to run the BLDC and Stepper motor using FOC algorithm. It is intended to support as many BLDC/Stepper motor+sensor+driver combinations as possible and in the same time maintain simplicity of usage. Library supports Arudino devices such as Arduino UNO, MEGA, NANO and similar, stm32 boards such as Nucleo and Bluepill, ESP32 and Teensy boards. +category=Device Control +url=https://docs.simplefoc.com +architectures=* +includes=SimpleFOC.h diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.cpp new file mode 100644 index 0000000..b72728b --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.cpp @@ -0,0 +1,736 @@ +#include "BLDCMotor.h" +#include "./communication/SimpleFOCDebug.h" + + +// see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 5 +// each is 60 degrees with values for 3 phases of 1=positive -1=negative 0=high-z +int trap_120_map[6][3] = { + {_HIGH_IMPEDANCE,1,-1}, + {-1,1,_HIGH_IMPEDANCE}, + {-1,_HIGH_IMPEDANCE,1}, + {_HIGH_IMPEDANCE,-1,1}, + {1,-1,_HIGH_IMPEDANCE}, + {1,_HIGH_IMPEDANCE,-1} +}; + +// see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 8 +// each is 30 degrees with values for 3 phases of 1=positive -1=negative 0=high-z +int trap_150_map[12][3] = { + {_HIGH_IMPEDANCE,1,-1}, + {-1,1,-1}, + {-1,1,_HIGH_IMPEDANCE}, + {-1,1,1}, + {-1,_HIGH_IMPEDANCE,1}, + {-1,-1,1}, + {_HIGH_IMPEDANCE,-1,1}, + {1,-1,1}, + {1,-1,_HIGH_IMPEDANCE}, + {1,-1,-1}, + {1,_HIGH_IMPEDANCE,-1}, + {1,1,-1} +}; + +// BLDCMotor( int pp , float R) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance +BLDCMotor::BLDCMotor(int pp, float _R, float _KV, float _inductance) +: FOCMotor() +{ + // save pole pairs number + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/KV + // 1/sqrt(2) - rms value + KV_rating = NOT_SET; + if (_isset(_KV)) + KV_rating = _KV*_SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + torque_controller = TorqueControlType::voltage; +} + + +/** + Link the driver which controls the motor +*/ +void BLDCMotor::linkDriver(BLDCDriver* _driver) { + driver = _driver; +} + +// init hardware pins +void BLDCMotor::init() { + if (!driver || !driver->initialized) { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if(voltage_limit > driver->voltage_limit) voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if(voltage_sensor_align > voltage_limit) voltage_sensor_align = voltage_limit; + + // update the controller limits + if(current_sense){ + // current control loop controls voltage + PID_current_q.limit = voltage_limit; + PID_current_d.limit = voltage_limit; + } + if(_isset(phase_resistance) || torque_controller != TorqueControlType::voltage){ + // velocity control loop controls current + PID_velocity.limit = current_limit; + }else{ + // velocity control loop controls the voltage + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + // if using open loop control, set a CW as the default direction if not already set + if ((controller==MotionControlType::angle_openloop + ||controller==MotionControlType::velocity_openloop) + && (sensor_direction == Direction::UNKNOWN)) { + sensor_direction = Direction::CW; + } + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + motor_status = FOCMotorStatus::motor_uncalibrated; +} + + +// disable motor driver +void BLDCMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0, 0); + // disable the driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void BLDCMotor::enable() +{ + // enable the driver + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0, 0); + // motor status update + enabled = 1; +} + +/** + FOC functions +*/ +// FOC initialization function +int BLDCMotor::initFOC() { + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // align motor if necessary + // alignment necessary for encoders! + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if(sensor){ + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = shaftAngle(); + }else { + exit_flag = 0; // no FOC without sensor + SIMPLEFOC_DEBUG("MOT: No sensor."); + } + + // aligning the current sensor - can be skipped + // checks if driver phases are the same as current sense phases + // and checks the direction of measuremnt. + _delay(500); + if(exit_flag){ + if(current_sense){ + if (!current_sense->initialized) { + motor_status = FOCMotorStatus::motor_calib_failed; + SIMPLEFOC_DEBUG("MOT: Init FOC error, current sense not initialized"); + exit_flag = 0; + }else{ + exit_flag *= alignCurrentSense(); + } + } + else { SIMPLEFOC_DEBUG("MOT: No current sense."); } + } + + if(exit_flag){ + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + }else{ + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Calibarthe the motor and current sense phases +int BLDCMotor::alignCurrentSense() { + int exit_flag = 1; // success + + SIMPLEFOC_DEBUG("MOT: Align current sense."); + + // align current sense and the driver + exit_flag = current_sense->driverAlign(voltage_sensor_align); + if(!exit_flag){ + // error in current sense - phase either not measured or bad connection + SIMPLEFOC_DEBUG("MOT: Align error!"); + exit_flag = 0; + }else{ + // output the alignment status flag + SIMPLEFOC_DEBUG("MOT: Success: ", exit_flag); + } + + return exit_flag > 0; +} + +// Encoder alignment to electrical 0 angle +int BLDCMotor::alignSensor() { + int exit_flag = 1; //success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // check if sensor needs zero search + if(sensor->needsSearch()) exit_flag = absoluteZeroSearch(); + // stop init if not found index + if(!exit_flag) return exit_flag; + + // if unknown natural direction + if(sensor_direction==Direction::UNKNOWN){ + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + float moved = fabs(mid_angle - end_angle); + if (moved 0.5f ) { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI/moved); + } else { + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + + } else { SIMPLEFOC_DEBUG("MOT: Skip dir calib."); } + + // zero electric angle not known + if(!_isset(zero_electric_angle)){ + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + //zero_electric_angle = _normalizeAngle(_electricalAngle(sensor_direction*sensor->getAngle(), pole_pairs)); + _delay(20); + if(monitor_port){ + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } else { SIMPLEFOC_DEBUG("MOT: Skip offset calib."); } + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int BLDCMotor::absoluteZeroSearch() { + // sensor precision: this is all ok, as the search happens near the 0-angle, where the precision + // of float is sufficient. + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while(sensor->needsSearch() && shaft_angle < _2PI){ + angleOpenloop(1.5f*_2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if(monitor_port){ + if(sensor->needsSearch()) { SIMPLEFOC_DEBUG("MOT: Error: Not found!"); } + else { SIMPLEFOC_DEBUG("MOT: Success!"); } + } + return !sensor->needsSearch(); +} + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void BLDCMotor::loopFOC() { + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) sensor->update(); + + // if open-loop do nothing + if( controller==MotionControlType::angle_openloop || controller==MotionControlType::velocity_openloop ) return; + + // if disabled do nothing + if(!enabled) return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + switch (torque_controller) { + case TorqueControlType::voltage: + // no need to do anything really + break; + case TorqueControlType::dc_current: + if(!current_sense) return; + // read overall current magnitude + current.q = current_sense->getDCCurrent(electrical_angle); + // filter the value values + current.q = LPF_current_q(current.q); + // calculate the phase voltage + voltage.q = PID_current_q(current_sp - current.q); + // d voltage - lag compensation + if(_isset(phase_inductance)) voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + else voltage.d = 0; + break; + case TorqueControlType::foc_current: + if(!current_sense) return; + // read dq currents + current = current_sense->getFOCCurrents(electrical_angle); + // filter values + current.q = LPF_current_q(current.q); + current.d = LPF_current_d(current.d); + // calculate the phase voltages + voltage.q = PID_current_q(current_sp - current.q); + voltage.d = PID_current_d(-current.d); + // d voltage - lag compensation - TODO verify + // if(_isset(phase_inductance)) voltage.d = _constrain( voltage.d - current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + default: + // no torque control selected + SIMPLEFOC_DEBUG("MOT: no torque control selected!"); + break; + } + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or torque loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void BLDCMotor::move(float new_target) { + + // downsampling (optional) + if(motion_cnt++ < motion_downsample) return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if( controller!=MotionControlType::angle_openloop && controller!=MotionControlType::velocity_openloop ) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity TODO the velocity reading probably also shouldn't happen in open loop modes? + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if(!enabled) return; + // set internal target variable + if(_isset(new_target)) target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) voltage_bemf = shaft_velocity/KV_rating/_RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if(!current_sense && _isset(phase_resistance)) current.q = (voltage.q - voltage_bemf)/phase_resistance; + + // upgrade the current based voltage limit + switch (controller) { + case MotionControlType::torque: + if(torque_controller == TorqueControlType::voltage){ // if voltage torque control + if(!_isset(phase_resistance)) voltage.q = target; + else voltage.q = target*phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -target*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + }else{ + current_sp = target; // if current/foc_current torque control + } + break; + case MotionControlType::angle: + // TODO sensor precision: this calculation is not numerically precise. The target value cannot express precise positions when + // the angles are large. This results in not being able to command small changes at high position values. + // to solve this, the delta-angle has to be calculated in a numerically precise way. + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = feed_forward_velocity + P_angle( shaft_angle_sp - shaft_angle ); + shaft_angle_sp = _constrain(shaft_angle_sp,-velocity_limit, velocity_limit); + // calculate the torque command - sensor precision: this calculation is ok, but based on bad value from previous calculation + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + if(torque_controller == TorqueControlType::voltage){ + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + } + break; + case MotionControlType::velocity: + // velocity set point - sensor precision: this calculation is numerically precise. + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + if(torque_controller == TorqueControlType::voltage){ + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + } + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop - sensor precision: this calculation is numerically precise. + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; + break; + case MotionControlType::angle_openloop: + // angle control in open loop - + // TODO sensor precision: this calculation NOT numerically precise, and subject + // to the same problems in small set-point changes at high angles + // as the closed loop version. + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; + break; + } +} + + +// Method using FOC to set Uq and Ud to the motor at the optimal angle +// Function implementing Space Vector PWM and Sine PWM algorithms +// +// Function using sine approximation +// regular sin + cos ~300us (no memory usage) +// approx _sin + _cos ~110us (400Byte ~ 20% of memory) +void BLDCMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) { + + float center; + int sector; + float _ca,_sa; + + switch (foc_modulation) + { + case FOCModulationType::Trapezoid_120 : + // see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 5 + // determine the sector + sector = 6 * (_normalizeAngle(angle_el + _PI_6 ) / _2PI); // adding PI/6 to align with other modes + // centering the voltages around either + // modulation_centered == true > driver.voltage_limit/2 + // modulation_centered == false > or Adaptable centering, all phases drawn to 0 when Uq=0 + center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + + if(trap_120_map[sector][0] == _HIGH_IMPEDANCE){ + Ua= center; + Ub = trap_120_map[sector][1] * Uq + center; + Uc = trap_120_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_120_map[sector][1] == _HIGH_IMPEDANCE){ + Ua = trap_120_map[sector][0] * Uq + center; + Ub = center; + Uc = trap_120_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_OFF, PhaseState::PHASE_ON);// disable phase if possible + }else{ + Ua = trap_120_map[sector][0] * Uq + center; + Ub = trap_120_map[sector][1] * Uq + center; + Uc = center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_OFF);// disable phase if possible + } + + break; + + case FOCModulationType::Trapezoid_150 : + // see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 8 + // determine the sector + sector = 12 * (_normalizeAngle(angle_el + _PI_6 ) / _2PI); // adding PI/6 to align with other modes + // centering the voltages around either + // modulation_centered == true > driver.voltage_limit/2 + // modulation_centered == false > or Adaptable centering, all phases drawn to 0 when Uq=0 + center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + + if(trap_150_map[sector][0] == _HIGH_IMPEDANCE){ + Ua= center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_150_map[sector][1] == _HIGH_IMPEDANCE){ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_OFF, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_150_map[sector][2] == _HIGH_IMPEDANCE){ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_OFF); // disable phase if possible + }else{ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // enable all phases + } + + break; + + case FOCModulationType::SinePWM : + // Sinusoidal PWM modulation + // Inverse Park + Clarke transformation + _sincos(angle_el, &_sa, &_ca); + + // Inverse park transform + Ualpha = _ca * Ud - _sa * Uq; // -sin(angle) * Uq; + Ubeta = _sa * Ud + _ca * Uq; // cos(angle) * Uq; + + // center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + center = driver->voltage_limit/2; + // Clarke transform + Ua = Ualpha + center; + Ub = -0.5f * Ualpha + _SQRT3_2 * Ubeta + center; + Uc = -0.5f * Ualpha - _SQRT3_2 * Ubeta + center; + + if (!modulation_centered) { + float Umin = min(Ua, min(Ub, Uc)); + Ua -= Umin; + Ub -= Umin; + Uc -= Umin; + } + + break; + + case FOCModulationType::SpaceVectorPWM : + // Nice video explaining the SpaceVectorModulation (SVPWM) algorithm + // https://www.youtube.com/watch?v=QMSWUMEAejg + + // the algorithm goes + // 1) Ualpha, Ubeta + // 2) Uout = sqrt(Ualpha^2 + Ubeta^2) + // 3) angle_el = atan2(Ubeta, Ualpha) + // + // equivalent to 2) because the magnitude does not change is: + // Uout = sqrt(Ud^2 + Uq^2) + // equivalent to 3) is + // angle_el = angle_el + atan2(Uq,Ud) + + float Uout; + // a bit of optitmisation + if(Ud){ // only if Ud and Uq set + // _sqrt is an approx of sqrt (3-4% error) + Uout = _sqrt(Ud*Ud + Uq*Uq) / driver->voltage_limit; + // angle normalisation in between 0 and 2pi + // only necessary if using _sin and _cos - approximation functions + angle_el = _normalizeAngle(angle_el + atan2(Uq, Ud)); + }else{// only Uq available - no need for atan2 and sqrt + Uout = Uq / driver->voltage_limit; + // angle normalisation in between 0 and 2pi + // only necessary if using _sin and _cos - approximation functions + angle_el = _normalizeAngle(angle_el + _PI_2); + } + // find the sector we are in currently + sector = floor(angle_el / _PI_3) + 1; + // calculate the duty cycles + float T1 = _SQRT3*_sin(sector*_PI_3 - angle_el) * Uout; + float T2 = _SQRT3*_sin(angle_el - (sector-1.0f)*_PI_3) * Uout; + // two versions possible + float T0 = 0; // pulled to 0 - better for low power supply voltage + if (modulation_centered) { + T0 = 1 - T1 - T2; // modulation_centered around driver->voltage_limit/2 + } + + // calculate the duty cycles(times) + float Ta,Tb,Tc; + switch(sector){ + case 1: + Ta = T1 + T2 + T0/2; + Tb = T2 + T0/2; + Tc = T0/2; + break; + case 2: + Ta = T1 + T0/2; + Tb = T1 + T2 + T0/2; + Tc = T0/2; + break; + case 3: + Ta = T0/2; + Tb = T1 + T2 + T0/2; + Tc = T2 + T0/2; + break; + case 4: + Ta = T0/2; + Tb = T1+ T0/2; + Tc = T1 + T2 + T0/2; + break; + case 5: + Ta = T2 + T0/2; + Tb = T0/2; + Tc = T1 + T2 + T0/2; + break; + case 6: + Ta = T1 + T2 + T0/2; + Tb = T0/2; + Tc = T1 + T0/2; + break; + default: + // possible error state + Ta = 0; + Tb = 0; + Tc = 0; + } + + // calculate the phase voltages and center + Ua = Ta*driver->voltage_limit; + Ub = Tb*driver->voltage_limit; + Uc = Tc*driver->voltage_limit; + break; + + } + + // set the voltages in driver + driver->setPwm(Ua, Ub, Uc); +} + + + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float BLDCMotor::velocityOpenloop(float target_velocity){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float BLDCMotor::angleOpenloop(float target_angle){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + // TODO sensor precision: this calculation is not numerically precise. The angle can grow to the point + // where small position changes are no longer captured by the precision of floats + // when the total position is large. + if(abs( target_angle - shaft_angle ) > abs(velocity_limit*Ts)){ + shaft_angle += _sign(target_angle - shaft_angle) * abs( velocity_limit )*Ts; + shaft_velocity = velocity_limit; + }else{ + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + // sensor precision: this calculation is OK due to the normalisation + setPhaseVoltage(Uq, 0, _electricalAngle(_normalizeAngle(shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.h new file mode 100644 index 0000000..a1f196a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/BLDCMotor.h @@ -0,0 +1,115 @@ +#ifndef BLDCMotor_h +#define BLDCMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/Sensor.h" +#include "common/base_classes/BLDCDriver.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + BLDC motor class +*/ +class BLDCMotor: public FOCMotor +{ + public: + /** + BLDCMotor class constructor + @param pp pole pairs number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + BLDCMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver BLDCDriver class implementing all the hardware specific functions necessary PWM setting + */ + virtual void linkDriver(BLDCDriver* driver); + + /** + * BLDCDriver link: + * - 3PWM + * - 6PWM + */ + BLDCDriver* driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + */ + int initFOC() override; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + + /** + * Function executing the control loops set by the controller parameter of the BLDCMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ua, Ub, Uc;//!< Current phase voltages Ua,Ub and Uc set to motor + float Ualpha, Ubeta; //!< Phase voltages U alpha and U beta used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + + private: + // FOC methods + + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Current sense and motor phase alignment */ + int alignCurrentSense(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/SimpleFOC.h b/.pio/libdeps/aioli-foc/Simple FOC/src/SimpleFOC.h new file mode 100644 index 0000000..4e6815e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/SimpleFOC.h @@ -0,0 +1,119 @@ +/*! + * @file SimpleFOC.h + * + * @mainpage Simple Field Oriented Control BLDC motor control library + * + * @section intro_sec Introduction + * + * Proper low-cost and low-power FOC supporting boards are very hard to find these days and even may not exist.
Even harder to find is a stable and simple FOC algorithm code capable of running on Arduino devices. Therefore this is an attempt to: + * - Demystify FOC algorithm and make a robust but simple Arduino library: Arduino SimpleFOC library + * - Develop a modular BLDC driver board: Arduino SimpleFOC shield. + * + * @section features Features + * - Arduino compatible: Arduino library code + * - Easy to setup and configure: + * - Easy hardware configuration + * - Easy tuning the control loops + * - Modular: + * - Supports as many sensors , BLDC motors and driver boards as possible + * - Supports as many application requirements as possible + * - Plug & play: Arduino SimpleFOC shield + * + * @section dependencies Supported Hardware + * - Motors + * - BLDC motors + * - Stepper motors + * - Drivers + * - BLDC drivers + * - Gimbal drivers + * - Stepper drivers + * - Position sensors + * - Encoders + * - Magnetic sensors + * - Hall sensors + * - Open-loop control + * - Microcontrollers + * - Arduino + * - STM32 + * - ESP32 + * - Teensy + * + * @section example_code Example code + * @code +#include + +// BLDCMotor( pole_pairs ) +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver( pin_pwmA, pin_pwmB, pin_pwmC, enable (optional) ) +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +// Encoder(pin_A, pin_B, CPR) +Encoder encoder = Encoder(2, 3, 2048); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +void setup() { + // initialize encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage [V] + driver.voltage_power_supply = 12; + // initialise driver hardware + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::velocity; + // initialize motor + motor.init(); + + // align encoder and start FOC + motor.initFOC(); +} + +void loop() { + // FOC algorithm function + motor.loopFOC(); + + // velocity control loop function + // setting the target velocity or 2rad/s + motor.move(2); +} + * @endcode + * + * @section license License + * + * MIT license, all text here must be included in any redistribution. + * + */ + +#ifndef SIMPLEFOC_H +#define SIMPLEFOC_H + +#include "BLDCMotor.h" +#include "StepperMotor.h" +#include "sensors/Encoder.h" +#include "sensors/MagneticSensorSPI.h" +#include "sensors/MagneticSensorI2C.h" +#include "sensors/MagneticSensorAnalog.h" +#include "sensors/MagneticSensorPWM.h" +#include "sensors/HallSensor.h" +#include "sensors/GenericSensor.h" +#include "drivers/BLDCDriver3PWM.h" +#include "drivers/BLDCDriver6PWM.h" +#include "drivers/StepperDriver4PWM.h" +#include "drivers/StepperDriver2PWM.h" +#include "current_sense/InlineCurrentSense.h" +#include "current_sense/LowsideCurrentSense.h" +#include "current_sense/GenericCurrentSense.h" +#include "communication/Commander.h" +#include "communication/StepDirListener.h" +#include "communication/SimpleFOCDebug.h" + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.cpp new file mode 100644 index 0000000..491a20e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.cpp @@ -0,0 +1,443 @@ +#include "StepperMotor.h" +#include "./communication/SimpleFOCDebug.h" + + +// StepperMotor(int pp) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance [H] +StepperMotor::StepperMotor(int pp, float _R, float _KV, float _inductance) +: FOCMotor() +{ + // number od pole pairs + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/K_bemf + // usually used rms + KV_rating = _KV*_SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + // current and foc_current not supported yet + torque_controller = TorqueControlType::voltage; +} + +/** + Link the driver which controls the motor +*/ +void StepperMotor::linkDriver(StepperDriver* _driver) { + driver = _driver; +} + +// init hardware pins +void StepperMotor::init() { + if (!driver || !driver->initialized) { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if(voltage_limit > driver->voltage_limit) voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if(voltage_sensor_align > voltage_limit) voltage_sensor_align = voltage_limit; + + // update the controller limits + if(_isset(phase_resistance)){ + // velocity control loop controls current + PID_velocity.limit = current_limit; + }else{ + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + // if using open loop control, set a CW as the default direction if not already set + if ((controller==MotionControlType::angle_openloop + ||controller==MotionControlType::velocity_openloop) + && (sensor_direction == Direction::UNKNOWN)) { + sensor_direction = Direction::CW; + } + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + + motor_status = FOCMotorStatus::motor_uncalibrated; +} + + +// disable motor driver +void StepperMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0); + // disable driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void StepperMotor::enable() +{ + // disable enable + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0); + // motor status update + enabled = 1; +} + + +/** + FOC functions +*/ +// FOC initialization function +int StepperMotor::initFOC() { + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // align motor if necessary + // alignment necessary for encoders! + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if(sensor){ + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = sensor->getAngle(); + } else { SIMPLEFOC_DEBUG("MOT: No sensor."); } + + if(exit_flag){ + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + }else{ + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Encoder alignment to electrical 0 angle +int StepperMotor::alignSensor() { + int exit_flag = 1; //success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // if unknown natural direction + if(!_isset(sensor_direction)){ + // check if sensor needs zero search + if(sensor->needsSearch()) exit_flag = absoluteZeroSearch(); + // stop init if not found index + if(!exit_flag) return exit_flag; + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + if (mid_angle == end_angle) { + SIMPLEFOC_DEBUG("MOT: Failed to notice movement"); + return 0; // failed calibration + } else if (mid_angle < end_angle) { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CCW"); + sensor_direction = Direction::CCW; + } else{ + SIMPLEFOC_DEBUG("MOT: sensor_direction==CW"); + sensor_direction = Direction::CW; + } + // check pole pair number + float moved = fabs(mid_angle - end_angle); + if( fabs(moved*pole_pairs - _2PI) > 0.5f ) { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI/moved); + } else { + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + + } else { + SIMPLEFOC_DEBUG("MOT: Skip dir calib."); + } + + // zero electric angle not known + if(!_isset(zero_electric_angle)){ + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + _delay(20); + if(monitor_port){ + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } else { SIMPLEFOC_DEBUG("MOT: Skip offset calib."); } + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int StepperMotor::absoluteZeroSearch() { + + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while(sensor->needsSearch() && shaft_angle < _2PI){ + angleOpenloop(1.5f*_2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if(monitor_port){ + if(sensor->needsSearch()) SIMPLEFOC_DEBUG("MOT: Error: Not found!"); + else { SIMPLEFOC_DEBUG("MOT: Success!"); } + } + return !sensor->needsSearch(); +} + + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void StepperMotor::loopFOC() { + + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) sensor->update(); + + // if open-loop do nothing + if( controller==MotionControlType::angle_openloop || controller==MotionControlType::velocity_openloop ) return; + // shaft angle + shaft_angle = shaftAngle(); + + // if disabled do nothing + if(!enabled) return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or voltage loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void StepperMotor::move(float new_target) { + + // downsampling (optional) + if(motion_cnt++ < motion_downsample) return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if( controller!=MotionControlType::angle_openloop && controller!=MotionControlType::velocity_openloop ) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if(!enabled) return; + + // set internal target variable + if(_isset(new_target) ) target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) voltage_bemf = shaft_velocity/KV_rating/_RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if(!current_sense && _isset(phase_resistance)) current.q = (voltage.q - voltage_bemf)/phase_resistance; + + // choose control loop + switch (controller) { + case MotionControlType::torque: + if(!_isset(phase_resistance)) voltage.q = target; // if voltage torque control + else voltage.q = target*phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -target*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::angle: + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = feed_forward_velocity + P_angle( shaft_angle_sp - shaft_angle ); + shaft_angle_sp = _constrain(shaft_angle_sp, -velocity_limit, velocity_limit); + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity: + // velocity set point + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + case MotionControlType::angle_openloop: + // angle control in open loop + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + } +} + + +// Method using FOC to set Uq and Ud to the motor at the optimal angle +// Function implementing Sine PWM algorithms +// - space vector not implemented yet +// +// Function using sine approximation +// regular sin + cos ~300us (no memory usaage) +// approx _sin + _cos ~110us (400Byte ~ 20% of memory) +void StepperMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) { + // Sinusoidal PWM modulation + // Inverse Park transformation + float _sa, _ca; + _sincos(angle_el, &_sa, &_ca); + + // Inverse park transform + Ualpha = _ca * Ud - _sa * Uq; // -sin(angle) * Uq; + Ubeta = _sa * Ud + _ca * Uq; // cos(angle) * Uq; + + // set the voltages in hardware + driver->setPwm(Ualpha, Ubeta); +} + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float StepperMotor::velocityOpenloop(float target_velocity){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float StepperMotor::angleOpenloop(float target_angle){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + if(abs( target_angle - shaft_angle ) > abs(velocity_limit*Ts)){ + shaft_angle += _sign(target_angle - shaft_angle) * abs( velocity_limit )*Ts; + shaft_velocity = velocity_limit; + }else{ + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle((shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.h new file mode 100644 index 0000000..7eda316 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/StepperMotor.h @@ -0,0 +1,115 @@ +/** + * @file StepperMotor.h + * + */ + +#ifndef StepperMotor_h +#define StepperMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/StepperDriver.h" +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + Stepper Motor class +*/ +class StepperMotor: public FOCMotor +{ + public: + /** + StepperMotor class constructor + @param pp pole pair number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + StepperMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver StepperDriver class implementing all the hardware specific functions necessary PWM setting + */ + void linkDriver(StepperDriver* driver); + + /** + * StepperDriver link: + * - 4PWM - L298N for example + */ + StepperDriver* driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + * + * - If zero_electric_offset parameter is set the alignment procedure is skipped + */ + int initFOC() override; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + /** + * Function executing the control loops set by the controller parameter of the StepperMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ualpha,Ubeta; //!< Phase voltages U alpha and U beta used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + + private: + + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/BLDCDriver.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/BLDCDriver.h new file mode 100644 index 0000000..f405d78 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/BLDCDriver.h @@ -0,0 +1,56 @@ +#ifndef BLDCDRIVER_H +#define BLDCDRIVER_H + +#include "Arduino.h" + + +enum PhaseState : uint8_t { + PHASE_OFF = 0, // both sides of the phase are off + PHASE_ON = 1, // both sides of the phase are driven with PWM, dead time is applied in 6-PWM mode + PHASE_HI = 2, // only the high side of the phase is driven with PWM (6-PWM mode only) + PHASE_LO = 3, // only the low side of the phase is driven with PWM (6-PWM mode only) +}; + + +class BLDCDriver{ + public: + + /** Initialise hardware */ + virtual int init() = 0; + /** Enable hardware */ + virtual void enable() = 0; + /** Disable hardware */ + virtual void disable() = 0; + + long pwm_frequency; //!< pwm frequency value in hertz + float voltage_power_supply; //!< power supply voltage + float voltage_limit; //!< limiting voltage set to the motor + + + float dc_a; //!< currently set duty cycle on phaseA + float dc_b; //!< currently set duty cycle on phaseB + float dc_c; //!< currently set duty cycle on phaseC + + bool initialized = false; // true if driver was successfully initialized + void* params = 0; // pointer to hardware specific parameters of driver + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + virtual void setPwm(float Ua, float Ub, float Uc) = 0; + + /** + * Set phase state, enable/disable + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) = 0; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.cpp new file mode 100644 index 0000000..609162c --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.cpp @@ -0,0 +1,99 @@ +#include "CurrentSense.h" + + +// get current magnitude +// - absolute - if no electrical_angle provided +// - signed - if angle provided +float CurrentSense::getDCCurrent(float motor_electrical_angle){ + // read current phase currents + PhaseCurrent_s current = getPhaseCurrents(); + // currnet sign - if motor angle not provided the magnitude is always positive + float sign = 1; + + // calculate clarke transform + float i_alpha, i_beta; + if(!current.c){ + // if only two measured currents + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * current.b; + }if(!current.a){ + // if only two measured currents + float a = -current.c - current.b; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * current.b; + }if(!current.b){ + // if only two measured currents + float b = -current.a - current.c; + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * b; + }else{ + // signal filtering using identity a + b + c = 0. Assumes measurement error is normally distributed. + float mid = (1.f/3) * (current.a + current.b + current.c); + float a = current.a - mid; + float b = current.b - mid; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * b; + } + + // if motor angle provided function returns signed value of the current + // determine the sign of the current + // sign(atan2(current.q, current.d)) is the same as c.q > 0 ? 1 : -1 + if(motor_electrical_angle) { + float ct; + float st; + _sincos(motor_electrical_angle, &st, &ct); + sign = (i_beta*ct - i_alpha*st) > 0 ? 1 : -1; + } + // return current magnitude + return sign*_sqrt(i_alpha*i_alpha + i_beta*i_beta); +} + +// function used with the foc algorihtm +// calculating DQ currents from phase currents +// - function calculating park and clarke transform of the phase currents +// - using getPhaseCurrents internally +DQCurrent_s CurrentSense::getFOCCurrents(float angle_el){ + // read current phase currents + PhaseCurrent_s current = getPhaseCurrents(); + + // calculate clarke transform + float i_alpha, i_beta; + if(!current.c){ + // if only two measured currents + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * current.b; + }if(!current.a){ + // if only two measured currents + float a = -current.c - current.b; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * current.b; + }if(!current.b){ + // if only two measured currents + float b = -current.a - current.c; + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * b; + } else { + // signal filtering using identity a + b + c = 0. Assumes measurement error is normally distributed. + float mid = (1.f/3) * (current.a + current.b + current.c); + float a = current.a - mid; + float b = current.b - mid; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * b; + } + + // calculate park transform + float ct; + float st; + _sincos(angle_el, &st, &ct); + DQCurrent_s return_current; + return_current.d = i_alpha * ct + i_beta * st; + return_current.q = i_beta * ct - i_alpha * st; + return return_current; +} + +/** + Driver linking to the current sense +*/ +void CurrentSense::linkDriver(BLDCDriver* _driver) { + driver = _driver; +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.h new file mode 100644 index 0000000..ad9f926 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/CurrentSense.h @@ -0,0 +1,75 @@ +#ifndef CURRENTSENSE_H +#define CURRENTSENSE_H + +#include "BLDCDriver.h" +#include "../foc_utils.h" + +/** + * Current sensing abstract class defintion + * Each current sensoring implementation needs to extend this interface + */ +class CurrentSense{ + public: + + /** + * Function intialising the CurrentSense class + * - All the necessary intialisations of adc and sync should be implemented here + * + * @returns - 0 - for failure & 1 - for success + */ + virtual int init() = 0; + + /** + * Linking the current sense with the motor driver + * Only necessary if synchronisation in between the two is required + */ + void linkDriver(BLDCDriver *driver); + + // variables + bool skip_align = false; //!< variable signaling that the phase current direction should be verified during initFOC() + + BLDCDriver* driver = nullptr; //!< driver link + bool initialized = false; // true if current sense was successfully initialized + void* params = 0; //!< pointer to hardware specific parameters of current sensing + + /** + * Function intended to verify if: + * - phase current are oriented properly + * - if their order is the same as driver phases + * + * This function corrects the alignment errors if possible ans if no such thing is needed it can be left empty (return 1) + * @returns - 0 - for failure & positive number (with status) - for success + */ + virtual int driverAlign(float align_voltage) = 0; + + /** + * Function rading the phase currents a, b and c + * This function will be used with the foc control throught the function + * CurrentSense::getFOCCurrents(electrical_angle) + * - it returns current c equal to 0 if only two phase measurements available + * + * @return PhaseCurrent_s current values + */ + virtual PhaseCurrent_s getPhaseCurrents() = 0; + /** + * Function reading the magnitude of the current set to the motor + * It returns the abosolute or signed magnitude if possible + * It can receive the motor electrical angle to help with calculation + * This function is used with the current control (not foc) + * + * @param angle_el - electrical angle of the motor (optional) + */ + virtual float getDCCurrent(float angle_el = 0); + + /** + * Function used for FOC contorl, it reads the DQ currents of the motor + * It uses the function getPhaseCurrents internally + * + * @param angle_el - motor electrical angle + */ + DQCurrent_s getFOCCurrents(float angle_el); + + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.cpp new file mode 100644 index 0000000..d1427bc --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.cpp @@ -0,0 +1,155 @@ +#include "FOCMotor.h" +#include "../../communication/SimpleFOCDebug.h" + +/** + * Default constructor - setting all variabels to default values + */ +FOCMotor::FOCMotor() +{ + // maximum angular velocity to be used for positioning + velocity_limit = DEF_VEL_LIM; + // maximum voltage to be set to the motor + voltage_limit = DEF_POWER_SUPPLY; + // not set on the begining + current_limit = DEF_CURRENT_LIM; + + // index search velocity + velocity_index_search = DEF_INDEX_SEARCH_TARGET_VELOCITY; + // sensor and motor align voltage + voltage_sensor_align = DEF_VOLTAGE_SENSOR_ALIGN; + + // default modulation is SinePWM + foc_modulation = FOCModulationType::SinePWM; + + // default target value + target = 0; + voltage.d = 0; + voltage.q = 0; + // current target values + current_sp = 0; + current.q = 0; + current.d = 0; + + // voltage bemf + voltage_bemf = 0; + + //monitor_port + monitor_port = nullptr; + //sensor + sensor_offset = 0.0f; + sensor = nullptr; + //current sensor + current_sense = nullptr; +} + + +/** + Sensor linking method +*/ +void FOCMotor::linkSensor(Sensor* _sensor) { + sensor = _sensor; +} + +/** + CurrentSense linking method +*/ +void FOCMotor::linkCurrentSense(CurrentSense* _current_sense) { + current_sense = _current_sense; +} + +// shaft angle calculation +float FOCMotor::shaftAngle() { + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return shaft_angle; + return sensor_direction*LPF_angle(sensor->getAngle()) - sensor_offset; +} +// shaft velocity calculation +float FOCMotor::shaftVelocity() { + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return shaft_velocity; + return sensor_direction*LPF_velocity(sensor->getVelocity()); +} + +float FOCMotor::electricalAngle(){ + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return electrical_angle; + return _normalizeAngle( (float)(sensor_direction * pole_pairs) * sensor->getMechanicalAngle() - zero_electric_angle ); +} + +/** + * Monitoring functions + */ +// function implementing the monitor_port setter +void FOCMotor::useMonitoring(Print &print){ + monitor_port = &print; //operate on the address of print + #ifndef SIMPLEFOC_DISABLE_DEBUG + SimpleFOCDebug::enable(&print); + SIMPLEFOC_DEBUG("MOT: Monitor enabled!"); + #endif +} + +// utility function intended to be used with serial plotter to monitor motor variables +// significantly slowing the execution down!!!! +void FOCMotor::monitor() { + if( !monitor_downsample || monitor_cnt++ < monitor_downsample ) return; + monitor_cnt = 0; + if(!monitor_port) return; + bool printed = 0; + + if(monitor_variables & _MON_TARGET){ + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + monitor_port->print(target,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_VOLT_Q) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(voltage.q,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_VOLT_D) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(voltage.d,monitor_decimals); + printed= true; + } + // read currents if possible - even in voltage mode (if current_sense available) + if(monitor_variables & _MON_CURR_Q || monitor_variables & _MON_CURR_D) { + DQCurrent_s c = current; + if( current_sense && torque_controller != TorqueControlType::foc_current ){ + c = current_sense->getFOCCurrents(electrical_angle); + c.q = LPF_current_q(c.q); + c.d = LPF_current_d(c.d); + } + if(monitor_variables & _MON_CURR_Q) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(c.q*1000, monitor_decimals); // mAmps + printed= true; + } + if(monitor_variables & _MON_CURR_D) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(c.d*1000, monitor_decimals); // mAmps + printed= true; + } + } + + if(monitor_variables & _MON_VEL) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(shaft_velocity,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_ANGLE) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(shaft_angle,monitor_decimals); + printed= true; + } + if(printed){ + if(monitor_end_char) monitor_port->println(monitor_end_char); + else monitor_port->println(""); + } +} + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.h new file mode 100644 index 0000000..318be99 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/FOCMotor.h @@ -0,0 +1,251 @@ +#ifndef FOCMOTOR_H +#define FOCMOTOR_H + +#include "Arduino.h" +#include "Sensor.h" +#include "CurrentSense.h" + +#include "../time_utils.h" +#include "../foc_utils.h" +#include "../defaults.h" +#include "../pid.h" +#include "../lowpass_filter.h" + + +// monitoring bitmap +#define _MON_TARGET 0b1000000 // monitor target value +#define _MON_VOLT_Q 0b0100000 // monitor voltage q value +#define _MON_VOLT_D 0b0010000 // monitor voltage d value +#define _MON_CURR_Q 0b0001000 // monitor current q value - if measured +#define _MON_CURR_D 0b0000100 // monitor current d value - if measured +#define _MON_VEL 0b0000010 // monitor velocity value +#define _MON_ANGLE 0b0000001 // monitor angle value + +/** + * Motiron control type + */ +enum MotionControlType : uint8_t { + torque = 0x00, //!< Torque control + velocity = 0x01, //!< Velocity motion control + angle = 0x02, //!< Position/angle motion control + velocity_openloop = 0x03, + angle_openloop = 0x04 +}; + +/** + * Motiron control type + */ +enum TorqueControlType : uint8_t { + voltage = 0x00, //!< Torque control using voltage + dc_current = 0x01, //!< Torque control using DC current (one current magnitude) + foc_current = 0x02, //!< torque control using dq currents +}; + +/** + * FOC modulation type + */ +enum FOCModulationType : uint8_t { + SinePWM = 0x00, //!< Sinusoidal PWM modulation + SpaceVectorPWM = 0x01, //!< Space vector modulation method + Trapezoid_120 = 0x02, + Trapezoid_150 = 0x03, +}; + + + +enum FOCMotorStatus : uint8_t { + motor_uninitialized = 0x00, //!< Motor is not yet initialized + motor_initializing = 0x01, //!< Motor intiialization is in progress + motor_uncalibrated = 0x02, //!< Motor is initialized, but not calibrated (open loop possible) + motor_calibrating = 0x03, //!< Motor calibration in progress + motor_ready = 0x04, //!< Motor is initialized and calibrated (closed loop possible) + motor_error = 0x08, //!< Motor is in error state (recoverable, e.g. overcurrent protection active) + motor_calib_failed = 0x0E, //!< Motor calibration failed (possibly recoverable) + motor_init_failed = 0x0F, //!< Motor initialization failed (not recoverable) +}; + + + +/** + Generic motor class +*/ +class FOCMotor +{ + public: + /** + * Default constructor - setting all variabels to default values + */ + FOCMotor(); + + /** Motor hardware init function */ + virtual void init()=0; + /** Motor disable function */ + virtual void disable()=0; + /** Motor enable function */ + virtual void enable()=0; + + /** + * Function linking a motor and a sensor + * + * @param sensor Sensor class wrapper for the FOC algorihtm to read the motor angle and velocity + */ + void linkSensor(Sensor* sensor); + + /** + * Function linking a motor and current sensing + * + * @param current_sense CurrentSense class wrapper for the FOC algorihtm to read the motor current measurements + */ + void linkCurrentSense(CurrentSense* current_sense); + + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + * + * - If zero_electric_offset parameter is set the alignment procedure is skipped + */ + virtual int initFOC()=0; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + virtual void loopFOC()=0; + /** + * Function executing the control loops set by the controller parameter of the BLDCMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + virtual void move(float target = NOT_SET)=0; + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + virtual void setPhaseVoltage(float Uq, float Ud, float angle_el)=0; + + // State calculation methods + /** Shaft angle calculation in radians [rad] */ + float shaftAngle(); + /** + * Shaft angle calculation function in radian per second [rad/s] + * It implements low pass filtering + */ + float shaftVelocity(); + + + + /** + * Electrical angle calculation + */ + float electricalAngle(); + + // state variables + float target; //!< current target value - depends of the controller + float feed_forward_velocity = 0.0f; //!< current feed forward velocity + float shaft_angle;//!< current motor angle + float electrical_angle;//!< current electrical angle + float shaft_velocity;//!< current motor velocity + float current_sp;//!< target current ( q current ) + float shaft_velocity_sp;//!< current target velocity + float shaft_angle_sp;//!< current target angle + DQVoltage_s voltage;//!< current d and q voltage set to the motor + DQCurrent_s current;//!< current d and q current measured + float voltage_bemf; //!< estimated backemf voltage (if provided KV constant) + + // motor configuration parameters + float voltage_sensor_align;//!< sensor and motor align voltage parameter + float velocity_index_search;//!< target velocity for index search + + // motor physical parameters + float phase_resistance; //!< motor phase resistance + int pole_pairs;//!< motor pole pairs number + float KV_rating; //!< motor KV rating + float phase_inductance; //!< motor phase inductance + + // limiting variables + float voltage_limit; //!< Voltage limiting variable - global limit + float current_limit; //!< Current limiting variable - global limit + float velocity_limit; //!< Velocity limiting variable - global limit + + // motor status vairables + int8_t enabled = 0;//!< enabled or disabled motor flag + FOCMotorStatus motor_status = FOCMotorStatus::motor_uninitialized; //!< motor status + + // pwm modulation related variables + FOCModulationType foc_modulation;//!< parameter determining modulation algorithm + int8_t modulation_centered = 1;//!< flag (1) centered modulation around driver limit /2 or (0) pulled to 0 + + + // configuration structures + TorqueControlType torque_controller; //!< parameter determining the torque control type + MotionControlType controller; //!< parameter determining the control loop to be used + + // controllers and low pass filters + PIDController PID_current_q{DEF_PID_CURR_P,DEF_PID_CURR_I,DEF_PID_CURR_D,DEF_PID_CURR_RAMP, DEF_POWER_SUPPLY};//!< parameter determining the q current PID config + PIDController PID_current_d{DEF_PID_CURR_P,DEF_PID_CURR_I,DEF_PID_CURR_D,DEF_PID_CURR_RAMP, DEF_POWER_SUPPLY};//!< parameter determining the d current PID config + LowPassFilter LPF_current_q{DEF_CURR_FILTER_Tf};//!< parameter determining the current Low pass filter configuration + LowPassFilter LPF_current_d{DEF_CURR_FILTER_Tf};//!< parameter determining the current Low pass filter configuration + PIDController PID_velocity{DEF_PID_VEL_P,DEF_PID_VEL_I,DEF_PID_VEL_D,DEF_PID_VEL_RAMP,DEF_PID_VEL_LIMIT};//!< parameter determining the velocity PID configuration + PIDController P_angle{DEF_P_ANGLE_P,0,0,0,DEF_VEL_LIM}; //!< parameter determining the position PID configuration + LowPassFilter LPF_velocity{DEF_VEL_FILTER_Tf};//!< parameter determining the velocity Low pass filter configuration + LowPassFilter LPF_angle{0.0};//!< parameter determining the angle low pass filter configuration + unsigned int motion_downsample = DEF_MOTION_DOWNSMAPLE; //!< parameter defining the ratio of downsampling for move commad + unsigned int motion_cnt = 0; //!< counting variable for downsampling for move commad + + // sensor related variabels + float sensor_offset; //!< user defined sensor zero offset + float zero_electric_angle = NOT_SET;//!< absolute zero electric angle - if available + Direction sensor_direction = Direction::UNKNOWN; //!< default is CW. if sensor_direction == Direction::CCW then direction will be flipped compared to CW. Set to UNKNOWN to set by calibration + + /** + * Function providing BLDCMotor class with the + * Serial interface and enabling monitoring mode + * + * @param serial Monitoring Serial class reference + */ + void useMonitoring(Print &serial); + + /** + * Utility function intended to be used with serial plotter to monitor motor variables + * significantly slowing the execution down!!!! + */ + void monitor(); + unsigned int monitor_downsample = DEF_MON_DOWNSMAPLE; //!< show monitor outputs each monitor_downsample calls + char monitor_start_char = '\0'; //!< monitor starting character + char monitor_end_char = '\0'; //!< monitor outputs ending character + char monitor_separator = '\t'; //!< monitor outputs separation character + unsigned int monitor_decimals = 4; //!< monitor outputs decimal places + // initial monitoring will display target, voltage, velocity and angle + uint8_t monitor_variables = _MON_TARGET | _MON_VOLT_Q | _MON_VEL | _MON_ANGLE; //!< Bit array holding the map of variables the user wants to monitor + + /** + * Sensor link: + * - Encoder + * - MagneticSensor* + * - HallSensor + */ + Sensor* sensor; + /** + * CurrentSense link + */ + CurrentSense* current_sense; + + // monitoring functions + Print* monitor_port; //!< Serial terminal variable if provided + private: + // monitor counting variable + unsigned int monitor_cnt = 0 ; //!< counting variable +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.cpp new file mode 100644 index 0000000..a80fa43 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.cpp @@ -0,0 +1,78 @@ +#include "Sensor.h" +#include "../foc_utils.h" +#include "../time_utils.h" + + + +void Sensor::update() { + float val = getSensorAngle(); + angle_prev_ts = _micros(); + float d_angle = val - angle_prev; + // if overflow happened track it as full rotation + if(abs(d_angle) > (0.8f*_2PI) ) full_rotations += ( d_angle > 0 ) ? -1 : 1; + angle_prev = val; +} + + + /** get current angular velocity (rad/s) */ +float Sensor::getVelocity() { + // calculate sample time + float Ts = (angle_prev_ts - vel_angle_prev_ts)*1e-6; + if (Ts < 0.0f) { // handle micros() overflow - we need to reset vel_angle_prev_ts + vel_angle_prev = angle_prev; + vel_full_rotations = full_rotations; + vel_angle_prev_ts = angle_prev_ts; + return velocity; + } + if (Ts < min_elapsed_time) return velocity; // don't update velocity if deltaT is too small + + velocity = ( (float)(full_rotations - vel_full_rotations)*_2PI + (angle_prev - vel_angle_prev) ) / Ts; + vel_angle_prev = angle_prev; + vel_full_rotations = full_rotations; + vel_angle_prev_ts = angle_prev_ts; + return velocity; +} + + + +void Sensor::init() { + // initialize all the internal variables of Sensor to ensure a "smooth" startup (without a 'jump' from zero) + getSensorAngle(); // call once + delayMicroseconds(1); + vel_angle_prev = getSensorAngle(); // call again + vel_angle_prev_ts = _micros(); + delay(1); + getSensorAngle(); // call once + delayMicroseconds(1); + angle_prev = getSensorAngle(); // call again + angle_prev_ts = _micros(); +} + + +float Sensor::getMechanicalAngle() { + return angle_prev; +} + + + +float Sensor::getAngle(){ + return (float)full_rotations * _2PI + angle_prev; +} + + + +double Sensor::getPreciseAngle() { + return (double)full_rotations * (double)_2PI + (double)angle_prev; +} + + + +int32_t Sensor::getFullRotations() { + return full_rotations; +} + + + +int Sensor::needsSearch() { + return 0; // default false +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.h new file mode 100644 index 0000000..c77eb91 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/Sensor.h @@ -0,0 +1,139 @@ +#ifndef SENSOR_H +#define SENSOR_H + +#include + +/** + * Direction structure + */ +enum Direction : int8_t { + CW = 1, // clockwise + CCW = -1, // counter clockwise + UNKNOWN = 0 // not yet known or invalid state +}; + + +/** + * Pullup configuration structure + */ +enum Pullup : uint8_t { + USE_INTERN = 0x00, //!< Use internal pullups + USE_EXTERN = 0x01 //!< Use external pullups +}; + +/** + * Sensor abstract class defintion + * + * This class is purposefully kept simple, as a base for all kinds of sensors. Currently we have + * Encoders, Magnetic Encoders and Hall Sensor implementations. This base class extracts the + * most basic common features so that a FOC driver can obtain the data it needs for operation. + * + * To implement your own sensors, create a sub-class of this class, and implement the getSensorAngle() + * method. getSensorAngle() returns a float value, in radians, representing the current shaft angle in the + * range 0 to 2*PI (one full turn). + * + * To function correctly, the sensor class update() method has to be called sufficiently quickly. Normally, + * the BLDCMotor's loopFOC() function calls it once per iteration, so you must ensure to call loopFOC() quickly + * enough, both for correct motor and sensor operation. + * + * The Sensor base class provides an implementation of getVelocity(), and takes care of counting full + * revolutions in a precise way, but if you wish you can additionally override these methods to provide more + * optimal implementations for your hardware. + * + */ +class Sensor{ + friend class SmoothingSensor; + public: + /** + * Get mechanical shaft angle in the range 0 to 2PI. This value will be as precise as possible with + * the hardware. Base implementation uses the values returned by update() so that + * the same values are returned until update() is called again. + */ + virtual float getMechanicalAngle(); + + /** + * Get current position (in rad) including full rotations and shaft angle. + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + * Note that this value has limited precision as the number of rotations increases, + * because the limited precision of float can't capture the large angle of the full + * rotations and the small angle of the shaft angle at the same time. + */ + virtual float getAngle(); + + /** + * On architectures supporting it, this will return a double precision position value, + * which should have improved precision for large position values. + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + */ + virtual double getPreciseAngle(); + + /** + * Get current angular velocity (rad/s) + * Can be overridden in subclasses. Base implementation uses the values + * returned by update() so that it only makes sense to call this if update() + * has been called in the meantime. + */ + virtual float getVelocity(); + + /** + * Get the number of full rotations + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + */ + virtual int32_t getFullRotations(); + + /** + * Updates the sensor values by reading the hardware sensor. + * Some implementations may work with interrupts, and not need this. + * The base implementation calls getSensorAngle(), and updates internal + * fields for angle, timestamp and full rotations. + * This method must be called frequently enough to guarantee that full + * rotations are not "missed" due to infrequent polling. + * Override in subclasses if alternative behaviours are required for your + * sensor hardware. + */ + virtual void update(); + + /** + * returns 0 if it does need search for absolute zero + * 0 - magnetic sensor (& encoder with index which is found) + * 1 - ecoder with index (with index not found yet) + */ + virtual int needsSearch(); + + /** + * Minimum time between updates to velocity. If time elapsed is lower than this, the velocity is not updated. + */ + float min_elapsed_time = 0.000100; // default is 100 microseconds, or 10kHz + + protected: + /** + * Get current shaft angle from the sensor hardware, and + * return it as a float in radians, in the range 0 to 2PI. + * + * This method is pure virtual and must be implemented in subclasses. + * Calling this method directly does not update the base-class internal fields. + * Use update() when calling from outside code. + */ + virtual float getSensorAngle()=0; + /** + * Call Sensor::init() from your sensor subclass's init method if you want smoother startup + * The base class init() method calls getSensorAngle() several times to initialize the internal fields + * to current values, ensuring there is no discontinuity ("jump from zero") during the first calls + * to sensor.getAngle() and sensor.getVelocity() + */ + virtual void init(); + + // velocity calculation variables + float velocity=0.0f; + float angle_prev=0.0f; // result of last call to getSensorAngle(), used for full rotations and velocity + long angle_prev_ts=0; // timestamp of last call to getAngle, used for velocity + float vel_angle_prev=0.0f; // angle at last call to getVelocity, used for velocity + long vel_angle_prev_ts=0; // last velocity calculation timestamp + int32_t full_rotations=0; // full rotation tracking + int32_t vel_full_rotations=0; // previous full rotation value for velocity calculation +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/StepperDriver.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/StepperDriver.h new file mode 100644 index 0000000..2006fc8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/base_classes/StepperDriver.h @@ -0,0 +1,32 @@ +#ifndef STEPPERDRIVER_H +#define STEPPERDRIVER_H + +#include "drivers/hardware_api.h" + +class StepperDriver{ + public: + + /** Initialise hardware */ + virtual int init() = 0; + /** Enable hardware */ + virtual void enable() = 0; + /** Disable hardware */ + virtual void disable() = 0; + + long pwm_frequency; //!< pwm frequency value in hertz + float voltage_power_supply; //!< power supply voltage + float voltage_limit; //!< limiting voltage set to the motor + + bool initialized = false; // true if driver was successfully initialized + void* params = 0; // pointer to hardware specific parameters of driver + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + virtual void setPwm(float Ua, float Ub) = 0; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/defaults.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/defaults.h new file mode 100644 index 0000000..c0a4618 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/defaults.h @@ -0,0 +1,49 @@ +// default configuration values +// change this file to optimal values for your application + +#define DEF_POWER_SUPPLY 12.0f //!< default power supply voltage +// velocity PI controller params +#define DEF_PID_VEL_P 0.5f //!< default PID controller P value +#define DEF_PID_VEL_I 10.0f //!< default PID controller I value +#define DEF_PID_VEL_D 0.0f //!< default PID controller D value +#define DEF_PID_VEL_RAMP 1000.0f //!< default PID controller voltage ramp value +#define DEF_PID_VEL_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit + +// current sensing PID values +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) +// for 16Mhz controllers like Arduino uno and mega +#define DEF_PID_CURR_P 2 //!< default PID controller P value +#define DEF_PID_CURR_I 100 //!< default PID controller I value +#define DEF_PID_CURR_D 0.0f //!< default PID controller D value +#define DEF_PID_CURR_RAMP 1000.0f //!< default PID controller voltage ramp value +#define DEF_PID_CURR_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit +#define DEF_CURR_FILTER_Tf 0.01f //!< default velocity filter time constant +#else +// for stm32, due, teensy, esp32 and similar +#define DEF_PID_CURR_P 3 //!< default PID controller P value +#define DEF_PID_CURR_I 300.0f //!< default PID controller I value +#define DEF_PID_CURR_D 0.0f //!< default PID controller D value +#define DEF_PID_CURR_RAMP 0 //!< default PID controller voltage ramp value +#define DEF_PID_CURR_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit +#define DEF_CURR_FILTER_Tf 0.005f //!< default currnet filter time constant +#endif +// default current limit values +#define DEF_CURRENT_LIM 2.0f //!< 2Amps current limit by default + +// default monitor downsample +#define DEF_MON_DOWNSMAPLE 100 //!< default monitor downsample +#define DEF_MOTION_DOWNSMAPLE 0 //!< default motion downsample - disable + +// angle P params +#define DEF_P_ANGLE_P 20.0f //!< default P controller P value +#define DEF_VEL_LIM 20.0f //!< angle velocity limit default + +// index search +#define DEF_INDEX_SEARCH_TARGET_VELOCITY 1.0f //!< default index search velocity +// align voltage +#define DEF_VOLTAGE_SENSOR_ALIGN 3.0f //!< default voltage for sensor and motor zero alignemt +// low pass filter velocity +#define DEF_VEL_FILTER_Tf 0.005 //!< default velocity filter time constant + +// current sense default parameters +#define DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf 0.0f //!< default currnet sense per phase low pass filter time constant diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.cpp new file mode 100644 index 0000000..4cb0986 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.cpp @@ -0,0 +1,73 @@ +#include "foc_utils.h" + + +// function approximating the sine calculation by using fixed size array +// uses a 65 element lookup table and interpolation +// thanks to @dekutree for his work on optimizing this +__attribute__((weak)) float _sin(float a){ + // 16bit integer array for sine lookup. interpolation is used for better precision + // 16 bit precision on sine value, 8 bit fractional value for interpolation, 6bit LUT size + // resulting precision compared to stdlib sine is 0.00006480 (RMS difference in range -PI,PI for 3217 steps) + static uint16_t sine_array[65] = {0,804,1608,2411,3212,4011,4808,5602,6393,7180,7962,8740,9512,10279,11039,11793,12540,13279,14010,14733,15447,16151,16846,17531,18205,18868,19520,20160,20788,21403,22006,22595,23170,23732,24279,24812,25330,25833,26320,26791,27246,27684,28106,28511,28899,29269,29622,29957,30274,30572,30853,31114,31357,31581,31786,31972,32138,32286,32413,32522,32610,32679,32729,32758,32768}; + unsigned int i = (unsigned int)(a * (64*4*256.0 /_2PI)); + int t1, t2, frac = i & 0xff; + i = (i >> 8) & 0xff; + if (i < 64) { + t1 = sine_array[i]; t2 = sine_array[i+1]; + } + else if(i < 128) { + t1 = sine_array[128 - i]; t2 = sine_array[127 - i]; + } + else if(i < 192) { + t1 = -sine_array[-128 + i]; t2 = -sine_array[-127 + i]; + } + else { + t1 = -sine_array[256 - i]; t2 = -sine_array[255 - i]; + } + return (1.0f/32768.0f) * (t1 + (((t2 - t1) * frac) >> 8)); +} + +// function approximating cosine calculation by using fixed size array +// ~55us (float array) +// ~56us (int array) +// precision +-0.005 +// it has to receive an angle in between 0 and 2PI +__attribute__((weak)) float _cos(float a){ + float a_sin = a + _PI_2; + a_sin = a_sin > _2PI ? a_sin - _2PI : a_sin; + return _sin(a_sin); +} + + +__attribute__((weak)) void _sincos(float a, float* s, float* c){ + *s = _sin(a); + *c = _cos(a); +} + + +// normalizing radian angle to [0,2PI] +__attribute__((weak)) float _normalizeAngle(float angle){ + float a = fmod(angle, _2PI); + return a >= 0 ? a : (a + _2PI); +} + +// Electrical angle calculation +float _electricalAngle(float shaft_angle, int pole_pairs) { + return (shaft_angle * pole_pairs); +} + +// square root approximation function using +// https://reprap.org/forum/read.php?147,219210 +// https://en.wikipedia.org/wiki/Fast_inverse_square_root +__attribute__((weak)) float _sqrtApprox(float number) {//low in fat + // float x; + // const float f = 1.5F; // better precision + + // x = number * 0.5F; + float y = number; + long i = * ( long * ) &y; + i = 0x5f375a86 - ( i >> 1 ); + y = * ( float * ) &i; + // y = y * ( f - ( x * y * y ) ); // better precision + return number * y; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.h new file mode 100644 index 0000000..0efe3b5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/foc_utils.h @@ -0,0 +1,106 @@ +#ifndef FOCUTILS_LIB_H +#define FOCUTILS_LIB_H + +#include "Arduino.h" + +// sign function +#define _sign(a) ( ( (a) < 0 ) ? -1 : ( (a) > 0 ) ) +#ifndef _round +#define _round(x) ((x)>=0?(long)((x)+0.5f):(long)((x)-0.5f)) +#endif +#define _constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt))) +#define _sqrt(a) (_sqrtApprox(a)) +#define _isset(a) ( (a) != (NOT_SET) ) +#define _UNUSED(v) (void) (v) +#define _powtwo(x) (1 << (x)) + +// utility defines +#define _2_SQRT3 1.15470053838f +#define _SQRT3 1.73205080757f +#define _1_SQRT3 0.57735026919f +#define _SQRT3_2 0.86602540378f +#define _SQRT2 1.41421356237f +#define _120_D2R 2.09439510239f +#define _PI 3.14159265359f +#define _PI_2 1.57079632679f +#define _PI_3 1.0471975512f +#define _2PI 6.28318530718f +#define _3PI_2 4.71238898038f +#define _PI_6 0.52359877559f +#define _RPM_TO_RADS 0.10471975512f + +#define NOT_SET -12345.0f +#define _HIGH_IMPEDANCE 0 +#define _HIGH_Z _HIGH_IMPEDANCE +#define _ACTIVE 1 +#define _NC (NOT_SET) + +#define MIN_ANGLE_DETECT_MOVEMENT (_2PI/101.0f) + +// dq current structure +struct DQCurrent_s +{ + float d; + float q; +}; +// phase current structure +struct PhaseCurrent_s +{ + float a; + float b; + float c; +}; +// dq voltage structs +struct DQVoltage_s +{ + float d; + float q; +}; + + +/** + * Function approximating the sine calculation by using fixed size array + * - execution time ~40us (Arduino UNO) + * + * @param a angle in between 0 and 2PI + */ +float _sin(float a); +/** + * Function approximating cosine calculation by using fixed size array + * - execution time ~50us (Arduino UNO) + * + * @param a angle in between 0 and 2PI + */ +float _cos(float a); +/** + * Function returning both sine and cosine of the angle in one call. + * Internally it uses the _sin and _cos functions, but you may wish to + * provide your own implementation which is more optimized. + */ +void _sincos(float a, float* s, float* c); + + +/** + * normalizing radian angle to [0,2PI] + * @param angle - angle to be normalized + */ +float _normalizeAngle(float angle); + + +/** + * Electrical angle calculation + * + * @param shaft_angle - shaft angle of the motor + * @param pole_pairs - number of pole pairs + */ +float _electricalAngle(float shaft_angle, int pole_pairs); + +/** + * Function approximating square root function + * - using fast inverse square root + * + * @param value - number + */ +float _sqrtApprox(float value); + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.cpp new file mode 100644 index 0000000..ffb15cf --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.cpp @@ -0,0 +1,28 @@ +#include "lowpass_filter.h" + +LowPassFilter::LowPassFilter(float time_constant) + : Tf(time_constant) + , y_prev(0.0f) +{ + timestamp_prev = _micros(); +} + + +float LowPassFilter::operator() (float x) +{ + unsigned long timestamp = _micros(); + float dt = (timestamp - timestamp_prev)*1e-6f; + + if (dt < 0.0f ) dt = 1e-3f; + else if(dt > 0.3f) { + y_prev = x; + timestamp_prev = timestamp; + return x; + } + + float alpha = Tf/(Tf + dt); + float y = alpha*y_prev + (1.0f - alpha)*x; + y_prev = y; + timestamp_prev = timestamp; + return y; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.h new file mode 100644 index 0000000..1f24e80 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/lowpass_filter.h @@ -0,0 +1,28 @@ +#ifndef LOWPASS_FILTER_H +#define LOWPASS_FILTER_H + + +#include "time_utils.h" +#include "foc_utils.h" + +/** + * Low pass filter class + */ +class LowPassFilter +{ +public: + /** + * @param Tf - Low pass filter time constant + */ + LowPassFilter(float Tf); + ~LowPassFilter() = default; + + float operator() (float x); + float Tf; //!< Low pass filter time constant + +protected: + unsigned long timestamp_prev; //!< Last execution timestamp + float y_prev; //!< filtered value in previous execution step +}; + +#endif // LOWPASS_FILTER_H \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.cpp new file mode 100644 index 0000000..da1bee1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.cpp @@ -0,0 +1,64 @@ +#include "pid.h" + +PIDController::PIDController(float P, float I, float D, float ramp, float limit) + : P(P) + , I(I) + , D(D) + , output_ramp(ramp) // output derivative limit [volts/second] + , limit(limit) // output supply limit [volts] + , error_prev(0.0f) + , output_prev(0.0f) + , integral_prev(0.0f) +{ + timestamp_prev = _micros(); +} + +// PID controller function +float PIDController::operator() (float error){ + // calculate the time from the last call + unsigned long timestamp_now = _micros(); + float Ts = (timestamp_now - timestamp_prev) * 1e-6f; + // quick fix for strange cases (micros overflow) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // u(s) = (P + I/s + Ds)e(s) + // Discrete implementations + // proportional part + // u_p = P *e(k) + float proportional = P * error; + // Tustin transform of the integral part + // u_ik = u_ik_1 + I*Ts/2*(ek + ek_1) + float integral = integral_prev + I*Ts*0.5f*(error + error_prev); + // antiwindup - limit the output + integral = _constrain(integral, -limit, limit); + // Discrete derivation + // u_dk = D(ek - ek_1)/Ts + float derivative = D*(error - error_prev)/Ts; + + // sum all the components + float output = proportional + integral + derivative; + // antiwindup - limit the output variable + output = _constrain(output, -limit, limit); + + // if output ramp defined + if(output_ramp > 0){ + // limit the acceleration by ramping the output + float output_rate = (output - output_prev)/Ts; + if (output_rate > output_ramp) + output = output_prev + output_ramp*Ts; + else if (output_rate < -output_ramp) + output = output_prev - output_ramp*Ts; + } + // saving for the next pass + integral_prev = integral; + output_prev = output; + error_prev = error; + timestamp_prev = timestamp_now; + return output; +} + +void PIDController::reset(){ + integral_prev = 0.0f; + output_prev = 0.0f; + error_prev = 0.0f; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.h new file mode 100644 index 0000000..acd68d6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/pid.h @@ -0,0 +1,41 @@ +#ifndef PID_H +#define PID_H + + +#include "time_utils.h" +#include "foc_utils.h" + +/** + * PID controller class + */ +class PIDController +{ +public: + /** + * + * @param P - Proportional gain + * @param I - Integral gain + * @param D - Derivative gain + * @param ramp - Maximum speed of change of the output value + * @param limit - Maximum output value + */ + PIDController(float P, float I, float D, float ramp, float limit); + ~PIDController() = default; + + float operator() (float error); + void reset(); + + float P; //!< Proportional gain + float I; //!< Integral gain + float D; //!< Derivative gain + float output_ramp; //!< Maximum speed of change of the output value + float limit; //!< Maximum output value + +protected: + float error_prev; //!< last tracking error value + float output_prev; //!< last pid output value + float integral_prev; //!< last integral component value + unsigned long timestamp_prev; //!< Last execution timestamp +}; + +#endif // PID_H \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.cpp new file mode 100644 index 0000000..2acb47a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.cpp @@ -0,0 +1,31 @@ +#include "time_utils.h" + +// function buffering delay() +// arduino uno function doesn't work well with interrupts +void _delay(unsigned long ms){ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) + // if arduino uno and other atmega328p chips + // use while instad of delay, + // due to wrong measurement based on changed timer0 + unsigned long t = _micros() + ms*1000; + while( _micros() < t ){}; +#else + // regular micros + delay(ms); +#endif +} + + +// function buffering _micros() +// arduino function doesn't work well with interrupts +unsigned long _micros(){ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) +// if arduino uno and other atmega328p chips + //return the value based on the prescaler + if((TCCR0B & 0b00000111) == 0x01) return (micros()/32); + else return (micros()); +#else + // regular micros + return micros(); +#endif +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.h b/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.h new file mode 100644 index 0000000..143d485 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/common/time_utils.h @@ -0,0 +1,22 @@ +#ifndef TIME_UTILS_H +#define TIME_UTILS_H + +#include "foc_utils.h" + +/** + * Function implementing delay() function in milliseconds + * - blocking function + * - hardware specific + + * @param ms number of milliseconds to wait + */ +void _delay(unsigned long ms); + +/** + * Function implementing timestamp getting function in microseconds + * hardware specific + */ +unsigned long _micros(); + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.cpp new file mode 100644 index 0000000..ee6be9f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.cpp @@ -0,0 +1,685 @@ +#include "Commander.h" + +Commander::Commander(Stream& serial, char eol, bool echo){ + com_port = &serial; + this->eol = eol; + this->echo = echo; +} +Commander::Commander(char eol, bool echo){ + this->eol = eol; + this->echo = echo; +} + + +void Commander::add(char id, CommandCallback onCommand, char* label ){ + call_list[call_count] = onCommand; + call_ids[call_count] = id; + call_label[call_count] = label; + call_count++; +} + + +void Commander::run(){ + if(!com_port) return; + run(*com_port, eol); +} + +void Commander::run(Stream& serial, char eol){ + Stream* tmp = com_port; // save the serial instance + char eol_tmp = this->eol; + this->eol = eol; + com_port = &serial; + + // a string to hold incoming data + while (serial.available()) { + // get the new byte: + int ch = serial.read(); + received_chars[rec_cnt++] = (char)ch; + // end of user input + if(echo) + print((char)ch); + if (isSentinel(ch)) { + // execute the user command + run(received_chars); + + // reset the command buffer + received_chars[0] = 0; + rec_cnt=0; + } + if (rec_cnt>=MAX_COMMAND_LENGTH) { // prevent buffer overrun if message is too long + received_chars[0] = 0; + rec_cnt=0; + } + } + + com_port = tmp; // reset the instance to the internal value + this->eol = eol_tmp; +} + +void Commander::run(char* user_input){ + // execute the user command + char id = user_input[0]; + switch(id){ + case CMD_SCAN: + for(int i=0; i < call_count; i++){ + printMachineReadable(CMD_SCAN); + print(call_ids[i]); + print(":"); + if(call_label[i]) println(call_label[i]); + else println(""); + } + break; + case CMD_VERBOSE: + if(!isSentinel(user_input[1])) verbose = (VerboseMode)atoi(&user_input[1]); + printVerbose(F("Verb:")); + printMachineReadable(CMD_VERBOSE); + switch (verbose){ + case VerboseMode::nothing: + println(F("off!")); + break; + case VerboseMode::on_request: + case VerboseMode::user_friendly: + println(F("on!")); + break; + case VerboseMode::machine_readable: + printlnMachineReadable(F("machine")); + break; + } + break; + case CMD_DECIMAL: + if(!isSentinel(user_input[1])) decimal_places = atoi(&user_input[1]); + printVerbose(F("Decimal:")); + printMachineReadable(CMD_DECIMAL); + println(decimal_places); + break; + default: + for(int i=0; i < call_count; i++){ + if(id == call_ids[i]){ + printMachineReadable(user_input[0]); + call_list[i](&user_input[1]); + break; + } + } + break; + } +} + +void Commander::motor(FOCMotor* motor, char* user_command) { + + // if target setting + if(isDigit(user_command[0]) || user_command[0] == '-' || user_command[0] == '+' || isSentinel(user_command[0])){ + target(motor, user_command); + return; + } + + // parse command letter + char cmd = user_command[0]; + char sub_cmd = user_command[1]; + // check if there is a subcommand or not + int value_index = (sub_cmd >= 'A' && sub_cmd <= 'Z') || (sub_cmd == '#') ? 2 : 1; + // check if get command + bool GET = isSentinel(user_command[value_index]); + // parse command values + float value = atof(&user_command[value_index]); + printMachineReadable(cmd); + if (sub_cmd >= 'A' && sub_cmd <= 'Z') { + printMachineReadable(sub_cmd); + } + + // a bit of optimisation of variable memory for Arduino UNO (atmega328) + switch(cmd){ + case CMD_C_Q_PID: // + printVerbose(F("PID curr q| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_current_q, &user_command[1]); + else pid(&motor->PID_current_q,&user_command[1]); + break; + case CMD_C_D_PID: // + printVerbose(F("PID curr d| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_current_d, &user_command[1]); + else pid(&motor->PID_current_d, &user_command[1]); + break; + case CMD_V_PID: // + printVerbose(F("PID vel| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_velocity, &user_command[1]); + else pid(&motor->PID_velocity, &user_command[1]); + break; + case CMD_A_PID: // + printVerbose(F("PID angle| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_angle, &user_command[1]); + else pid(&motor->P_angle, &user_command[1]); + break; + case CMD_LIMITS: // + printVerbose(F("Limits| ")); + switch (sub_cmd){ + case SCMD_LIM_VOLT: // voltage limit change + printVerbose(F("volt: ")); + if(!GET) { + motor->voltage_limit = value; + motor->PID_current_d.limit = value; + motor->PID_current_q.limit = value; + // change velocity pid limit if in voltage mode and no phase resistance set + if( !_isset(motor->phase_resistance) && motor->torque_controller==TorqueControlType::voltage) motor->PID_velocity.limit = value; + } + println(motor->voltage_limit); + break; + case SCMD_LIM_CURR: // current limit + printVerbose(F("curr: ")); + if(!GET){ + motor->current_limit = value; + // if phase resistance specified or the current control is on set the current limit to the velocity PID + if(_isset(motor->phase_resistance) || motor->torque_controller != TorqueControlType::voltage ) motor->PID_velocity.limit = value; + } + println(motor->current_limit); + break; + case SCMD_LIM_VEL: // velocity limit + printVerbose(F("vel: ")); + if(!GET){ + motor->velocity_limit = value; + motor->P_angle.limit = value; + } + println(motor->velocity_limit); + break; + default: + printError(); + break; + } + break; + case CMD_MOTION_TYPE: + case CMD_TORQUE_TYPE: + case CMD_STATUS: + motion(motor, &user_command[0]); + break; + case CMD_PWMMOD: + // PWM modulation change + printVerbose(F("PWM Mod | ")); + switch (sub_cmd){ + case SCMD_PWMMOD_TYPE: // zero offset + printVerbose(F("type: ")); + if(!GET) motor->foc_modulation = (FOCModulationType)value; + switch(motor->foc_modulation){ + case FOCModulationType::SinePWM: + println(F("SinePWM")); + break; + case FOCModulationType::SpaceVectorPWM: + println(F("SVPWM")); + break; + case FOCModulationType::Trapezoid_120: + println(F("Trap 120")); + break; + case FOCModulationType::Trapezoid_150: + println(F("Trap 150")); + break; + } + break; + case SCMD_PWMMOD_CENTER: // centered modulation + printVerbose(F("center: ")); + if(!GET) motor->modulation_centered = value; + println(motor->modulation_centered); + break; + default: + printError(); + break; + } + break; + case CMD_RESIST: + printVerbose(F("R phase: ")); + if(!GET){ + motor->phase_resistance = value; + if(motor->torque_controller==TorqueControlType::voltage) + motor->PID_velocity.limit= motor->current_limit; + } + if(_isset(motor->phase_resistance)) println(motor->phase_resistance); + else println(0); + break; + case CMD_INDUCTANCE: + printVerbose(F("L phase: ")); + if(!GET){ + motor->phase_inductance = value; + } + if(_isset(motor->phase_inductance)) println(motor->phase_inductance); + else println(0); + break; + case CMD_KV_RATING: + printVerbose(F("Motor KV: ")); + if(!GET){ + motor->KV_rating = value; + } + if(_isset(motor->KV_rating)) println(motor->KV_rating); + else println(0); + break; + case CMD_SENSOR: + // Sensor zero offset + printVerbose(F("Sensor | ")); + switch (sub_cmd){ + case SCMD_SENS_MECH_OFFSET: // zero offset + printVerbose(F("offset: ")); + if(!GET) motor->sensor_offset = value; + println(motor->sensor_offset); + break; + case SCMD_SENS_ELEC_OFFSET: // electrical zero offset - not suggested to touch + printVerbose(F("el. offset: ")); + if(!GET) motor->zero_electric_angle = value; + println(motor->zero_electric_angle); + break; + default: + printError(); + break; + } + break; + case CMD_MONITOR: // get current values of the state variables + printVerbose(F("Monitor | ")); + switch (sub_cmd){ + case SCMD_GET: // get command + switch((uint8_t)value){ + case 0: // get target + printVerbose(F("target: ")); + println(motor->target); + break; + case 1: // get voltage q + printVerbose(F("Vq: ")); + println(motor->voltage.q); + break; + case 2: // get voltage d + printVerbose(F("Vd: ")); + println(motor->voltage.d); + break; + case 3: // get current q + printVerbose(F("Cq: ")); + println(motor->current.q); + break; + case 4: // get current d + printVerbose(F("Cd: ")); + println(motor->current.d); + break; + case 5: // get velocity + printVerbose(F("vel: ")); + println(motor->shaft_velocity); + break; + case 6: // get angle + printVerbose(F("angle: ")); + println(motor->shaft_angle); + break; + case 7: // get all states + printVerbose(F("all: ")); + print(motor->target); + print(";"); + print(motor->voltage.q); + print(";"); + print(motor->voltage.d); + print(";"); + print(motor->current.q); + print(";"); + print(motor->current.d); + print(";"); + print(motor->shaft_velocity); + print(";"); + println(motor->shaft_angle); + break; + default: + printError(); + break; + } + break; + case SCMD_DOWNSAMPLE: + printVerbose(F("downsample: ")); + if(!GET) motor->monitor_downsample = value; + println((int)motor->monitor_downsample); + break; + case SCMD_CLEAR: + motor->monitor_variables = (uint8_t) 0; + println(F("clear")); + break; + case CMD_DECIMAL: + printVerbose(F("decimal: ")); + motor->monitor_decimals = value; + println((int)motor->monitor_decimals); + break; + case SCMD_SET: + if(!GET){ + // set the variables + motor->monitor_variables = (uint8_t) 0; + for(int i = 0; i < 7; i++){ + if(isSentinel(user_command[value_index+i])) break; + motor->monitor_variables |= (user_command[value_index+i] - '0') << (6-i); + } + } + // print the variables + for(int i = 0; i < 7; i++){ + print( (motor->monitor_variables & (1 << (6-i))) >> (6-i)); + } + println(""); + break; + default: + printError(); + break; + } + break; + default: // unknown cmd + printVerbose(F("unknown cmd ")); + printError(); + } +} + +void Commander::motion(FOCMotor* motor, char* user_cmd, char* separator){ + char cmd = user_cmd[0]; + char sub_cmd = user_cmd[1]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[(sub_cmd >= 'A' && sub_cmd <= 'Z') ? 2 : 1]); + + switch(cmd){ + case CMD_MOTION_TYPE: + printVerbose(F("Motion:")); + switch(sub_cmd){ + case SCMD_DOWNSAMPLE: + printVerbose(F(" downsample: ")); + if(!GET) motor->motion_downsample = value; + println((int)motor->motion_downsample); + break; + default: + // change control type + if(!GET && value >= 0 && (int)value < 5) // if set command + motor->controller = (MotionControlType)value; + switch(motor->controller){ + case MotionControlType::torque: + println(F("torque")); + break; + case MotionControlType::velocity: + println(F("vel")); + break; + case MotionControlType::angle: + println(F("angle")); + break; + case MotionControlType::velocity_openloop: + println(F("vel open")); + break; + case MotionControlType::angle_openloop: + println(F("angle open")); + break; + } + break; + } + break; + case CMD_TORQUE_TYPE: + // change control type + printVerbose(F("Torque: ")); + if(!GET && (int8_t)value >= 0 && (int8_t)value < 3)// if set command + motor->torque_controller = (TorqueControlType)value; + switch(motor->torque_controller){ + case TorqueControlType::voltage: + println(F("volt")); + // change the velocity control limits if necessary + if( !_isset(motor->phase_resistance) ) motor->PID_velocity.limit = motor->voltage_limit; + break; + case TorqueControlType::dc_current: + println(F("dc curr")); + // change the velocity control limits if necessary + motor->PID_velocity.limit = motor->current_limit; + break; + case TorqueControlType::foc_current: + println(F("foc curr")); + // change the velocity control limits if necessary + motor->PID_velocity.limit = motor->current_limit; + break; + } + break; + case CMD_STATUS: + // enable/disable + printVerbose(F("Status: ")); + if(!GET) (bool)value ? motor->enable() : motor->disable(); + println(motor->enabled); + break; + default: + target(motor, user_cmd, separator); + break; + } +} + +void Commander::pid(PIDController* pid, char* user_cmd){ + char cmd = user_cmd[0]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[1]); + + switch (cmd){ + case SCMD_PID_P: // P gain change + printVerbose("P: "); + if(!GET) pid->P = value; + println(pid->P); + break; + case SCMD_PID_I: // I gain change + printVerbose("I: "); + if(!GET) pid->I = value; + println(pid->I); + break; + case SCMD_PID_D: // D gain change + printVerbose("D: "); + if(!GET) pid->D = value; + println(pid->D); + break; + case SCMD_PID_RAMP: // ramp change + printVerbose("ramp: "); + if(!GET) pid->output_ramp = value; + println(pid->output_ramp); + break; + case SCMD_PID_LIM: // limit change + printVerbose("limit: "); + if(!GET) pid->limit = value; + println(pid->limit); + break; + default: + printError(); + break; + } +} + +void Commander::lpf(LowPassFilter* lpf, char* user_cmd){ + char cmd = user_cmd[0]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[1]); + + switch (cmd){ + case SCMD_LPF_TF: // Tf value change + printVerbose(F("Tf: ")); + if(!GET) lpf->Tf = value; + println(lpf->Tf); + break; + default: + printError(); + break; + } +} + +void Commander::scalar(float* value, char* user_cmd){ + bool GET = isSentinel(user_cmd[0]); + if(!GET) *value = atof(user_cmd); + println(*value); +} + + +void Commander::target(FOCMotor* motor, char* user_cmd, char* separator){ + // if no values sent + if(isSentinel(user_cmd[0])) { + printlnMachineReadable(motor->target); + return; + }; + + float pos, vel, torque; + char* next_value; + switch(motor->controller){ + case MotionControlType::torque: // setting torque target + torque = atof(strtok (user_cmd, separator)); + motor->target = torque; + break; + case MotionControlType::velocity: // setting velocity target + torque limit + // set the target + vel= atof(strtok (user_cmd, separator)); + motor->target = vel; + + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value){ + torque = atof(next_value); + motor->PID_velocity.limit = torque; + // torque command can be voltage or current + if(!_isset(motor->phase_resistance) && motor->torque_controller == TorqueControlType::voltage) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + break; + case MotionControlType::angle: // setting angle target + torque, velocity limit + // setting the target position + pos= atof(strtok (user_cmd, separator)); + motor->target = pos; + + // allow for setting only the target position without chaning the velocity/torque limits + next_value = strtok (NULL, separator); + if( next_value ){ + vel = atof(next_value); + motor->velocity_limit = vel; + motor->P_angle.limit = vel; + + // allow for setting only the target position and velocity limit without the torque limit + next_value = strtok (NULL, separator); + if( next_value ){ + torque= atof(next_value); + motor->PID_velocity.limit = torque; + // torque command can be voltage or current + if(!_isset(motor->phase_resistance) && motor->torque_controller == TorqueControlType::voltage) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + } + break; + case MotionControlType::velocity_openloop: // setting velocity target + torque limit + // set the target + vel= atof(strtok (user_cmd, separator)); + motor->target = vel; + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value ){ + torque = atof(next_value); + // torque command can be voltage or current + if(!_isset(motor->phase_resistance)) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + break; + case MotionControlType::angle_openloop: // setting angle target + torque, velocity limit + // set the target + pos= atof(strtok (user_cmd, separator)); + motor->target = pos; + + // allow for setting only the target position without chaning the velocity/torque limits + next_value = strtok (NULL, separator); + if( next_value ){ + vel = atof(next_value); + motor->velocity_limit = vel; + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value ){ + torque = atof(next_value); + // torque command can be voltage or current + if(!_isset(motor->phase_resistance)) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + } + break; + } + printVerbose(F("Target: ")); + println(motor->target); +} + + +bool Commander::isSentinel(char ch) +{ + if(ch == eol) + return true; + else if (ch == '\r') + { + printVerbose(F("Warn: \\r detected! \n")); + return true; // lets still consider it to end the line... + } + return false; +} + +void Commander::print(const int number){ + if( !com_port || verbose == VerboseMode::nothing ) return; + com_port->print(number); +} +void Commander::print(const float number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print((float)number,(int)decimal_places); +} +void Commander::print(const char* message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} +void Commander::print(const __FlashStringHelper *message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} +void Commander::print(const char message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} + +void Commander::println(const int number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(number); +} +void Commander::println(const float number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println((float)number, (int)decimal_places); +} +void Commander::println(const char* message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} +void Commander::println(const __FlashStringHelper *message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} +void Commander::println(const char message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} + + +void Commander::printVerbose(const char* message){ + if(verbose == VerboseMode::user_friendly) print(message); +} +void Commander::printVerbose(const __FlashStringHelper *message){ + if(verbose == VerboseMode::user_friendly) print(message); +} + +void Commander::printMachineReadable(const int number){ + if(verbose == VerboseMode::machine_readable) print(number); +} +void Commander::printMachineReadable(const float number){ + if(verbose == VerboseMode::machine_readable) print(number); +} +void Commander::printMachineReadable(const char* message){ + if(verbose == VerboseMode::machine_readable) print(message); +} +void Commander::printMachineReadable(const __FlashStringHelper *message){ + if(verbose == VerboseMode::machine_readable) print(message); +} +void Commander::printMachineReadable(const char message){ + if(verbose == VerboseMode::machine_readable) print(message); +} + +void Commander::printlnMachineReadable(const int number){ + if(verbose == VerboseMode::machine_readable) println(number); +} +void Commander::printlnMachineReadable(const float number){ + if(verbose == VerboseMode::machine_readable) println(number); +} +void Commander::printlnMachineReadable(const char* message){ + if(verbose == VerboseMode::machine_readable) println(message); +} +void Commander::printlnMachineReadable(const __FlashStringHelper *message){ + if(verbose == VerboseMode::machine_readable) println(message); +} +void Commander::printlnMachineReadable(const char message){ + if(verbose == VerboseMode::machine_readable) println(message); +} + +void Commander::printError(){ + println(F("err")); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.h b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.h new file mode 100644 index 0000000..91e3dc4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/Commander.h @@ -0,0 +1,301 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +#include "Arduino.h" +#include "../common/base_classes/FOCMotor.h" +#include "../common/pid.h" +#include "../common/lowpass_filter.h" +#include "commands.h" + + +#define MAX_COMMAND_LENGTH 20 + + +// Commander verbose display to the user type +enum VerboseMode : uint8_t { + nothing = 0x00, // display nothing - good for monitoring + on_request = 0x01, // display only on user request + user_friendly = 0x02, // display textual messages to the user + machine_readable = 0x03 // display machine readable commands, matching commands to set each settings +}; + + +// callback function pointer definiton +typedef void (* CommandCallback)(char*); //!< command callback function pointer + +/** + * Commander class implementing string communication protocol based on IDvalue (ex AB5.321 - command id `A`, sub-command id `B`,value `5.321`) + * + * - This class can be used in combination with HardwareSerial instance which it would read and write + * or it can be used to parse strings that have been received from the user outside this library + * - Commander class implements command protocol for few standard components of the SimpleFOC library + * - FOCMotor + * - PIDController + * - LowPassFilter + * - Commander also provides a very simple command > callback interface that enables user to + * attach a callback function to certain command id - see function add() + */ +class Commander +{ + public: + /** + * Default constructor receiving a serial interface that it uses to output the values to + * Also if the function run() is used it uses this serial instance to read the serial for user commands + * + * @param serial - Serial com port instance + * @param eol - the end of line sentinel character + * @param echo - echo last typed character (for command line feedback) + */ + Commander(Stream &serial, char eol = '\n', bool echo = false); + Commander(char eol = '\n', bool echo = false); + + /** + * Function reading the serial port and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + */ + void run(); + /** + * Function reading the string of user input and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + * + * @param reader - temporary stream to read user input + * @param eol - temporary end of line sentinel + */ + void run(Stream &reader, char eol = '\n'); + /** + * Function reading the string of user input and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + * + * @param user_input - string of user inputs + */ + void run(char* user_input); + + /** + * Function adding a callback to the coomander withe the command id + * @param id - char command letter + * @param onCommand - function pointer void function(char*) + * @param label - string label to be displayed when scan command sent + */ + void add(char id , CommandCallback onCommand, char* label = nullptr); + + // printing variables + VerboseMode verbose = VerboseMode::user_friendly; //!< flag signaling that the commands should output user understanable text + uint8_t decimal_places = 3; //!< number of decimal places to be used when displaying numbers + + // monitoring functions + Stream* com_port = nullptr; //!< Serial terminal variable if provided + char eol = '\n'; //!< end of line sentinel character + bool echo = false; //!< echo last typed character (for command line feedback) + + /** + * + * FOC motor (StepperMotor and BLDCMotor) command interface + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * + * - It has several paramters (the letters can be changed in the commands.h file) + * 'Q' - Q current PID controller & LPF (see function pid and lpf for commands) + * 'D' - D current PID controller & LPF (see function pid and lpf for commands) + * 'V' - Velocity PID controller & LPF (see function pid and lpf for commands) + * 'A' - Angle PID controller & LPF (see function pid and lpf for commands) + * 'L' - Limits + * sub-commands: + * 'C' - Current + * 'U' - Voltage + * 'V' - Velocity + * 'C' - Motion control type config + * sub-commands: + * 'D' - downsample motiron loop + * '0' - torque + * '1' - velocity + * '2' - angle + * 'T' - Torque control type + * sub-commands: + * '0' - voltage + * '1' - current + * '2' - foc_current + * 'E' - Motor status (enable/disable) + * sub-commands: + * '0' - enable + * '1' - disable + * 'R' - Motor resistance + * 'S' - Sensor offsets + * sub-commands: + * 'M' - sensor offset + * 'E' - sensor electrical zero + * 'M' - Monitoring control + * sub-commands: + * 'D' - downsample monitoring + * 'C' - clear monitor + * 'S' - set monitoring variables + * 'G' - get variable value + * '' - Target setting interface + * Depends of the motion control mode: + * - torque : torque (ex. M2.5) + * - velocity (open and closed loop) : velocity torque (ex.M10 2.5 or M10 to only chanage the target witout limits) + * - angle (open and closed loop) : angle velocity torque (ex.M3.5 10 2.5 or M3.5 to only chanage the target witout limits) + * + * - Each of them can be get by sening the command letter -(ex. 'R' - to get the phase resistance) + * - Each of them can be set by sending 'IdSubidValue' - (ex. SM1.5 for setting sensor zero offset to 1.5f) + * + */ + void motor(FOCMotor* motor, char* user_cmd); + + /** + * Low pass fileter command interface + * @param lpf - LowPassFilter instance + * @param user_cmd - the string command + * + * - It only has one property - filtering time constant Tf + * - It can be get by sending 'F' + * - It can be set by sending 'Fvalue' - (ex. F0.01 for settin Tf=0.01) + */ + void lpf(LowPassFilter* lpf, char* user_cmd); + /** + * PID controller command interface + * @param pid - PIDController instance + * @param user_cmd - the string command + * + * - It has several paramters (the letters can be changed in the commands.h file) + * - P gain - 'P' + * - I gain - 'I' + * - D gain - 'D' + * - output ramp - 'R' + * - output limit - 'L' + * - Each of them can be get by sening the command letter -(ex. 'D' - to get the D gain) + * - Each of them can be set by sending 'IDvalue' - (ex. I1.5 for setting I=1.5) + */ + void pid(PIDController* pid, char* user_cmd); + /** + * Float variable scalar command interface + * @param value - float variable pointer + * @param user_cmd - the string command + * + * - It only has one property - one float value + * - It can be get by sending an empty string '\n' + * - It can be set by sending 'value' - (ex. 0.01f for settin *value=0.01) + */ + void scalar(float* value, char* user_cmd); + /** + * Target setting interface, enables setting the target and limiting variables at once. + * The values are sent separated by a separator specified as the third argument. The default separator is the space. + * + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * @param separator - the string separator in between target and limit values, default is space - " " + * + * Example: P2.34 70 2 + * `P` is the user defined command, `2.34` is the target angle `70` is the target + * velocity and `2` is the desired max current. + * + * It depends of the motion control mode: + * - torque : torque (ex. P2.5) + * - velocity : velocity torque (ex.P10 2.5 or P10 to only chanage the target witout limits) + * - angle : angle velocity torque (ex.P3.5 10 2.5 or P3.5 to only chanage the target witout limits) + */ + void target(FOCMotor* motor, char* user_cmd, char* separator = (char *)" "); + + /** + * FOC motor (StepperMotor and BLDCMotor) motion control interfaces + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * @param separator - the string separator in between target and limit values, default is space - " " + * + * Commands: + * 'C' - Motion control type config + * sub-commands: + * 'D' - downsample motiron loop + * '0' - torque + * '1' - velocity + * '2' - angle + * 'T' - Torque control type + * sub-commands: + * '0' - voltage + * '1' - current + * '2' - foc_current + * 'E' - Motor status (enable/disable) + * sub-commands: + * '0' - enable + * '1' - disable + * '' - Target setting interface + * Depends of the motion control mode: + * - torque : torque (ex. M2.5) + * - velocity (open and closed loop) : velocity torque (ex.M10 2.5 or M10 to only chanage the target witout limits) + * - angle (open and closed loop) : angle velocity torque (ex.M3.5 10 2.5 or M3.5 to only chanage the target witout limits) + */ + void motion(FOCMotor* motor, char* user_cmd, char* separator = (char *)" "); + + bool isSentinel(char ch); + private: + // Subscribed command callback variables + CommandCallback call_list[20];//!< array of command callback pointers - 20 is an arbitrary number + char call_ids[20]; //!< added callback commands + char* call_label[20]; //!< added callback labels + int call_count = 0;//!< number callbacks that are subscribed + + // helping variable for serial communication reading + char received_chars[MAX_COMMAND_LENGTH] = {0}; //!< so far received user message - waiting for newline + int rec_cnt = 0; //!< number of characters receives + + // serial printing functions + /** + * print the string message only if verbose mode on + * @param message - message to be printed + */ + void printVerbose(const char* message); + /** + * Print the string message only if verbose mode on + * - Function handling the case for strings defined by F macro + * @param message - message to be printed + */ + void printVerbose(const __FlashStringHelper *message); + /** + * print the numbers to the serial with desired decimal point number + * @param message - number to be printed + * @param newline - if needs lewline (1) otherwise (0) + */ + + void print(const float number); + void print(const int number); + void print(const char* message); + void print(const __FlashStringHelper *message); + void print(const char message); + void println(const float number); + void println(const int number); + void println(const char* message); + void println(const __FlashStringHelper *message); + void println(const char message); + + void printMachineReadable(const float number); + void printMachineReadable(const int number); + void printMachineReadable(const char* message); + void printMachineReadable(const __FlashStringHelper *message); + void printMachineReadable(const char message); + + void printlnMachineReadable(const float number); + void printlnMachineReadable(const int number); + void printlnMachineReadable(const char* message); + void printlnMachineReadable(const __FlashStringHelper *message); + void printlnMachineReadable(const char message); + + + void printError(); +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.cpp new file mode 100644 index 0000000..e969d8a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.cpp @@ -0,0 +1,104 @@ + +#include "SimpleFOCDebug.h" + +#ifndef SIMPLEFOC_DISABLE_DEBUG + + +Print* SimpleFOCDebug::_debugPrint = NULL; + + +void SimpleFOCDebug::enable(Print* debugPrint) { + _debugPrint = debugPrint; +} + + +void SimpleFOCDebug::println(int val) { + if (_debugPrint != NULL) { + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(float val) { + if (_debugPrint != NULL) { + _debugPrint->println(val); + } +} + + + +void SimpleFOCDebug::println(const char* str) { + if (_debugPrint != NULL) { + _debugPrint->println(str); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str) { + if (_debugPrint != NULL) { + _debugPrint->println(str); + } +} + +void SimpleFOCDebug::println(const char* str, float val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str, float val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const char* str, int val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str, int val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + + +void SimpleFOCDebug::print(const char* str) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + } +} + + +void SimpleFOCDebug::print(const __FlashStringHelper* str) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + } +} + + +void SimpleFOCDebug::print(int val) { + if (_debugPrint != NULL) { + _debugPrint->print(val); + } +} + + +void SimpleFOCDebug::print(float val) { + if (_debugPrint != NULL) { + _debugPrint->print(val); + } +} + + +void SimpleFOCDebug::println() { + if (_debugPrint != NULL) { + _debugPrint->println(); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.h b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.h new file mode 100644 index 0000000..614e637 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/SimpleFOCDebug.h @@ -0,0 +1,79 @@ + +#ifndef __SIMPLEFOCDEBUG_H__ +#define __SIMPLEFOCDEBUG_H__ + +#include "Arduino.h" + + +/** + * SimpleFOCDebug class + * + * This class is used to print debug messages to a chosen output. + * Currently, Print instances are supported as targets, e.g. serial port. + * + * Activate debug output globally by calling enable(), optionally passing + * in a Print instance. If none is provided "Serial" is used by default. + * + * To produce debug output, use the macro SIMPLEFOC_DEBUG: + * SIMPLEFOC_DEBUG("Debug message!"); + * SIMPLEFOC_DEBUG("a float value:", 123.456f); + * SIMPLEFOC_DEBUG("an integer value: ", 123); + * + * Keep debugging output short and simple. Some of our MCUs have limited + * RAM and limited serial output capabilities. + * + * By default, the SIMPLEFOC_DEBUG macro uses the flash string helper to + * help preserve memory on Arduino boards. + * + * You can also disable debug output completely. In this case all debug output + * and the SimpleFOCDebug class is removed from the compiled code. + * Add -DSIMPLEFOC_DISABLE_DEBUG to your compiler flags to disable debug in + * this way. + * + **/ + + +#ifndef SIMPLEFOC_DISABLE_DEBUG + +class SimpleFOCDebug { +public: + static void enable(Print* debugPrint = &Serial); + + static void println(const __FlashStringHelper* msg); + static void println(const char* msg); + static void println(const __FlashStringHelper* msg, float val); + static void println(const char* msg, float val); + static void println(const __FlashStringHelper* msg, int val); + static void println(const char* msg, int val); + static void println(); + static void println(int val); + static void println(float val); + + static void print(const char* msg); + static void print(const __FlashStringHelper* msg); + static void print(int val); + static void print(float val); + +protected: + static Print* _debugPrint; +}; + + +#define SIMPLEFOC_DEBUG(msg, ...) \ + SimpleFOCDebug::println(F(msg), ##__VA_ARGS__) + + + + + +#else //ifndef SIMPLEFOC_DISABLE_DEBUG + + + +#define SIMPLEFOC_DEBUG(msg, ...) + + + +#endif //ifndef SIMPLEFOC_DISABLE_DEBUG +#endif + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.cpp new file mode 100644 index 0000000..ee4f69e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.cpp @@ -0,0 +1,40 @@ +#include "StepDirListener.h" + +StepDirListener::StepDirListener(int _pinStep, int _pinDir, float _counter_to_value){ + pin_step = _pinStep; + pin_dir = _pinDir; + counter_to_value = _counter_to_value; +} + +void StepDirListener::init(){ + pinMode(pin_dir, INPUT); + pinMode(pin_step, INPUT_PULLUP); + count = 0; +} + +void StepDirListener::enableInterrupt(void (*doA)()){ + attachInterrupt(digitalPinToInterrupt(pin_step), doA, polarity); +} + +void StepDirListener::attach(float* variable){ + attached_variable = variable; +} + +void StepDirListener::handle(){ + // read step status + //bool step = digitalRead(pin_step); + // update counter only on rising edge + //if(step && step != step_active){ + if(digitalRead(pin_dir)) + count++; + else + count--; + //} + //step_active = step; + // if attached variable update it + if(attached_variable) *attached_variable = getValue(); +} +// calculate the position from counter +float StepDirListener::getValue(){ + return (float) count * counter_to_value; +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.h b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.h new file mode 100644 index 0000000..f9691fd --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/StepDirListener.h @@ -0,0 +1,65 @@ +#ifndef STEPDIR_H +#define STEPDIR_H + +#include "Arduino.h" +#include "../common/foc_utils.h" + + +#if !defined(TARGET_RP2040) && !defined(_SAMD21_) && !defined(_SAMD51_) && !defined(_SAME51_) && !defined(ARDUINO_UNOR4_WIFI) && !defined(ARDUINO_UNOR4_MINIMA) && !defined(NRF52_SERIES) && !defined(ARDUINO_ARCH_MEGAAVR) +#define PinStatus int +#endif + + +/** + * Step/Dir listenner class for easier interraction with this communication interface. + */ +class StepDirListener +{ + public: + + /** + * Constructor for step/direction interface + * @param step - pin + * @param direction - pin + * @param counter_to_value - step counter to value + */ + StepDirListener(int pinStep, int pinDir, float counter_to_value = 1); + /** + * Start listenning for step commands + * + * @param handleStep - on step received handler + */ + void enableInterrupt(void (*handleStep)()); + + /** + * Initialise dir and step commands + */ + void init(); + /** + * step handler + */ + void handle(); + /** + * Get so far received valued + */ + float getValue(); + /** + * Attach the value to be updated on each step receive + * - no need to call getValue function + */ + void attach(float* variable); + + // variables + int pin_step; //!< step pin + int pin_dir; //!< direction pin + long count; //!< current counter value - should be set to 0 for homing + PinStatus polarity = RISING; //!< polarity of the step pin + + private: + float* attached_variable = nullptr; //!< pointer to the attached variable + float counter_to_value; //!< step counter to value + //bool step_active = 0; //!< current step pin status (HIGH/LOW) - debouncing variable + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/communication/commands.h b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/commands.h new file mode 100644 index 0000000..323a8e7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/communication/commands.h @@ -0,0 +1,52 @@ +#ifndef COMMANDS_h +#define COMMANDS_h + +// see docs.simplefoc.com for in depth explanation of the commands + +// list of commands + #define CMD_C_D_PID 'D' //!< current d PID & LPF + #define CMD_C_Q_PID 'Q' //!< current d PID & LPF + #define CMD_V_PID 'V' //!< velocity PID & LPF + #define CMD_A_PID 'A' //!< angle PID & LPF + #define CMD_STATUS 'E' //!< motor status enable/disable + #define CMD_LIMITS 'L' //!< limits current/voltage/velocity + #define CMD_MOTION_TYPE 'C' //!< motion control type + #define CMD_TORQUE_TYPE 'T' //!< torque control type + #define CMD_SENSOR 'S' //!< sensor offsets + #define CMD_MONITOR 'M' //!< monitoring + #define CMD_RESIST 'R' //!< motor phase resistance + #define CMD_INDUCTANCE 'I' //!< motor phase inductance + #define CMD_KV_RATING 'K' //!< motor kv rating + #define CMD_PWMMOD 'W' //!< pwm modulation + + // commander configuration + #define CMD_SCAN '?' //!< command scaning the network - only for commander + #define CMD_VERBOSE '@' //!< command setting output mode - only for commander + #define CMD_DECIMAL '#' //!< command setting decimal places - only for commander + + // subcomands + //pid - lpf + #define SCMD_PID_P 'P' //!< PID gain P + #define SCMD_PID_I 'I' //!< PID gain I + #define SCMD_PID_D 'D' //!< PID gain D + #define SCMD_PID_RAMP 'R' //!< PID ramp + #define SCMD_PID_LIM 'L' //!< PID limit + #define SCMD_LPF_TF 'F' //!< LPF time constant + // limits + #define SCMD_LIM_CURR 'C' //!< Limit current + #define SCMD_LIM_VOLT 'U' //!< Limit voltage + #define SCMD_LIM_VEL 'V' //!< Limit velocity + //sensor + #define SCMD_SENS_MECH_OFFSET 'M' //!< Sensor offset + #define SCMD_SENS_ELEC_OFFSET 'E' //!< Sensor electrical zero offset + // monitoring + #define SCMD_DOWNSAMPLE 'D' //!< Monitoring downsample value + #define SCMD_CLEAR 'C' //!< Clear all monitored variables + #define SCMD_GET 'G' //!< Get variable only one value + #define SCMD_SET 'S' //!< Set variables to be monitored + + #define SCMD_PWMMOD_TYPE 'T' //!<< Pwm modulation type + #define SCMD_PWMMOD_CENTER 'C' //!<< Pwm modulation center flag + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.cpp new file mode 100644 index 0000000..635535f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.cpp @@ -0,0 +1,163 @@ +#include "GenericCurrentSense.h" + +// GenericCurrentSense constructor +GenericCurrentSense::GenericCurrentSense(PhaseCurrent_s (*readCallback)(), void (*initCallback)()){ + // if function provided add it to the + if(readCallback != nullptr) this->readCallback = readCallback; + if(initCallback != nullptr) this->initCallback = initCallback; +} + +// Inline sensor init function +int GenericCurrentSense::init(){ + // configure ADC variables + if(initCallback != nullptr) initCallback(); + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void GenericCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 1000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + PhaseCurrent_s current = readCallback(); + offset_ia += current.a; + offset_ib += current.b; + offset_ic += current.c; + _delay(1); + } + // calculate the mean offsets + offset_ia = offset_ia / calibration_rounds; + offset_ib = offset_ib / calibration_rounds; + offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s GenericCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current = readCallback(); + current.a = (current.a - offset_ia); // amps + current.b = (current.b - offset_ib); // amps + current.c = (current.c - offset_ic); // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int GenericCurrentSense::driverAlign(float voltage){ + _UNUSED(voltage) ; // remove unused parameter warning + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (!initialized) return 0; + + // // set phase A active and phases B and C down + // driver->setPwm(voltage, 0, 0); + // _delay(200); + // PhaseCurrent_s c = getPhaseCurrents(); + // // read the current 100 times ( arbitrary number ) + // for (int i = 0; i < 100; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.a = c.a*0.6f + 0.4f*c1.a; + // c.b = c.b*0.6f + 0.4f*c1.b; + // c.c = c.c*0.6f + 0.4f*c1.c; + // _delay(3); + // } + // driver->setPwm(0, 0, 0); + // // align phase A + // float ab_ratio = fabs(c.a / c.b); + // float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + // if( ab_ratio > 1.5f ){ // should be ~2 + // gain_a *= _sign(c.a); + // }else if( ab_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and B + // int tmp_pinA = pinA; + // pinA = pinB; + // pinB = tmp_pinA; + // gain_a *= _sign(c.b); + // exit_flag = 2; // signal that pins have been switched + // }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and C + // int tmp_pinA = pinA; + // pinA = pinC; + // pinC= tmp_pinA; + // gain_a *= _sign(c.c); + // exit_flag = 2;// signal that pins have been switched + // }else{ + // // error in current sense - phase either not measured or bad connection + // return 0; + // } + + // // set phase B active and phases A and C down + // driver->setPwm(0, voltage, 0); + // _delay(200); + // c = getPhaseCurrents(); + // // read the current 50 times + // for (int i = 0; i < 100; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.a = c.a*0.6f + 0.4f*c1.a; + // c.b = c.b*0.6f + 0.4f*c1.b; + // c.c = c.c*0.6f + 0.4f*c1.c; + // _delay(3); + // } + // driver->setPwm(0, 0, 0); + // float ba_ratio = fabs(c.b/c.a); + // float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + // if( ba_ratio > 1.5f ){ // should be ~2 + // gain_b *= _sign(c.b); + // }else if( ba_ratio < 0.7f ){ // it should be ~0.5 + // // switch phase A and B + // int tmp_pinB = pinB; + // pinB = pinA; + // pinA = tmp_pinB; + // gain_b *= _sign(c.a); + // exit_flag = 2; // signal that pins have been switched + // }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and C + // int tmp_pinB = pinB; + // pinB = pinC; + // pinC = tmp_pinB; + // gain_b *= _sign(c.c); + // exit_flag = 2; // signal that pins have been switched + // }else{ + // // error in current sense - phase either not measured or bad connection + // return 0; + // } + + // // if phase C measured + // if(_isset(pinC)){ + // // set phase B active and phases A and C down + // driver->setPwm(0, 0, voltage); + // _delay(200); + // c = getPhaseCurrents(); + // // read the adc voltage 500 times ( arbitrary number ) + // for (int i = 0; i < 50; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.c = (c.c+c1.c)/50.0f; + // } + // driver->setPwm(0, 0, 0); + // gain_c *= _sign(c.c); + // } + + // if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + return exit_flag; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.h new file mode 100644 index 0000000..a63df49 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/GenericCurrentSense.h @@ -0,0 +1,40 @@ +#ifndef GENERIC_CS_LIB_H +#define GENERIC_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class GenericCurrentSense: public CurrentSense{ + public: + /** + GenericCurrentSense class constructor + */ + GenericCurrentSense(PhaseCurrent_s (*readCallback)() = nullptr, void (*initCallback)() = nullptr); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + + PhaseCurrent_s (*readCallback)() = nullptr; //!< function pointer to sensor reading + void (*initCallback)() = nullptr; //!< function pointer to sensor initialisation + + private: + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.cpp new file mode 100644 index 0000000..492b3ac --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.cpp @@ -0,0 +1,253 @@ +#include "InlineCurrentSense.h" +#include "communication/SimpleFOCDebug.h" +// InlineCurrentSensor constructor +// - shunt_resistor - shunt resistor value +// - gain - current-sense op-amp gain +// - phA - A phase adc pin +// - phB - B phase adc pin +// - phC - C phase adc pin (optional) +InlineCurrentSense::InlineCurrentSense(float _shunt_resistor, float _gain, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + shunt_resistor = _shunt_resistor; + amp_gain = _gain; + volts_to_amps_ratio = 1.0f /_shunt_resistor / _gain; // volts to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +}; + + +InlineCurrentSense::InlineCurrentSense(float _mVpA, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + volts_to_amps_ratio = 1000.0f / _mVpA; // mV to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +}; + + + +// Inline sensor init function +int InlineCurrentSense::init(){ + // if no linked driver its fine in this case + // at least for init() + void* drv_params = driver ? driver->params : nullptr; + // configure ADC variables + params = _configureADCInline(drv_params,pinA,pinB,pinC); + // if init failed return fail + if (params == SIMPLEFOC_CURRENT_SENSE_INIT_FAILED) return 0; + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void InlineCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 1000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + if(_isset(pinA)) offset_ia += _readADCVoltageInline(pinA, params); + if(_isset(pinB)) offset_ib += _readADCVoltageInline(pinB, params); + if(_isset(pinC)) offset_ic += _readADCVoltageInline(pinC, params); + _delay(1); + } + // calculate the mean offsets + if(_isset(pinA)) offset_ia = offset_ia / calibration_rounds; + if(_isset(pinB)) offset_ib = offset_ib / calibration_rounds; + if(_isset(pinC)) offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s InlineCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current; + current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageInline(pinA, params) - offset_ia)*gain_a;// amps + current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageInline(pinB, params) - offset_ib)*gain_b;// amps + current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageInline(pinC, params) - offset_ic)*gain_c; // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int InlineCurrentSense::driverAlign(float voltage){ + + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (driver==nullptr) { + SIMPLEFOC_DEBUG("CUR: No driver linked!"); + return 0; + } + + if (!initialized) return 0; + + if(_isset(pinA)){ + // set phase A active and phases B and C down + driver->setPwm(voltage, 0, 0); + _delay(2000); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 100 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6f + 0.4f*c1.a; + c.b = c.b*0.6f + 0.4f*c1.b; + c.c = c.c*0.6f + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + // align phase A + float ab_ratio = c.b ? fabs(c.a / c.b) : 0; + float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + if(_isset(pinB) && ab_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinC) && ac_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinB) && ab_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and B + int tmp_pinA = pinA; + pinA = pinB; + pinB = tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ib; + offset_ib = tmp_offsetA; + gain_a *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinA = pinA; + pinA = pinC; + pinC= tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ic; + offset_ic = tmp_offsetA; + gain_a *= _sign(c.c); + exit_flag = 2;// signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(_isset(pinB)){ + // set phase B active and phases A and C down + driver->setPwm(0, voltage, 0); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 50 times + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ba_ratio = c.a ? fabs(c.b / c.a) : 0; + float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + if(_isset(pinA) && ba_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinC) && bc_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinA) && ba_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and B + int tmp_pinB = pinB; + pinB = pinA; + pinA = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ia; + offset_ia = tmp_offsetB; + gain_b *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinB = pinB; + pinB = pinC; + pinC = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ic; + offset_ic = tmp_offsetB; + gain_b *= _sign(c.c); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + // if phase C measured + if(_isset(pinC)){ + // set phase C active and phases A and B down + driver->setPwm(0, 0, voltage); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the adc voltage 500 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ca_ratio = c.a ? fabs(c.c / c.a) : 0; + float cb_ratio = c.b ? fabs(c.c / c.b) : 0; + if(_isset(pinA) && ca_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinB) && cb_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinA) && ca_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and C + int tmp_pinC = pinC; + pinC = pinA; + pinA = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ia; + offset_ia = tmp_offsetC; + gain_c *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinB) && cb_ratio < 0.7f ){ // should be ~0.5 + // switch phase B and C + int tmp_pinC = pinC; + pinC = pinB; + pinB = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ib; + offset_ib = tmp_offsetC; + gain_c *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + + return exit_flag; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.h new file mode 100644 index 0000000..5fdca7d --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/InlineCurrentSense.h @@ -0,0 +1,73 @@ +#ifndef INLINE_CS_LIB_H +#define INLINE_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class InlineCurrentSense: public CurrentSense{ + public: + /** + InlineCurrentSense class constructor + @param shunt_resistor shunt resistor value + @param gain current-sense op-amp gain + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + InlineCurrentSense(float shunt_resistor, float gain, int pinA, int pinB, int pinC = NOT_SET); + /** + InlineCurrentSense class constructor + @param mVpA mV per Amp ratio + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + InlineCurrentSense(float mVpA, int pinA, int pinB, int pinC = NOT_SET); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + // ADC measuremnet gain for each phase + // support for different gains for different phases of more commonly - inverted phase currents + // this should be automated later + float gain_a; //!< phase A gain + float gain_b; //!< phase B gain + float gain_c; //!< phase C gain + + // // per phase low pass fileters + // LowPassFilter lpf_a{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current A low pass filter + // LowPassFilter lpf_b{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current B low pass filter + // LowPassFilter lpf_c{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current C low pass filter + + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + + private: + + // hardware variables + int pinA; //!< pin A analog pin for current measurement + int pinB; //!< pin B analog pin for current measurement + int pinC; //!< pin C analog pin for current measurement + + // gain variables + float shunt_resistor; //!< Shunt resistor value + float amp_gain; //!< amp gain value + float volts_to_amps_ratio; //!< Volts to amps ratio + + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.cpp new file mode 100644 index 0000000..a706bee --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.cpp @@ -0,0 +1,254 @@ +#include "LowsideCurrentSense.h" +#include "communication/SimpleFOCDebug.h" +// LowsideCurrentSensor constructor +// - shunt_resistor - shunt resistor value +// - gain - current-sense op-amp gain +// - phA - A phase adc pin +// - phB - B phase adc pin +// - phC - C phase adc pin (optional) +LowsideCurrentSense::LowsideCurrentSense(float _shunt_resistor, float _gain, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + shunt_resistor = _shunt_resistor; + amp_gain = _gain; + volts_to_amps_ratio = 1.0f /_shunt_resistor / _gain; // volts to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +} + + +LowsideCurrentSense::LowsideCurrentSense(float _mVpA, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + volts_to_amps_ratio = 1000.0f / _mVpA; // mV to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +} + + +// Lowside sensor init function +int LowsideCurrentSense::init(){ + + if (driver==nullptr) { + SIMPLEFOC_DEBUG("CUR: Driver not linked!"); + return 0; + } + + // configure ADC variables + params = _configureADCLowSide(driver->params,pinA,pinB,pinC); + // if init failed return fail + if (params == SIMPLEFOC_CURRENT_SENSE_INIT_FAILED) return 0; + // sync the driver + _driverSyncLowSide(driver->params, params); + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void LowsideCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 2000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + _startADC3PinConversionLowSide(); + if(_isset(pinA)) offset_ia += (_readADCVoltageLowSide(pinA, params)); + if(_isset(pinB)) offset_ib += (_readADCVoltageLowSide(pinB, params)); + if(_isset(pinC)) offset_ic += (_readADCVoltageLowSide(pinC, params)); + _delay(1); + } + // calculate the mean offsets + if(_isset(pinA)) offset_ia = offset_ia / calibration_rounds; + if(_isset(pinB)) offset_ib = offset_ib / calibration_rounds; + if(_isset(pinC)) offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s LowsideCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current; + _startADC3PinConversionLowSide(); + current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageLowSide(pinA, params) - offset_ia)*gain_a;// amps + current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageLowSide(pinB, params) - offset_ib)*gain_b;// amps + current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageLowSide(pinC, params) - offset_ic)*gain_c; // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int LowsideCurrentSense::driverAlign(float voltage){ + + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (!initialized) return 0; + + if(_isset(pinA)){ + // set phase A active and phases B and C down + driver->setPwm(voltage, 0, 0); + _delay(2000); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 100 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6f + 0.4f*c1.a; + c.b = c.b*0.6f + 0.4f*c1.b; + c.c = c.c*0.6f + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + // align phase A + float ab_ratio = c.b ? fabs(c.a / c.b) : 0; + float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + if(_isset(pinB) && ab_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinC) && ac_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinB) && ab_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and B + int tmp_pinA = pinA; + pinA = pinB; + pinB = tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ib; + offset_ib = tmp_offsetA; + gain_a *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinA = pinA; + pinA = pinC; + pinC= tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ic; + offset_ic = tmp_offsetA; + gain_a *= _sign(c.c); + exit_flag = 2;// signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(_isset(pinB)){ + // set phase B active and phases A and C down + driver->setPwm(0, voltage, 0); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 50 times + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ba_ratio = c.a ? fabs(c.b / c.a) : 0; + float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + if(_isset(pinA) && ba_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinC) && bc_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinA) && ba_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and B + int tmp_pinB = pinB; + pinB = pinA; + pinA = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ia; + offset_ia = tmp_offsetB; + gain_b *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinB = pinB; + pinB = pinC; + pinC = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ic; + offset_ic = tmp_offsetB; + gain_b *= _sign(c.c); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + // if phase C measured + if(_isset(pinC)){ + // set phase C active and phases A and B down + driver->setPwm(0, 0, voltage); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the adc voltage 500 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ca_ratio = c.a ? fabs(c.c / c.a) : 0; + float cb_ratio = c.b ? fabs(c.c / c.b) : 0; + if(_isset(pinA) && ca_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinB) && cb_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinA) && ca_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and C + int tmp_pinC = pinC; + pinC = pinA; + pinA = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ia; + offset_ia = tmp_offsetC; + gain_c *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinB) && cb_ratio < 0.7f ){ // should be ~0.5 + // switch phase B and C + int tmp_pinC = pinC; + pinC = pinB; + pinB = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ib; + offset_ib = tmp_offsetC; + gain_c *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + + return exit_flag; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.h new file mode 100644 index 0000000..1652b33 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/LowsideCurrentSense.h @@ -0,0 +1,73 @@ +#ifndef LOWSIDE_CS_LIB_H +#define LOWSIDE_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/base_classes/FOCMotor.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class LowsideCurrentSense: public CurrentSense{ + public: + /** + LowsideCurrentSense class constructor + @param shunt_resistor shunt resistor value + @param gain current-sense op-amp gain + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + LowsideCurrentSense(float shunt_resistor, float gain, int pinA, int pinB, int pinC = _NC); + /** + LowsideCurrentSense class constructor + @param mVpA mV per Amp ratio + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + LowsideCurrentSense(float mVpA, int pinA, int pinB, int pinC = _NC); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + // ADC measuremnet gain for each phase + // support for different gains for different phases of more commonly - inverted phase currents + // this should be automated later + float gain_a; //!< phase A gain + float gain_b; //!< phase B gain + float gain_c; //!< phase C gain + + // // per phase low pass fileters + // LowPassFilter lpf_a{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current A low pass filter + // LowPassFilter lpf_b{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current B low pass filter + // LowPassFilter lpf_c{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current C low pass filter + + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + private: + + // hardware variables + int pinA; //!< pin A analog pin for current measurement + int pinB; //!< pin B analog pin for current measurement + int pinC; //!< pin C analog pin for current measurement + + // gain variables + float shunt_resistor; //!< Shunt resistor value + float amp_gain; //!< amp gain value + float volts_to_amps_ratio; //!< Volts to amps ratio + + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_api.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_api.h new file mode 100644 index 0000000..7862b70 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_api.h @@ -0,0 +1,65 @@ +#ifndef HARDWARE_UTILS_CURRENT_H +#define HARDWARE_UTILS_CURRENT_H + +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// flag returned if current sense init fails +#define SIMPLEFOC_CURRENT_SENSE_INIT_FAILED ((void*)-1) + +// generic implementation of the hardware specific structure +// containing all the necessary current sense parameters +// will be returned as a void pointer from the _configureADCx functions +// will be provided to the _readADCVoltageX() as a void pointer +typedef struct GenericCurrentSenseParams { + int pins[3]; + float adc_voltage_conv; +} GenericCurrentSenseParams; + + +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + * @param cs_params -current sense parameter structure - hardware specific + */ +float _readADCVoltageInline(const int pinA, const void* cs_params); + +/** + * function reading an ADC value and returning the read voltage + * + * @param driver_params - driver parameter structure - hardware specific + * @param pinA - adc pin A + * @param pinB - adc pin B + * @param pinC - adc pin C + */ +void* _configureADCInline(const void *driver_params, const int pinA,const int pinB,const int pinC = NOT_SET); + +/** + * function reading an ADC value and returning the read voltage + * + * @param driver_params - driver parameter structure - hardware specific + * @param pinA - adc pin A + * @param pinB - adc pin B + * @param pinC - adc pin C + */ +void* _configureADCLowSide(const void *driver_params, const int pinA,const int pinB,const int pinC = NOT_SET); + +void _startADC3PinConversionLowSide(); + +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + * @param cs_params -current sense parameter structure - hardware specific + */ +float _readADCVoltageLowSide(const int pinA, const void* cs_params); + +/** + * function syncing the Driver with the ADC for the LowSide Sensing + * @param driver_params - driver parameter structure - hardware specific + * @param cs_params - current sense parameter structure - hardware specific + */ +void _driverSyncLowSide(void* driver_params, void* cs_params); + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp new file mode 100644 index 0000000..50ae596 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp @@ -0,0 +1,40 @@ +#include "../hardware_api.h" + +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) + +#define _ADC_VOLTAGE 5.0f +#define _ADC_RESOLUTION 1024.0f + +#ifndef cbi + #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) +#endif +#ifndef sbi + #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) +#endif + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + // set hight frequency adc - ADPS2,ADPS1,ADPS0 | 001 (16mhz/2) | 010 ( 16mhz/4 ) | 011 (16mhz/8) | 100(16mhz/16) | 101 (16mhz/32) | 110 (16mhz/64) | 111 (16mhz/128 default) + // set divisor to 8 - adc frequency 16mhz/8 = 2 mhz + // arduino takes 25 conversions per sample so - 2mhz/25 = 80k samples per second - 12.5us per sample + cbi(ADCSRA, ADPS2); + sbi(ADCSRA, ADPS1); + sbi(ADCSRA, ADPS0); + + return params; +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp new file mode 100644 index 0000000..ce58cd9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp @@ -0,0 +1,27 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(__SAM3X8E__) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp new file mode 100644 index 0000000..807c387 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp @@ -0,0 +1,259 @@ +#include "esp32_adc_driver.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "rom/ets_sys.h" +#include "esp_attr.h" +#include "esp_intr.h" +#include "soc/rtc_io_reg.h" +#include "soc/rtc_cntl_reg.h" +#include "soc/sens_reg.h" + +static uint8_t __analogAttenuation = 3;//11db +static uint8_t __analogWidth = 3;//12 bits +static uint8_t __analogCycles = 8; +static uint8_t __analogSamples = 0;//1 sample +static uint8_t __analogClockDiv = 1; + +// Width of returned answer () +static uint8_t __analogReturnedWidth = 12; + +void __analogSetWidth(uint8_t bits){ + if(bits < 9){ + bits = 9; + } else if(bits > 12){ + bits = 12; + } + __analogReturnedWidth = bits; + __analogWidth = bits - 9; + SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR1_BIT_WIDTH, __analogWidth, SENS_SAR1_BIT_WIDTH_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_BIT, __analogWidth, SENS_SAR1_SAMPLE_BIT_S); + + SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR2_BIT_WIDTH, __analogWidth, SENS_SAR2_BIT_WIDTH_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_BIT, __analogWidth, SENS_SAR2_SAMPLE_BIT_S); +} + +void __analogSetCycles(uint8_t cycles){ + __analogCycles = cycles; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_CYCLE, __analogCycles, SENS_SAR1_SAMPLE_CYCLE_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_CYCLE, __analogCycles, SENS_SAR2_SAMPLE_CYCLE_S); +} + +void __analogSetSamples(uint8_t samples){ + if(!samples){ + return; + } + __analogSamples = samples - 1; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_NUM, __analogSamples, SENS_SAR1_SAMPLE_NUM_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_NUM, __analogSamples, SENS_SAR2_SAMPLE_NUM_S); +} + +void __analogSetClockDiv(uint8_t clockDiv){ + if(!clockDiv){ + return; + } + __analogClockDiv = clockDiv; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_CLK_DIV, __analogClockDiv, SENS_SAR1_CLK_DIV_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_CLK_DIV, __analogClockDiv, SENS_SAR2_CLK_DIV_S); +} + +void __analogSetAttenuation(uint8_t attenuation) +{ + __analogAttenuation = attenuation & 3; + uint32_t att_data = 0; + int i = 10; + while(i--){ + att_data |= __analogAttenuation << (i * 2); + } + WRITE_PERI_REG(SENS_SAR_ATTEN1_REG, att_data & 0xFFFF);//ADC1 has 8 channels + WRITE_PERI_REG(SENS_SAR_ATTEN2_REG, att_data); +} + +void IRAM_ATTR __analogInit(){ + static bool initialized = false; + if(initialized){ + return; + } + + __analogSetAttenuation(__analogAttenuation); + __analogSetCycles(__analogCycles); + __analogSetSamples(__analogSamples + 1);//in samples + __analogSetClockDiv(__analogClockDiv); + __analogSetWidth(__analogWidth + 9);//in bits + + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL_REG, SENS_SAR1_DATA_INV); + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); + + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_FORCE_M); //SAR ADC1 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD_FORCE_M); //SAR ADC1 pad enable bitmap is controlled by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_FORCE_M); //SAR ADC2 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD_FORCE_M); //SAR ADC2 pad enable bitmap is controlled by SW + + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR_M); //force XPD_SAR=0, use XPD_FSM + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_AMP, 0x2, SENS_FORCE_XPD_AMP_S); //force XPD_AMP=0 + + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_CTRL_REG, 0xfff << SENS_AMP_RST_FB_FSM_S); //clear FSM + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT1_REG, SENS_SAR_AMP_WAIT1, 0x1, SENS_SAR_AMP_WAIT1_S); + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT1_REG, SENS_SAR_AMP_WAIT2, 0x1, SENS_SAR_AMP_WAIT2_S); + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_SAR_AMP_WAIT3, 0x1, SENS_SAR_AMP_WAIT3_S); + while (GET_PERI_REG_BITS2(SENS_SAR_SLAVE_ADDR1_REG, 0x7, SENS_MEAS_STATUS_S) != 0); //wait det_fsm== + + initialized = true; +} + +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0 || attenuation > 3){ + return ; + } + __analogInit(); + if(channel > 7){ + SET_PERI_REG_BITS(SENS_SAR_ATTEN2_REG, 3, attenuation, ((channel - 10) * 2)); + } else { + SET_PERI_REG_BITS(SENS_SAR_ATTEN1_REG, 3, attenuation, (channel * 2)); + } +} + +bool IRAM_ATTR __adcAttachPin(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + int8_t pad = digitalPinToTouchChannel(pin); + if(pad >= 0){ + uint32_t touch = READ_PERI_REG(SENS_SAR_TOUCH_ENABLE_REG); + if(touch & (1 << pad)){ + touch &= ~((1 << (pad + SENS_TOUCH_PAD_OUTEN2_S)) + | (1 << (pad + SENS_TOUCH_PAD_OUTEN1_S)) + | (1 << (pad + SENS_TOUCH_PAD_WORKEN_S))); + WRITE_PERI_REG(SENS_SAR_TOUCH_ENABLE_REG, touch); + } + } else if(pin == 25){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC | RTC_IO_PDAC1_DAC_XPD_FORCE); //stop dac1 + } else if(pin == 26){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC2_REG, RTC_IO_PDAC2_XPD_DAC | RTC_IO_PDAC2_DAC_XPD_FORCE); //stop dac2 + } + + pinMode(pin, ANALOG); + + __analogInit(); + return true; +} + +bool IRAM_ATTR __adcStart(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + } + return true; +} + +bool IRAM_ATTR __adcBusy(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 7){ + return (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); + } + return (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); +} + +uint16_t IRAM_ATTR __adcEnd(uint8_t pin) +{ + + uint16_t value = 0; + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return 0;//not adc pin + } + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + +void __analogReadResolution(uint8_t bits) +{ + if(!bits || bits > 16){ + return; + } + __analogSetWidth(bits); // hadware from 9 to 12 + __analogReturnedWidth = bits; // software from 1 to 16 +} + +uint16_t IRAM_ATTR adcRead(uint8_t pin) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + __analogInit(); + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + } + + uint16_t value = 0; + + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h new file mode 100644 index 0000000..869c4dd --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h @@ -0,0 +1,88 @@ + + +#ifndef SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ +#define SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ + +#include "Arduino.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) +/* + * Get ADC value for pin + * */ +uint16_t adcRead(uint8_t pin); + +/* + * Set the resolution of analogRead return values. Default is 12 bits (range from 0 to 4096). + * If between 9 and 12, it will equal the set hardware resolution, else value will be shifted. + * Range is 1 - 16 + * + * Note: compatibility with Arduino SAM + */ +void __analogReadResolution(uint8_t bits); + +/* + * Sets the sample bits and read resolution + * Default is 12bit (0 - 4095) + * Range is 9 - 12 + * */ +void __analogSetWidth(uint8_t bits); + +/* + * Set number of cycles per sample + * Default is 8 and seems to do well + * Range is 1 - 255 + * */ +void __analogSetCycles(uint8_t cycles); + +/* + * Set number of samples in the range. + * Default is 1 + * Range is 1 - 255 + * This setting splits the range into + * "samples" pieces, which could look + * like the sensitivity has been multiplied + * that many times + * */ +void __analogSetSamples(uint8_t samples); + +/* + * Set the divider for the ADC clock. + * Default is 1 + * Range is 1 - 255 + * */ +void __analogSetClockDiv(uint8_t clockDiv); + +/* + * Set the attenuation for all channels + * Default is 11db + * */ +void __analogSetAttenuation(uint8_t attenuation); + +/* + * Set the attenuation for particular pin + * Default is 11db + * */ +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation); + +/* + * Attach pin to ADC (will also clear any other analog mode that could be on) + * */ +bool __adcAttachPin(uint8_t pin); + +/* + * Start ADC conversion on attached pin's bus + * */ +bool __adcStart(uint8_t pin); + +/* + * Check if conversion on the pin's ADC bus is currently running + * */ +bool __adcBusy(uint8_t pin); + +/* + * Get the result of the conversion (will wait if it have not finished) + * */ +uint16_t __adcEnd(uint8_t pin); + +#endif /* SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ */ +#endif /* ESP32 */ \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp new file mode 100644 index 0000000..f2d65f8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp @@ -0,0 +1,27 @@ +#include "../../hardware_api.h" +#include "../../../drivers/hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && !defined(SOC_MCPWM_SUPPORTED) + +#include "esp32_adc_driver.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4095.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp new file mode 100644 index 0000000..3df9dff --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp @@ -0,0 +1,162 @@ +#include "../../hardware_api.h" +#include "../../../drivers/hardware_api.h" +#include "../../../drivers/hardware_specific/esp32/esp32_driver_mcpwm.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + +#include "esp32_adc_driver.h" + +#include "driver/mcpwm.h" +#include "soc/mcpwm_reg.h" +#include "soc/mcpwm_struct.h" + +#include +#include + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4095.0f + + +typedef struct ESP32MCPWMCurrentSenseParams { + int pins[3]; + float adc_voltage_conv; + mcpwm_unit_t mcpwm_unit; + int buffer_index; +} ESP32MCPWMCurrentSenseParams; + + +/** + * Inline adc reading implementation +*/ +// function reading an ADC value and returning the read voltage +float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = adcRead(pinA); + return raw_adc * ((ESP32MCPWMCurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA, const int pinB, const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + ESP32MCPWMCurrentSenseParams* params = new ESP32MCPWMCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + + + +/** + * Low side adc reading implementation +*/ + +static void IRAM_ATTR mcpwm0_isr_handler(void*); +static void IRAM_ATTR mcpwm1_isr_handler(void*); +byte currentState = 1; +// two mcpwm units +// - max 2 motors per mcpwm unit (6 adc channels) +int adc_pins[2][6]={0}; +int adc_pin_count[2]={0}; +uint32_t adc_buffer[2][6]={0}; +int adc_read_index[2]={0}; + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + mcpwm_unit_t unit = ((ESP32MCPWMCurrentSenseParams*)cs_params)->mcpwm_unit; + int buffer_index = ((ESP32MCPWMCurrentSenseParams*)cs_params)->buffer_index; + float adc_voltage_conv = ((ESP32MCPWMCurrentSenseParams*)cs_params)->adc_voltage_conv; + + for(int i=0; i < adc_pin_count[unit]; i++){ + if( pin == ((ESP32MCPWMCurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_buffer[unit][buffer_index + i] * adc_voltage_conv; + } + // not found + return 0; +} + +// function configuring low-side current sensing +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + + mcpwm_unit_t unit = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_unit; + int index_start = adc_pin_count[unit]; + if( _isset(pinA) ) adc_pins[unit][adc_pin_count[unit]++] = pinA; + if( _isset(pinB) ) adc_pins[unit][adc_pin_count[unit]++] = pinB; + if( _isset(pinC) ) adc_pins[unit][adc_pin_count[unit]++] = pinC; + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + ESP32MCPWMCurrentSenseParams* params = new ESP32MCPWMCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION), + .mcpwm_unit = unit, + .buffer_index = index_start + }; + + return params; +} + + +void _driverSyncLowSide(void* driver_params, void* cs_params){ + + mcpwm_dev_t* mcpwm_dev = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_dev; + mcpwm_unit_t mcpwm_unit = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_unit; + + // low-side register enable interrupt + mcpwm_dev->int_ena.timer0_tep_int_ena = true;//A PWM timer 0 TEP event will trigger this interrupt + // high side registers enable interrupt + //mcpwm_dev->int_ena.timer0_tep_int_ena = true;//A PWM timer 0 TEZ event will trigger this interrupt + + // register interrupts (mcpwm number, interrupt handler, handler argument = NULL, interrupt signal/flag, return handler = NULL) + if(mcpwm_unit == MCPWM_UNIT_0) + mcpwm_isr_register(mcpwm_unit, mcpwm0_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL); //Set ISR Handler + else + mcpwm_isr_register(mcpwm_unit, mcpwm1_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL); //Set ISR Handler +} + +// Read currents when interrupt is triggered +static void IRAM_ATTR mcpwm0_isr_handler(void*){ + // // high side + // uint32_t mcpwm_intr_status = MCPWM0.int_st.timer0_tez_int_st; + + // low side + uint32_t mcpwm_intr_status = MCPWM0.int_st.timer0_tep_int_st; + if(mcpwm_intr_status){ + adc_buffer[0][adc_read_index[0]] = adcRead(adc_pins[0][adc_read_index[0]]); + adc_read_index[0]++; + if(adc_read_index[0] == adc_pin_count[0]) adc_read_index[0] = 0; + } + // low side + MCPWM0.int_clr.timer0_tep_int_clr = mcpwm_intr_status; + // high side + // MCPWM0.int_clr.timer0_tez_int_clr = mcpwm_intr_status_0; +} + + +// Read currents when interrupt is triggered +static void IRAM_ATTR mcpwm1_isr_handler(void*){ + // // high side + // uint32_t mcpwm_intr_status = MCPWM1.int_st.timer0_tez_int_st; + + // low side + uint32_t mcpwm_intr_status = MCPWM1.int_st.timer0_tep_int_st; + if(mcpwm_intr_status){ + adc_buffer[1][adc_read_index[1]] = adcRead(adc_pins[1][adc_read_index[1]]); + adc_read_index[1]++; + if(adc_read_index[1] == adc_pin_count[1]) adc_read_index[1] = 0; + } + // low side + MCPWM1.int_clr.timer0_tep_int_clr = mcpwm_intr_status; + // high side + // MCPWM1.int_clr.timer0_tez_int_clr = mcpwm_intr_status_0; +} + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp new file mode 100644 index 0000000..a2f58ac --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp @@ -0,0 +1,258 @@ +#include "esp32_adc_driver.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "rom/ets_sys.h" +#include "esp_attr.h" +//#include "esp_intr.h" // deprecated +#include "esp_intr_alloc.h" +#include "soc/rtc_io_reg.h" +#include "soc/rtc_cntl_reg.h" +#include "soc/sens_reg.h" + +static uint8_t __analogAttenuation = 3;//11db +static uint8_t __analogWidth = 3;//12 bits +static uint8_t __analogCycles = 8; +static uint8_t __analogSamples = 0;//1 sample +static uint8_t __analogClockDiv = 1; + +// Width of returned answer () +static uint8_t __analogReturnedWidth = 12; + +void __analogSetWidth(uint8_t bits){ + if(bits < 9){ + bits = 9; + } else if(bits > 12){ + bits = 12; + } + __analogReturnedWidth = bits; + __analogWidth = bits - 9; + // SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR1_BIT_WIDTH, __analogWidth, SENS_SAR1_BIT_WIDTH_S); + // SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_BIT, __analogWidth, SENS_SAR1_SAMPLE_BIT_S); + + // SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR2_BIT_WIDTH, __analogWidth, SENS_SAR2_BIT_WIDTH_S); + // SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_BIT, __analogWidth, SENS_SAR2_SAMPLE_BIT_S); +} + +void __analogSetCycles(uint8_t cycles){ + __analogCycles = cycles; + // SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_CYCLE, __analogCycles, SENS_SAR1_SAMPLE_CYCLE_S); + // SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_CYCLE, __analogCycles, SENS_SAR2_SAMPLE_CYCLE_S); +} + +void __analogSetSamples(uint8_t samples){ + if(!samples){ + return; + } + __analogSamples = samples - 1; + SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_NUM, __analogSamples, SENS_SAR1_SAMPLE_NUM_S); + SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_NUM, __analogSamples, SENS_SAR2_SAMPLE_NUM_S); +} + +void __analogSetClockDiv(uint8_t clockDiv){ + if(!clockDiv){ + return; + } + __analogClockDiv = clockDiv; + SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_CLK_DIV, __analogClockDiv, SENS_SAR1_CLK_DIV_S); + SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_CLK_DIV, __analogClockDiv, SENS_SAR2_CLK_DIV_S); +} + +void __analogSetAttenuation(uint8_t attenuation) +{ + __analogAttenuation = attenuation & 3; + uint32_t att_data = 0; + int i = 10; + while(i--){ + att_data |= __analogAttenuation << (i * 2); + } + WRITE_PERI_REG(SENS_SAR_ATTEN1_REG, att_data & 0xFFFF);//ADC1 has 8 channels + WRITE_PERI_REG(SENS_SAR_ATTEN2_REG, att_data); +} + +void IRAM_ATTR __analogInit(){ + static bool initialized = false; + if(initialized){ + return; + } + + __analogSetAttenuation(__analogAttenuation); + __analogSetCycles(__analogCycles); + __analogSetSamples(__analogSamples + 1);//in samples + __analogSetClockDiv(__analogClockDiv); + __analogSetWidth(__analogWidth + 9);//in bits + + SET_PERI_REG_MASK(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_DATA_INV); + SET_PERI_REG_MASK(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_DATA_INV); + + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_FORCE_M); //SAR ADC1 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD_FORCE_M); //SAR ADC1 pad enable bitmap is controlled by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_FORCE_M); //SAR ADC2 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD_FORCE_M); //SAR ADC2 pad enable bitmap is controlled by SW + + CLEAR_PERI_REG_MASK(SENS_SAR_POWER_XPD_SAR_REG, SENS_FORCE_XPD_SAR_M); //force XPD_SAR=0, use XPD_FSM + SET_PERI_REG_BITS(SENS_SAR_POWER_XPD_SAR_REG, SENS_FORCE_XPD_AMP, 0x2, SENS_FORCE_XPD_AMP_S); //force XPD_AMP=0 + + CLEAR_PERI_REG_MASK(SENS_SAR_AMP_CTRL3_REG, 0xfff << SENS_AMP_RST_FB_FSM_S); //clear FSM + SET_PERI_REG_BITS(SENS_SAR_AMP_CTRL1_REG, SENS_SAR_AMP_WAIT1, 0x1, SENS_SAR_AMP_WAIT1_S); + SET_PERI_REG_BITS(SENS_SAR_AMP_CTRL1_REG, SENS_SAR_AMP_WAIT2, 0x1, SENS_SAR_AMP_WAIT2_S); + SET_PERI_REG_BITS(SENS_SAR_POWER_XPD_SAR_REG, SENS_SAR_AMP_WAIT3, 0x1, SENS_SAR_AMP_WAIT3_S); + while (GET_PERI_REG_BITS2(SENS_SAR_SLAVE_ADDR1_REG, 0x7, SENS_SARADC_MEAS_STATUS_S) != 0); //wait det_fsm== + + initialized = true; +} + +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0 || attenuation > 3){ + return ; + } + __analogInit(); + if(channel > 7){ + SET_PERI_REG_BITS(SENS_SAR_ATTEN2_REG, 3, attenuation, ((channel - 10) * 2)); + } else { + SET_PERI_REG_BITS(SENS_SAR_ATTEN1_REG, 3, attenuation, (channel * 2)); + } +} + +bool IRAM_ATTR __adcAttachPin(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + int8_t pad = digitalPinToTouchChannel(pin); + if(pad >= 0){ + uint32_t touch = READ_PERI_REG(SENS_SAR_TOUCH_CONF_REG); + if(touch & (1 << pad)){ + touch &= ~((1 << (pad + SENS_TOUCH_OUTEN_S))); + WRITE_PERI_REG(SENS_SAR_TOUCH_CONF_REG, touch); + } + } else if(pin == 25){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC | RTC_IO_PDAC1_DAC_XPD_FORCE); //stop dac1 + } else if(pin == 26){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC2_REG, RTC_IO_PDAC2_XPD_DAC | RTC_IO_PDAC2_DAC_XPD_FORCE); //stop dac2 + } + + pinMode(pin, ANALOG); + + __analogInit(); + return true; +} + +bool IRAM_ATTR __adcStart(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + } + return true; +} + +bool IRAM_ATTR __adcBusy(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 7){ + return (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); + } + return (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); +} + +uint16_t IRAM_ATTR __adcEnd(uint8_t pin) +{ + + uint16_t value = 0; + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return 0;//not adc pin + } + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + +void __analogReadResolution(uint8_t bits) +{ + if(!bits || bits > 16){ + return; + } + __analogSetWidth(bits); // hadware from 9 to 12 + __analogReturnedWidth = bits; // software from 1 to 16 +} + +uint16_t IRAM_ATTR adcRead(uint8_t pin) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + __analogInit(); + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + } + + uint16_t value = 0; + + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp new file mode 100644 index 0000000..dec7205 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp @@ -0,0 +1,41 @@ +#include "../hardware_api.h" + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = analogRead(pinA); + return raw_adc * ((GenericCurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (5.0f)/(1024.0f) + }; + + return params; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageLowSide(const int pinA, const void* cs_params){ + return _readADCVoltageInline(pinA, cs_params); +} + +// Configure low side for generic mcu +// cannot do much but +__attribute__((weak)) void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + return _configureADCInline(driver_params, pinA, pinB, pinC); +} + +// sync driver and the adc +__attribute__((weak)) void _driverSyncLowSide(void* driver_params, void* cs_params){ + _UNUSED(driver_params); + _UNUSED(cs_params); +} +__attribute__((weak)) void _startADC3PinConversionLowSide(){ } diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp new file mode 100644 index 0000000..d84c2fd --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp @@ -0,0 +1,265 @@ + +#if defined(TARGET_RP2040) + + +#include "../../hardware_api.h" +#include "./rp2040_mcu.h" +#include "../../../drivers/hardware_specific/rp2040/rp2040_mcu.h" +#include "communication/SimpleFOCDebug.h" + +#include "hardware/dma.h" +#include "hardware/irq.h" +#include "hardware/pwm.h" +#include "hardware/adc.h" + + +/* Singleton instance of the ADC engine */ +RP2040ADCEngine engine; + +alignas(32) const uint32_t trigger_value = ADC_CS_START_ONCE_BITS; // start once + +/* Hardware API implementation */ + +float _readADCVoltageInline(const int pinA, const void* cs_params) { + // not super-happy with this. Here we have to return 1 phase current at a time, when actually we want to + // return readings from the same ADC conversion run. The ADC on RP2040 is anyway in round robin mode :-( + // like this we either have to block interrupts, or of course have the chance of reading across + // new ADC conversions, which probably won't improve the accuracy. + _UNUSED(cs_params); + + if (pinA>=26 && pinA<=29 && engine.channelsEnabled[pinA-26]) { + return engine.lastResults.raw[pinA-26]*engine.adc_conv; + } + + // otherwise return NaN + return NAN; +}; + + +void* _configureADCInline(const void *driver_params, const int pinA, const int pinB, const int pinC) { + _UNUSED(driver_params); + + if( _isset(pinA) ) + engine.addPin(pinA); + if( _isset(pinB) ) + engine.addPin(pinB); + if( _isset(pinC) ) + engine.addPin(pinC); + engine.init(); // TODO this has to happen later if we want to support more than one motor... + engine.start(); + return &engine; +}; + + +// not supported at the moment +// void* _configureADCLowSide(const void *driver_params, const int pinA, const int pinB, const int pinC) { +// if( _isset(pinA) ) +// engine.addPin(pinA); +// if( _isset(pinB) ) +// engine.addPin(pinB); +// if( _isset(pinC) ) +// engine.addPin(pinC); +// engine.setPWMTrigger(((RP2040DriverParams*)driver_params)->slice[0]); +// engine.init(); +// engine.start(); +// return &engine; +// }; + + +// void _startADC3PinConversionLowSide() { +// // what is this for? +// }; + + +// float _readADCVoltageLowSide(const int pinA, const void* cs_params) { +// // not super-happy with this. Here we have to return 1 phase current at a time, when actually we want to +// // return readings from the same ADC conversion run. The ADC on RP2040 is anyway in round robin mode :-( +// // like this we have block interrupts 3x instead of just once, and of course have the chance of reading across +// // new ADC conversions, which probably won't improve the accuracy. + +// if (pinA>=26 && pinA<=29 && engine.channelsEnabled[pinA-26]) { +// return engine.lastResults[pinA-26]*engine.adc_conv; +// } + +// // otherwise return NaN +// return NAN; +// }; + + +// void _driverSyncLowSide(void* driver_params, void* cs_params) { +// // nothing to do +// }; + + + +volatile int rp2040_intcount = 0; + +void _adcConversionFinishedHandler() { + // conversion of all channels finished. copy results. + volatile uint8_t* from = engine.samples; + if (engine.channelsEnabled[0]) + engine.lastResults.raw[0] = (*from++); + if (engine.channelsEnabled[1]) + engine.lastResults.raw[1] = (*from++); + if (engine.channelsEnabled[2]) + engine.lastResults.raw[2] = (*from++); + if (engine.channelsEnabled[3]) + engine.lastResults.raw[3] = (*from++); + //dma_channel_acknowledge_irq0(engine.readDMAChannel); + dma_hw->ints0 = 1u << engine.readDMAChannel; + //dma_start_channel_mask( (1u << engine.readDMAChannel) ); + dma_channel_set_write_addr(engine.readDMAChannel, engine.samples, true); + // if (engine.triggerPWMSlice>=0) + // dma_channel_set_trans_count(engine.triggerDMAChannel, 1, true); + rp2040_intcount++; +}; + + + +/* ADC engine implementation */ + + +RP2040ADCEngine::RP2040ADCEngine() { + channelsEnabled[0] = false; + channelsEnabled[1] = false; + channelsEnabled[2] = false; + channelsEnabled[3] = false; + initialized = false; +}; + + + +void RP2040ADCEngine::addPin(int pin) { + if (pin>=26 && pin<=29) + channelsEnabled[pin-26] = true; + else + SIMPLEFOC_DEBUG("RP2040-CUR: ERR: Not an ADC pin: ", pin); +}; + + + +// void RP2040ADCEngine::setPWMTrigger(uint slice){ +// triggerPWMSlice = slice; +// }; + + + + +bool RP2040ADCEngine::init() { + if (initialized) + return true; + + adc_init(); + int enableMask = 0x00; + int channelCount = 0; + for (int i = 3; i>=0; i--) { + if (channelsEnabled[i]){ + adc_gpio_init(i+26); + enableMask |= (0x01<=500000) { + samples_per_second = 0; + adc_set_clkdiv(0); + } + else + adc_set_clkdiv(48000000/samples_per_second); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC init"); + + readDMAChannel = dma_claim_unused_channel(true); + dma_channel_config cc1 = dma_channel_get_default_config(readDMAChannel); + channel_config_set_transfer_data_size(&cc1, DMA_SIZE_8); + channel_config_set_read_increment(&cc1, false); + channel_config_set_write_increment(&cc1, true); + channel_config_set_dreq(&cc1, DREQ_ADC); + channel_config_set_irq_quiet(&cc1, false); + dma_channel_configure(readDMAChannel, + &cc1, + samples, // dest + &adc_hw->fifo, // source + channelCount, // count + false // defer start + ); + dma_channel_set_irq0_enabled(readDMAChannel, true); + irq_add_shared_handler(DMA_IRQ_0, _adcConversionFinishedHandler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); + + SIMPLEFOC_DEBUG("RP2040-CUR: DMA init"); + + // if (triggerPWMSlice>=0) { // if we have a trigger + // triggerDMAChannel = dma_claim_unused_channel(true); + // dma_channel_config cc3 = dma_channel_get_default_config(triggerDMAChannel); + // channel_config_set_transfer_data_size(&cc3, DMA_SIZE_32); + // channel_config_set_read_increment(&cc3, false); + // channel_config_set_write_increment(&cc3, false); + // channel_config_set_irq_quiet(&cc3, true); + // channel_config_set_dreq(&cc3, DREQ_PWM_WRAP0+triggerPWMSlice); //pwm_get_dreq(triggerPWMSlice)); + // pwm_set_irq_enabled(triggerPWMSlice, true); + // dma_channel_configure(triggerDMAChannel, + // &cc3, + // hw_set_alias_untyped(&adc_hw->cs), // dest + // &trigger_value, // source + // 1, // count + // true // defer start + // ); + // SIMPLEFOC_DEBUG("RP2040-CUR: PWM trigger init slice ", triggerPWMSlice); + // } + + initialized = true; + return initialized; +}; + + + + +void RP2040ADCEngine::start() { + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine starting"); + irq_set_enabled(DMA_IRQ_0, true); + dma_start_channel_mask( (1u << readDMAChannel) ); + for (int i=0;i<4;i++) { + if (channelsEnabled[i]) { + adc_select_input(i); // set input to first enabled channel + break; + } + } + // if (triggerPWMSlice>=0) { + // dma_start_channel_mask( (1u << triggerDMAChannel) ); + // //hw_set_bits(&adc_hw->cs, trigger_value); + // } + // else + adc_run(true); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine started"); +}; + + + + +void RP2040ADCEngine::stop() { + adc_run(false); + irq_set_enabled(DMA_IRQ_0, false); + dma_channel_abort(readDMAChannel); + // if (triggerPWMSlice>=0) + // dma_channel_abort(triggerDMAChannel); + adc_fifo_drain(); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine stopped"); +}; + + + +ADCResults RP2040ADCEngine::getLastResults() { + ADCResults r; + r.value = lastResults.value; + return r; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h new file mode 100644 index 0000000..ae28bb2 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h @@ -0,0 +1,93 @@ + + +#pragma once + +/* + * RP2040 ADC features are very weak :-( + * - only 4 inputs + * - only 9 bit effective resolution + * - read only 1 input at a time + * - 2 microseconds conversion time! + * - no triggers from PWM / events, only DMA + * + * So to read 3 phases takes 6 microseconds. :-( + * + * The RP2040 ADC engine takes over the control of the MCU's ADC. Parallel ADC access is not permitted, as this would + * cause conflicts with the engine's DMA based access and cause crashes. + * To use the other ADC channels, use them via this engine. Use addPin() to add them to the conversion, and getLastResult() + * to retrieve their value at any time. + * + * For motor current sensing, the engine supports inline sensing only. + * + * Inline sensing is supported by offering a user-selectable fixed ADC sampling rate, which can be set between 500kHz and 1Hz. + * After starting the engine it will continuously sample and provide new values at the configured rate. + * + * The default sampling rate is 20kHz, which is suitable for 2 channels assuming you a 5kHz main loop speed (a new measurement is used per + * main loop iteration). + * + * Low-side sensing is currently not supported. + * + * The SimpleFOC PWM driver for RP2040 syncs all the slices, so the PWM trigger is applied to the first used slice. For current + * sensing to work correctly, all PWM slices have to be set to the same PWM frequency. + * In theory, two motors could be sensed using 2 shunts on each motor. + * + * Note that if using other ADC channels along with the motor current sensing, those channels will be subject to the same conversion schedule as the motor's ADC channels, i.e. convert at the same fixed rate in case + * of inline sensing. + * + * Solution to trigger ADC conversion from PWM via DMA: + * use the PWM wrap as a DREQ to a DMA channel, and have the DMA channel write to the ADC's CS register to trigger an ADC sample. + * Unfortunately, I could not get this to work, so no low side sensing for the moment. + * + * Solution for ADC conversion: + * ADC converts all channels in round-robin mode, and writes to FIFO. FIFO is emptied by a DMA which triggers after N conversions, + * where N is the number of ADC channels used. So this DMA copies all the values from one round-robin conversion. + * + * + */ + + +#define SIMPLEFOC_RP2040_ADC_RESOLUTION 256 +#ifndef SIMPLEFOC_RP2040_ADC_VDDA +#define SIMPLEFOC_RP2040_ADC_VDDA 3.3f +#endif + + +union ADCResults { + uint32_t value; + uint8_t raw[4]; + struct { + uint8_t ch0; + uint8_t ch1; + uint8_t ch2; + uint8_t ch3; + }; +}; + + +class RP2040ADCEngine { + +public: + RP2040ADCEngine(); + void addPin(int pin); + //void setPWMTrigger(uint slice); + + bool init(); + void start(); + void stop(); + + ADCResults getLastResults(); // TODO find a better API and representation for this + + int samples_per_second = 20000; // 20kHz default (assuming 2 shunts and 5kHz loop speed), set to 0 to convert in tight loop + float adc_conv = (SIMPLEFOC_RP2040_ADC_VDDA / SIMPLEFOC_RP2040_ADC_RESOLUTION); // conversion from raw ADC to float + + //int triggerPWMSlice = -1; + bool initialized; + uint readDMAChannel; + //uint copyDMAChannel; + //uint triggerDMAChannel; + + bool channelsEnabled[4]; + volatile uint8_t samples[4]; + volatile ADCResults lastResults; + //alignas(32) volatile uint8_t nextResults[4]; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp new file mode 100644 index 0000000..da5ba85 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp @@ -0,0 +1,318 @@ +#ifdef _SAMD21_ + +#include "samd21_mcu.h" +#include "../../hardware_api.h" + + +static bool freeRunning = false; +static int _pinA, _pinB, _pinC; +static uint16_t a = 0xFFFF, b = 0xFFFF, c = 0xFFFF; // updated by adcStopWithDMA when configured in freerunning mode +static SAMDCurrentSenseADCDMA instance; + +// function configuring low-side current sensing +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC) +{ + _UNUSED(driver_params); + + _pinA = pinA; + _pinB = pinB; + _pinC = pinC; + freeRunning = true; + instance.init(pinA, pinB, pinC); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC } + }; + + return params; +} +void _startADC3PinConversionLowSide() +{ + instance.startADCScan(); +} +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + */ +float _readADCVoltageLowSide(const int pinA, const void* cs_params) +{ + _UNUSED(cs_params); + + instance.readResults(a, b, c); + + if(pinA == _pinA) + return instance.toVolts(a); + if(pinA == _pinB) + return instance.toVolts(b); + if(pinA == _pinC) + return instance.toVolts(c); + + return NAN; +} + +/** + * function syncing the Driver with the ADC for the LowSide Sensing + */ +void _driverSyncLowSide(void* driver_params, void* cs_params) +{ + _UNUSED(driver_params); + _UNUSED(cs_params); + + SIMPLEFOC_SAMD_DEBUG_SERIAL.println(F("TODO! _driverSyncLowSide() is not implemented")); + instance.startADCScan(); + //TODO: hook with PWM interrupts +} + + + + + + + + + + + // Credit: significant portions of this code were pulled from Paul Gould's git https://github.com/gouldpa/FOC-Arduino-Brushless + +static void adcStopWithDMA(void); +static void adcStartWithDMA(void); + +/** + * @brief ADC sync wait + * @retval void + */ +static __inline__ void ADCsync() __attribute__((always_inline, unused)); +static void ADCsync() { + while (ADC->STATUS.bit.SYNCBUSY == 1); //Just wait till the ADC is free +} + +// ADC DMA sequential free running (6) with Interrupts ///////////////// + +SAMDCurrentSenseADCDMA * SAMDCurrentSenseADCDMA::getHardwareAPIInstance() +{ + + return &instance; +} + +SAMDCurrentSenseADCDMA::SAMDCurrentSenseADCDMA() +{ +} + +void SAMDCurrentSenseADCDMA::init(int pinA, int pinB, int pinC, int pinAREF, float voltageAREF, uint32_t adcBits, uint32_t channelDMA) +{ + this->pinA = pinA; + this->pinB = pinB; + this->pinC = pinC; + this->pinAREF = pinAREF; + this->channelDMA = channelDMA; + this->voltageAREF = voltageAREF; + this->maxCountsADC = 1 << adcBits; + countsToVolts = ( voltageAREF / maxCountsADC ); + + initPins(); + initADC(); + initDMA(); + startADCScan(); //so we have something to read next time we call readResults() +} + + +void SAMDCurrentSenseADCDMA::startADCScan(){ + adcToDMATransfer(adcBuffer + oneBeforeFirstAIN, BufferSize); + adcStartWithDMA(); +} + +bool SAMDCurrentSenseADCDMA::readResults(uint16_t & a, uint16_t & b, uint16_t & c){ + if(ADC->CTRLA.bit.ENABLE) + return false; + uint32_t ainA = g_APinDescription[pinA].ulADCChannelNumber; + uint32_t ainB = g_APinDescription[pinB].ulADCChannelNumber; + a = adcBuffer[ainA]; + b = adcBuffer[ainB]; + if(_isset(pinC)) + { + uint32_t ainC = g_APinDescription[pinC].ulADCChannelNumber; + c = adcBuffer[ainC]; + } + return true; +} + + +float SAMDCurrentSenseADCDMA::toVolts(uint16_t counts) { + return counts * countsToVolts; +} + +void SAMDCurrentSenseADCDMA::initPins(){ + + if (pinAREF>=0) + pinMode(pinAREF, INPUT); + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + + uint32_t ainA = g_APinDescription[pinA].ulADCChannelNumber; + uint32_t ainB = g_APinDescription[pinB].ulADCChannelNumber; + firstAIN = min(ainA, ainB); + lastAIN = max(ainA, ainB); + if( _isset(pinC) ) + { + uint32_t ainC = g_APinDescription[pinC].ulADCChannelNumber; + pinMode(pinC, INPUT); + firstAIN = min(firstAIN, ainC); + lastAIN = max(lastAIN, ainC); + } + + oneBeforeFirstAIN = firstAIN - 1; //hack to discard noisy first readout + BufferSize = lastAIN - oneBeforeFirstAIN + 1; + +} + +void SAMDCurrentSenseADCDMA::initADC(){ + + analogRead(pinA); // do some pin init pinPeripheral() + analogRead(pinB); // do some pin init pinPeripheral() + analogRead(pinC); // do some pin init pinPeripheral() + + ADC->CTRLA.bit.ENABLE = 0x00; // Disable ADC + ADCsync(); + //ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC0_Val; // 2.2297 V Supply VDDANA + ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_1X_Val; // Gain select as 1X + // ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_DIV2_Val; // default + if (pinAREF>=0) + ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_AREFA; + else + ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC0; + ADCsync(); // ref 31.6.16 + + /* + Bits 19:16 – INPUTSCAN[3:0]: Number of Input Channels Included in Scan + This register gives the number of input sources included in the pin scan. The number of input sources included is + INPUTSCAN + 1. The input channels included are in the range from MUXPOS + INPUTOFFSET to MUXPOS + + INPUTOFFSET + INPUTSCAN. + The range of the scan mode must not exceed the number of input channels available on the device. + Bits 4:0 – MUXPOS[4:0]: Positive Mux Input Selection + These bits define the Mux selection for the positive ADC input. Table 32-14 shows the possible input selections. If + the internal bandgap voltage or temperature sensor input channel is selected, then the Sampling Time Length bit + group in the SamplingControl register must be written. + Table 32-14. Positive Mux Input Selection + MUXPOS[4:0] Group configuration Description + 0x00 PIN0 ADC AIN0 pin + 0x01 PIN1 ADC AIN1 pin + 0x02 PIN2 ADC AIN2 pin + 0x03 PIN3 ADC AIN3 pin + 0x04 PIN4 ADC AIN4 pin + 0x05 PIN5 ADC AIN5 pin + 0x06 PIN6 ADC AIN6 pin + 0x07 PIN7 ADC AIN7 pin + 0x08 PIN8 ADC AIN8 pin + 0x09 PIN9 ADC AIN9 pin + 0x0A PIN10 ADC AIN10 pin + 0x0B PIN11 ADC AIN11 pin + 0x0C PIN12 ADC AIN12 pin + 0x0D PIN13 ADC AIN13 pin + 0x0E PIN14 ADC AIN14 pin + 0x0F PIN15 ADC AIN15 pin + 0x10 PIN16 ADC AIN16 pin + 0x11 PIN17 ADC AIN17 pin + 0x12 PIN18 ADC AIN18 pin + 0x13 PIN19 ADC AIN19 pin + 0x14-0x17 Reserved + 0x18 TEMP Temperature reference + 0x19 BANDGAP Bandgap voltage + 0x1A SCALEDCOREVCC 1/4 scaled core supply + 0x1B SCALEDIOVCC 1/4 scaled I/O supply + 0x1C DAC DAC output + 0x1D-0x1F Reserved + */ + ADC->INPUTCTRL.bit.MUXPOS = oneBeforeFirstAIN; + ADCsync(); + ADC->INPUTCTRL.bit.INPUTSCAN = lastAIN; // so the adc will scan from oneBeforeFirstAIN to lastAIN (inclusive) + ADCsync(); + ADC->INPUTCTRL.bit.INPUTOFFSET = 0; //input scan cursor + ADCsync(); + ADC->AVGCTRL.reg = 0x00 ; //no averaging + ADC->SAMPCTRL.reg = 0x05; ; //sample length in 1/2 CLK_ADC cycles, see GCLK_ADC and ADC_CTRLB_PRESCALER_DIV16 + // according to the specsheet: f_GCLK_ADC ADC input clock frequency 48 MHz, so same as fCPU + ADCsync(); + ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV16 | ADC_CTRLB_FREERUN | ADC_CTRLB_RESSEL_12BIT; + ADCsync(); +} + +volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16))); + +void SAMDCurrentSenseADCDMA::initDMA() { + // probably on by default + PM->AHBMASK.reg |= PM_AHBMASK_DMAC ; + PM->APBBMASK.reg |= PM_APBBMASK_DMAC ; + NVIC_EnableIRQ( DMAC_IRQn ) ; + DMAC->BASEADDR.reg = (uint32_t)descriptor_section; + DMAC->WRBADDR.reg = (uint32_t)wrb; + DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); +} + + +void SAMDCurrentSenseADCDMA::adcToDMATransfer(void *rxdata, uint32_t hwords) { + + DMAC->CHID.reg = DMAC_CHID_ID(channelDMA); + DMAC->CHCTRLA.reg &= ~DMAC_CHCTRLA_ENABLE; + DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST; + DMAC->SWTRIGCTRL.reg &= (uint32_t)(~(1 << channelDMA)); + + DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) + | DMAC_CHCTRLB_TRIGSRC(ADC_DMAC_ID_RESRDY) + | DMAC_CHCTRLB_TRIGACT_BEAT; + DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK ; // enable all 3 interrupts + descriptor.descaddr = 0; + descriptor.srcaddr = (uint32_t) &ADC->RESULT.reg; + descriptor.btcnt = hwords; + descriptor.dstaddr = (uint32_t)rxdata + hwords*2; // end address + descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID; + memcpy(&descriptor_section[channelDMA],&descriptor, sizeof(dmacdescriptor)); + + // start channel + DMAC->CHID.reg = DMAC_CHID_ID(channelDMA); + DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; +} + + +int iii = 0; + +void adcStopWithDMA(void){ + ADCsync(); + ADC->CTRLA.bit.ENABLE = 0x00; + // ADCsync(); + // if(iii++ % 1000 == 0) + // { + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(a); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(" :: "); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(b); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(" :: "); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(c); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.println("yo!"); + // } + + +} + +void adcStartWithDMA(void){ + ADCsync(); + ADC->INPUTCTRL.bit.INPUTOFFSET = 0; + ADCsync(); + ADC->SWTRIG.bit.FLUSH = 1; + ADCsync(); + ADC->CTRLA.bit.ENABLE = 0x01; + ADCsync(); +} + +void DMAC_Handler() { + uint8_t active_channel; + active_channel = DMAC->INTPEND.reg & DMAC_INTPEND_ID_Msk; // get channel number + DMAC->CHID.reg = DMAC_CHID_ID(active_channel); + adcStopWithDMA(); + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL; // clear + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR; + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP; + +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h new file mode 100644 index 0000000..e7d7442 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h @@ -0,0 +1,69 @@ +#ifdef _SAMD21_ + +#ifndef CURRENT_SENSE_SAMD21_H +#define CURRENT_SENSE_SAMD21_H + +#define SIMPLEFOC_SAMD_DEBUG +#if !defined(SIMPLEFOC_SAMD_DEBUG_SERIAL) +#define SIMPLEFOC_SAMD_DEBUG_SERIAL Serial +#endif + +#include + typedef struct { + uint16_t btctrl; + uint16_t btcnt; + uint32_t srcaddr; + uint32_t dstaddr; + uint32_t descaddr; + } dmacdescriptor ; + + +// AREF pin is 42 + +class SAMDCurrentSenseADCDMA +{ + +public: + static SAMDCurrentSenseADCDMA * getHardwareAPIInstance(); + SAMDCurrentSenseADCDMA(); + void init(int pinA, int pinB, int pinC, int pinAREF = 42, float voltageAREF = 3.3, uint32_t adcBits = 12, uint32_t channelDMA = 3); + void startADCScan(); + bool readResults(uint16_t & a, uint16_t & b, uint16_t & c); + float toVolts(uint16_t counts); +private: + + void adcToDMATransfer(void *rxdata, uint32_t hwords); + + void initPins(); + void initADC(); + void initDMA(); + + uint32_t oneBeforeFirstAIN; // hack to discard first noisy readout + uint32_t firstAIN; + uint32_t lastAIN; + uint32_t BufferSize = 0; + + uint16_t adcBuffer[20]; + + + uint32_t pinA; + uint32_t pinB; + uint32_t pinC; + uint32_t pinAREF; + uint32_t channelDMA; // DMA channel + bool freeRunning; + + float voltageAREF; + float maxCountsADC; + float countsToVolts; + + dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16))); + dmacdescriptor descriptor __attribute__ ((aligned (16))); + +}; + +#endif + + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp new file mode 100644 index 0000000..2ec47c0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp @@ -0,0 +1,23 @@ +#include "../../hardware_api.h" + +#if defined(_SAMD21_)|| defined(_SAMD51_) || defined(_SAME51_) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp new file mode 100644 index 0000000..dc505d6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp @@ -0,0 +1,377 @@ +#include "../../../hardware_api.h" +#if defined(ARDUINO_B_G431B_ESC1) + +#include "communication/SimpleFOCDebug.h" + +#include "stm32g4xx_hal.h" +#include "stm32g4xx_ll_pwr.h" +#include "stm32g4xx_hal_adc.h" +#include "b_g431_hal.h" + +// From STM32 cube IDE +/** + ****************************************************************************** + * @attention + * + *

© Copyright (c) 2020 STMicroelectronics. + * All rights reserved.

+ * + * This software component is licensed by ST under BSD 3-Clause license, + * the "License"; You may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * opensource.org/licenses/BSD-3-Clause + * + ****************************************************************************** + */ + + +/** + * @brief GPIO Initialization Function + * @param None + * @retval None + */ +void MX_GPIO_Init(void) +{ + /* GPIO Ports Clock Enable */ + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOF_CLK_ENABLE(); + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + + __HAL_RCC_ADC12_CLK_ENABLE(); +} + +/** + * Enable DMA controller clock + */ +void MX_DMA_Init(void) +{ + /* DMA controller clock enable */ + __HAL_RCC_DMAMUX1_CLK_ENABLE(); + __HAL_RCC_DMA1_CLK_ENABLE(); + __HAL_RCC_DMA2_CLK_ENABLE(); + + /* DMA interrupt init */ + /* DMA1_Channel1_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); + /* DMA1_Channel2_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); + + // Enable external clock for ADC + RCC_PeriphCLKInitTypeDef PeriphClkInit; + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC12; + PeriphClkInit.Adc12ClockSelection = RCC_ADC12CLKSOURCE_PLL; + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); +} + + +/** + * @brief ADC1 Initialization Function + * @param None + * @retval None + */ +void MX_ADC1_Init(ADC_HandleTypeDef* hadc1) +{ + /* USER CODE BEGIN ADC1_Init 0 */ + + /* USER CODE END ADC1_Init 0 */ + + ADC_MultiModeTypeDef multimode = {0}; + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC1_Init 1 */ + + /* USER CODE END ADC1_Init 1 */ + /** Common config + */ + hadc1->Instance = ADC1; + hadc1->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV16; + hadc1->Init.Resolution = ADC_RESOLUTION_12B; + hadc1->Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc1->Init.GainCompensation = 0; + hadc1->Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc1->Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc1->Init.LowPowerAutoWait = DISABLE; + hadc1->Init.ContinuousConvMode = DISABLE; + hadc1->Init.NbrOfConversion = 5; + hadc1->Init.DiscontinuousConvMode = DISABLE; + hadc1->Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO; + hadc1->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; + hadc1->Init.DMAContinuousRequests = ENABLE; + hadc1->Init.Overrun = ADC_OVR_DATA_PRESERVED; + + if (HAL_ADC_Init(hadc1) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_Init failed!"); + } + + /** Configure the ADC multi-mode + */ + multimode.Mode = ADC_MODE_INDEPENDENT; + if (HAL_ADCEx_MultiModeConfigChannel(hadc1, &multimode) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADCEx_MultiModeConfigChannel failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_12; // ADC1_IN12 = PB1 = OP3_OUT + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_3; // ADC1_IN3 = PA2 = OP1_OUT + sConfig.Rank = ADC_REGULAR_RANK_2; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + //****************************************************************** + // Temp, Poti .... + /* Configure Regular Channel (PB12, Potentiometer) + */ + sConfig.Channel = ADC_CHANNEL_11; + sConfig.Rank = ADC_REGULAR_RANK_3; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + /** Configure Regular Channel (PB14, Temperature) + */ + sConfig.Channel = ADC_CHANNEL_5; + sConfig.Rank = ADC_REGULAR_RANK_4; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + /** Configure Regular Channel (PB14, Temperature) + */ + sConfig.Channel = ADC_CHANNEL_1; + sConfig.Rank = ADC_REGULAR_RANK_5; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /* USER CODE BEGIN ADC1_Init 2 */ + + /* USER CODE END ADC1_Init 2 */ + +} + +/** + * @brief ADC2 Initialization Function + * @param None + * @retval None + */ +void MX_ADC2_Init(ADC_HandleTypeDef* hadc2) +{ + /* USER CODE BEGIN ADC2_Init 0 */ + + /* USER CODE END ADC2_Init 0 */ + + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC2_Init 1 */ + + /* USER CODE END ADC2_Init 1 */ + /** Common config + */ + hadc2->Instance = ADC2; + hadc2->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV16; + hadc2->Init.Resolution = ADC_RESOLUTION_12B; + hadc2->Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc2->Init.GainCompensation = 0; + hadc2->Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc2->Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc2->Init.LowPowerAutoWait = DISABLE; + hadc2->Init.ContinuousConvMode = DISABLE; + hadc2->Init.NbrOfConversion = 1; + hadc2->Init.DiscontinuousConvMode = DISABLE; + hadc2->Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO; + hadc2->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; + hadc2->Init.DMAContinuousRequests = ENABLE; + hadc2->Init.Overrun = ADC_OVR_DATA_PRESERVED; + + if (HAL_ADC_Init(hadc2) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_Init failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_3; // ADC2_IN3 = PA6 + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc2, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /* USER CODE BEGIN ADC2_Init 2 */ + + /* USER CODE END ADC2_Init 2 */ + +} + +/** +* @brief OPAMP MSP Initialization +* This function configures the hardware resources used in this example +* @param hopamp-> OPAMP handle pointer +* @retval None +*/ +void HAL_OPAMP_MspInit(OPAMP_HandleTypeDef* hopamp) +{ + GPIO_InitTypeDef GPIO_InitStruct = {0}; + if(hopamp->Instance==OPAMP1) + { + /* USER CODE BEGIN OPAMP1_MspInit 0 */ + + /* USER CODE END OPAMP1_MspInit 0 */ + + __HAL_RCC_GPIOA_CLK_ENABLE(); + /**OPAMP1 GPIO Configuration + PA1 ------> OPAMP1_VINP + PA2 ------> OPAMP1_VOUT + PA3 ------> OPAMP1_VINM + */ + GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP1_MspInit 1 */ + + /* USER CODE END OPAMP1_MspInit 1 */ + } + else if(hopamp->Instance==OPAMP2) + { + /* USER CODE BEGIN OPAMP2_MspInit 0 */ + + /* USER CODE END OPAMP2_MspInit 0 */ + + __HAL_RCC_GPIOA_CLK_ENABLE(); + /**OPAMP2 GPIO Configuration + PA5 ------> OPAMP2_VINM + PA6 ------> OPAMP2_VOUT + PA7 ------> OPAMP2_VINP + */ + GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP2_MspInit 1 */ + + /* USER CODE END OPAMP2_MspInit 1 */ + } + else if(hopamp->Instance==OPAMP3) + { + /* USER CODE BEGIN OPAMP3_MspInit 0 */ + + /* USER CODE END OPAMP3_MspInit 0 */ + + __HAL_RCC_GPIOB_CLK_ENABLE(); + /**OPAMP3 GPIO Configuration + PB0 ------> OPAMP3_VINP + PB1 ------> OPAMP3_VOUT + PB2 ------> OPAMP3_VINM + */ + GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP3_MspInit 1 */ + + /* USER CODE END OPAMP3_MspInit 1 */ + } + +} + +/** +* @brief OPAMP MSP De-Initialization +* This function freeze the hardware resources used in this example +* @param hopamp-> OPAMP handle pointer +* @retval None +*/ +void HAL_OPAMP_MspDeInit(OPAMP_HandleTypeDef* hopamp) +{ + if(hopamp->Instance==OPAMP1) + { + /* USER CODE BEGIN OPAMP1_MspDeInit 0 */ + + /* USER CODE END OPAMP1_MspDeInit 0 */ + + /**OPAMP1 GPIO Configuration + PA1 ------> OPAMP1_VINP + PA2 ------> OPAMP1_VOUT + PA3 ------> OPAMP1_VINM + */ + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3); + + /* USER CODE BEGIN OPAMP1_MspDeInit 1 */ + + /* USER CODE END OPAMP1_MspDeInit 1 */ + } + else if(hopamp->Instance==OPAMP2) + { + /* USER CODE BEGIN OPAMP2_MspDeInit 0 */ + + /* USER CODE END OPAMP2_MspDeInit 0 */ + + /**OPAMP2 GPIO Configuration + PA5 ------> OPAMP2_VINM + PA6 ------> OPAMP2_VOUT + PA7 ------> OPAMP2_VINP + */ + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7); + + /* USER CODE BEGIN OPAMP2_MspDeInit 1 */ + + /* USER CODE END OPAMP2_MspDeInit 1 */ + } + else if(hopamp->Instance==OPAMP3) + { + /* USER CODE BEGIN OPAMP3_MspDeInit 0 */ + + /* USER CODE END OPAMP3_MspDeInit 0 */ + + /**OPAMP3 GPIO Configuration + PB0 ------> OPAMP3_VINP + PB1 ------> OPAMP3_VOUT + PB2 ------> OPAMP3_VINM + */ + HAL_GPIO_DeInit(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2); + + /* USER CODE BEGIN OPAMP3_MspDeInit 1 */ + + /* USER CODE END OPAMP3_MspDeInit 1 */ + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h new file mode 100644 index 0000000..2d6a1f0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h @@ -0,0 +1,18 @@ +#ifndef B_G431_ESC1_HAL +#define B_G431_ESC1_HAL + +#if defined(ARDUINO_B_G431B_ESC1) + +#include +#include + +void MX_GPIO_Init(void); +void MX_DMA_Init(void); +void MX_ADC1_Init(ADC_HandleTypeDef* hadc1); +void MX_ADC2_Init(ADC_HandleTypeDef* hadc2); +void MX_OPAMP1_Init(OPAMP_HandleTypeDef* hopamp); +void MX_OPAMP2_Init(OPAMP_HandleTypeDef* hopamp); +void MX_OPAMP3_Init(OPAMP_HandleTypeDef* hopamp); +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp new file mode 100644 index 0000000..3e10bbc --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp @@ -0,0 +1,171 @@ +#include "../../../hardware_api.h" + +#if defined(ARDUINO_B_G431B_ESC1) + +#include "b_g431_hal.h" +#include "Arduino.h" +#include "../stm32_mcu.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "communication/SimpleFOCDebug.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4096.0f +#define ADC_BUF_LEN_1 5 +#define ADC_BUF_LEN_2 1 + +static ADC_HandleTypeDef hadc1; +static ADC_HandleTypeDef hadc2; +static OPAMP_HandleTypeDef hopamp1; +static OPAMP_HandleTypeDef hopamp2; +static OPAMP_HandleTypeDef hopamp3; + +static DMA_HandleTypeDef hdma_adc1; +static DMA_HandleTypeDef hdma_adc2; + +volatile uint16_t adcBuffer1[ADC_BUF_LEN_1] = {0}; // Buffer for store the results of the ADC conversion +volatile uint16_t adcBuffer2[ADC_BUF_LEN_2] = {0}; // Buffer for store the results of the ADC conversion + +// function reading an ADC value and returning the read voltage +// As DMA is being used just return the DMA result +float _readADCVoltageInline(const int pin, const void* cs_params){ + uint32_t raw_adc = 0; + if(pin == PA2) // = ADC1_IN3 = phase U (OP1_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer1[1]; + else if(pin == PA6) // = ADC2_IN3 = phase V (OP2_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer2[0]; +#ifdef PB1 + else if(pin == PB1) // = ADC1_IN12 = phase W (OP3_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer1[0]; +#endif + + else if (pin == A_POTENTIOMETER) + raw_adc = adcBuffer1[2]; + else if (pin == A_TEMPERATURE) + raw_adc = adcBuffer1[3]; + else if (pin == A_VBUS) + raw_adc = adcBuffer1[4]; + + return raw_adc * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +void _configureOPAMP(OPAMP_HandleTypeDef *hopamp, OPAMP_TypeDef *OPAMPx_Def){ + // could this be replaced with LL_OPAMP calls?? + hopamp->Instance = OPAMPx_Def; + hopamp->Init.PowerMode = OPAMP_POWERMODE_HIGHSPEED; + hopamp->Init.Mode = OPAMP_PGA_MODE; + hopamp->Init.NonInvertingInput = OPAMP_NONINVERTINGINPUT_IO0; + hopamp->Init.InternalOutput = DISABLE; + hopamp->Init.TimerControlledMuxmode = OPAMP_TIMERCONTROLLEDMUXMODE_DISABLE; + hopamp->Init.PgaConnect = OPAMP_PGA_CONNECT_INVERTINGINPUT_IO0_BIAS; + hopamp->Init.PgaGain = OPAMP_PGA_GAIN_16_OR_MINUS_15; + hopamp->Init.UserTrimming = OPAMP_TRIMMING_FACTORY; + if (HAL_OPAMP_Init(hopamp) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_OPAMP_Init failed!"); + } +} +void _configureOPAMPs(OPAMP_HandleTypeDef *OPAMPA, OPAMP_HandleTypeDef *OPAMPB, OPAMP_HandleTypeDef *OPAMPC){ + // Configure the opamps + _configureOPAMP(OPAMPA, OPAMP1); + _configureOPAMP(OPAMPB, OPAMP2); + _configureOPAMP(OPAMPC, OPAMP3); +} + +void MX_DMA1_Init(ADC_HandleTypeDef *hadc, DMA_HandleTypeDef *hdma_adc, DMA_Channel_TypeDef* channel, uint32_t request) { + hdma_adc->Instance = channel; + hdma_adc->Init.Request = request; + hdma_adc->Init.Direction = DMA_PERIPH_TO_MEMORY; + hdma_adc->Init.PeriphInc = DMA_PINC_DISABLE; + hdma_adc->Init.MemInc = DMA_MINC_ENABLE; + hdma_adc->Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_adc->Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + hdma_adc->Init.Mode = DMA_CIRCULAR; + hdma_adc->Init.Priority = DMA_PRIORITY_LOW; + HAL_DMA_DeInit(hdma_adc); + if (HAL_DMA_Init(hdma_adc) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_DMA_Init failed!"); + } + __HAL_LINKDMA(hadc, DMA_Handle, *hdma_adc); +} + +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + _UNUSED(pinA); + _UNUSED(pinB); + _UNUSED(pinC); + + SIMPLEFOC_DEBUG("B-G431B does not implement inline current sense. Use low-side current sense instead."); + return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; +} + + +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + HAL_Init(); + MX_GPIO_Init(); + MX_DMA_Init(); + _configureOPAMPs(&hopamp1, &hopamp3, &hopamp2); + MX_ADC1_Init(&hadc1); + MX_ADC2_Init(&hadc2); + + MX_DMA1_Init(&hadc1, &hdma_adc1, DMA1_Channel1, DMA_REQUEST_ADC1); + MX_DMA1_Init(&hadc2, &hdma_adc2, DMA1_Channel2, DMA_REQUEST_ADC2); + + if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer1, ADC_BUF_LEN_1) != HAL_OK) + { + SIMPLEFOC_DEBUG("DMA read init failed"); + } + if (HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adcBuffer2, ADC_BUF_LEN_2) != HAL_OK) + { + SIMPLEFOC_DEBUG("DMA read init failed"); + } + + HAL_OPAMP_Start(&hopamp1); + HAL_OPAMP_Start(&hopamp2); + HAL_OPAMP_Start(&hopamp3); + + Stm32CurrentSenseParams* params = new Stm32CurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE) / (_ADC_RESOLUTION), + .timer_handle = (HardwareTimer *)(HardwareTimer_Handle[get_timer_index(TIM1)]->__this) + }; + + return params; +} + +extern "C" { +void DMA1_Channel1_IRQHandler(void) { + HAL_DMA_IRQHandler(&hdma_adc1); +} + +void DMA1_Channel2_IRQHandler(void) { + HAL_DMA_IRQHandler(&hdma_adc2); +} +} + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger for DMA transfer aligns with the pwm peaks instead of throughs. + // only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp new file mode 100644 index 0000000..94253d7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp @@ -0,0 +1,33 @@ + +#include "../../hardware_api.h" + +#if defined(_STM32_DEF_) and !defined(ARDUINO_B_G431B_ESC1) + +#include "stm32_mcu.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + Stm32CurrentSenseParams* params = new Stm32CurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = analogRead(pinA); + return raw_adc * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h new file mode 100644 index 0000000..6e23817 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h @@ -0,0 +1,21 @@ + +#ifndef STM32_CURRENTSENSE_MCU_DEF +#define STM32_CURRENTSENSE_MCU_DEF +#include "../../hardware_api.h" +#include "../../../common/foc_utils.h" + +#if defined(_STM32_DEF_) + +// generic implementation of the hardware specific structure +// containing all the necessary current sense parameters +// will be returned as a void pointer from the _configureADCx functions +// will be provided to the _readADCVoltageX() as a void pointer +typedef struct Stm32CurrentSenseParams { + int pins[3] = {(int)NOT_SET}; + float adc_voltage_conv; + ADC_HandleTypeDef* adc_handle = NP; + HardwareTimer* timer_handle = NP; +} Stm32CurrentSenseParams; + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp new file mode 100644 index 0000000..27e1195 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp @@ -0,0 +1,162 @@ +#include "stm32f1_hal.h" + +#if defined(STM32F1xx) + +#include "../../../../communication/SimpleFOCDebug.h" +#define _TRGO_NOT_AVAILABLE 12345 + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_adc_ex.h#L215 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJECCONV_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJECCONV_T2_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJECCONV_T4_TRGO; +#endif +#ifdef TIM5 // if defined timer 5 + else if(timer->getHandle()->Instance == TIM5) + return ADC_EXTERNALTRIGINJECCONV_T5_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_adc_ex.h#L215 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGCONV_T3_TRGO; +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGCONV_T8_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + if(hadc.Instance == ADC1) __HAL_RCC_ADC1_CLK_ENABLE(); +#ifdef ADC2 // if defined ADC2 + else if(hadc.Instance == ADC2) __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef ADC3 // if defined ADC3 + else if(hadc.Instance == ADC3) __HAL_RCC_ADC3_CLK_ENABLE(); +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = ENABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 0; + HAL_ADC_Init(&hadc); + /**Configure for the selected ADC regular channel to be converted. + */ + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_1CYCLE_5; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffset = 0; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); + #endif + return -1; + } + // display which timer is being used + #ifdef SIMPLEFOC_STM32_DEBUG + // it would be better to use the getTimerNumber from driver + SIMPLEFOC_DEBUG("STM32-CS: injected trigger for timer index: ", get_timer_index(cs_params->timer_handle->getHandle()->Instance) + 1); + #endif + + // first channel + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_1; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[0]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + // second channel + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_2; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[1]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_3; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[2]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + } + + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + + cs_params->adc_handle = &hadc; + + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } + +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h new file mode 100644 index 0000000..b0f4f83 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h @@ -0,0 +1,17 @@ +#ifndef STM32F1_LOWSIDE_HAL +#define STM32F1_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32F1xx) +#include "stm32f1xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp new file mode 100644 index 0000000..8daa7ee --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp @@ -0,0 +1,104 @@ +#include "../../../hardware_api.h" + +#if defined(STM32F1xx) +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32f1_hal.h" +#include "Arduino.h" + +#define _ADC_VOLTAGE_F1 3.3f +#define _ADC_RESOLUTION_F1 4096.0f + +// array of values of 4 injected channels per adc instance (3) +uint32_t adc_val[3][4]={0}; +// does adc interrupt need a downsample - per adc (3) +bool needs_downsample[3] = {1}; +// downsampling variable - per adc (3) +uint8_t tim_downsample[3] = {0}; + +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + if(AdcHandle->Instance == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle->Instance == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle->Instance == ADC3) return 2; +#endif + return 0; +} + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET,(int)NOT_SET,(int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_F1) / (_ADC_RESOLUTION_F1) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp new file mode 100644 index 0000000..68a9d09 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp @@ -0,0 +1,170 @@ +#include "stm32f4_hal.h" + +#if defined(STM32F4xx) + +//#define SIMPLEFOC_STM32_DEBUG + +#include "../../../../communication/SimpleFOCDebug.h" +#define _TRGO_NOT_AVAILABLE 12345 + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) __HAL_RCC_ADC1_CLK_ENABLE(); +#ifdef ADC2 // if defined ADC2 + else if(hadc.Instance == ADC2) __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef ADC3 // if defined ADC3 + else if(hadc.Instance == ADC3) __HAL_RCC_ADC3_CLK_ENABLE(); +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ENABLE; + hadc.Init.ContinuousConvMode = ENABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 1; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_3CYCLES; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONVEDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffset = 0; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); + #endif + return -1; + } + // display which timer is being used + #ifdef SIMPLEFOC_STM32_DEBUG + // it would be better to use the getTimerNumber from driver + SIMPLEFOC_DEBUG("STM32-CS: injected trigger for timer index: ", get_timer_index(cs_params->timer_handle->getHandle()->Instance) + 1); + #endif + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + // enable interrupt + HAL_NVIC_SetPriority(ADC_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC_IRQn); + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h new file mode 100644 index 0000000..71071a5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h @@ -0,0 +1,18 @@ +#ifndef STM32F4_LOWSIDE_HAL +#define STM32F4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32F4xx) +#include "stm32f4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32f4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp new file mode 100644 index 0000000..6034478 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp @@ -0,0 +1,96 @@ +#include "../../../hardware_api.h" + +#if defined(STM32F4xx) +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32f4_hal.h" +#include "stm32f4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_F4 3.3f +#define _ADC_RESOLUTION_F4 4096.0f + + +// array of values of 4 injected channels per adc instance (3) +uint32_t adc_val[3][4]={0}; +// does adc interrupt need a downsample - per adc (3) +bool needs_downsample[3] = {1}; +// downsampling variable - per adc (3) +uint8_t tim_downsample[3] = {0}; + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET,(int)NOT_SET,(int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_F4) / (_ADC_RESOLUTION_F4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp new file mode 100644 index 0000000..20793d8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp @@ -0,0 +1,193 @@ +#include "stm32f4_utils.h" + +#if defined(STM32F4xx) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc_ex.h#L179 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJECCONV_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJECCONV_T2_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJECCONV_T4_TRGO; +#endif +#ifdef TIM5 // if defined timer 5 + else if(timer->getHandle()->Instance == TIM5) + return ADC_EXTERNALTRIGINJECCONV_T5_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h#L331 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGCONV_T2_TRGO; +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGCONV_T3_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGCONV_T8_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h new file mode 100644 index 0000000..b4549ba --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32F4_UTILS_HAL +#define STM32F4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32F4xx) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc_ex.h#L179 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h#L331 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp new file mode 100644 index 0000000..9132211 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp @@ -0,0 +1,267 @@ +#include "stm32g4_hal.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "../../../../communication/SimpleFOCDebug.h" + +#define SIMPLEFOC_STM32_DEBUG + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) { +#ifdef __HAL_RCC_ADC1_CLK_ENABLE + __HAL_RCC_ADC1_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { +#ifdef __HAL_RCC_ADC2_CLK_ENABLE + __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { +#ifdef __HAL_RCC_ADC3_CLK_ENABLE + __HAL_RCC_ADC3_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { +#ifdef __HAL_RCC_ADC4_CLK_ENABLE + __HAL_RCC_ADC4_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = DISABLE; + hadc.Init.LowPowerAutoWait = DISABLE; + hadc.Init.GainCompensation = 0; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 2; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONV_EDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedSingleDiff = ADC_SINGLE_ENDED; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffsetNumber = ADC_OFFSET_NONE; + sConfigInjected.InjectedOffset = 0; + sConfigInjected.InjecOversamplingMode = DISABLE; + sConfigInjected.QueueInjectedContext = DISABLE; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); +#endif + return -1; + } + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + + + if(hadc.Instance == ADC1) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { + // enable interrupt + HAL_NVIC_SetPriority(ADC3_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC3_IRQn); + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { + // enable interrupt + HAL_NVIC_SetPriority(ADC4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC4_IRQn); + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { + // enable interrupt + HAL_NVIC_SetPriority(ADC5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC5_IRQn); + } +#endif + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#ifdef ADC3 + void ADC3_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC4 + void ADC4_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC5 + void ADC5_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h new file mode 100644 index 0000000..2298b17 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h @@ -0,0 +1,19 @@ +#ifndef STM32G4_LOWSIDE_HAL +#define STM32G4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "stm32g4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32g4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp new file mode 100644 index 0000000..ae96bad --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp @@ -0,0 +1,98 @@ +#include "../../../hardware_api.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32g4_hal.h" +#include "stm32g4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_G4 3.3f +#define _ADC_RESOLUTION_G4 4096.0f + + +// array of values of 4 injected channels per adc instance (5) +uint32_t adc_val[5][4]={0}; +// does adc interrupt need a downsample - per adc (5) +bool needs_downsample[5] = {1}; +// downsampling variable - per adc (5) +uint8_t tim_downsample[5] = {0}; + + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET, (int)NOT_SET, (int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_G4) / (_ADC_RESOLUTION_G4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp new file mode 100644 index 0000000..89a9bc3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp @@ -0,0 +1,237 @@ +#include "stm32g4_utils.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJEC_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJEC_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGINJEC_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJEC_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIGINJEC_T6_TRGO; +#endif +#ifdef TIM7 // if defined timer 7 + else if(timer->getHandle()->Instance == TIM7) + return ADC_EXTERNALTRIGINJEC_T7_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGINJEC_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIGINJEC_T15_TRGO; +#endif +#ifdef TIM20 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM20) + return ADC_EXTERNALTRIGINJEC_T20_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIG_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIG_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIG_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIG_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIG_T6_TRGO; +#endif +#ifdef TIM7 // if defined timer 7 + else if(timer->getHandle()->Instance == TIM7) + return ADC_EXTERNALTRIG_T7_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIG_T7_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIG_T15_TRGO; +#endif +#ifdef TIM20 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM20) + return ADC_EXTERNALTRIG_T20_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h new file mode 100644 index 0000000..fa857bd --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32G4_UTILS_HAL +#define STM32G4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp new file mode 100644 index 0000000..bfec217 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp @@ -0,0 +1,266 @@ +#include "stm32l4_hal.h" + +#if defined(STM32L4xx) + +#include "../../../../communication/SimpleFOCDebug.h" + +#define SIMPLEFOC_STM32_DEBUG + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) { +#ifdef __HAL_RCC_ADC1_CLK_ENABLE + __HAL_RCC_ADC1_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { +#ifdef __HAL_RCC_ADC2_CLK_ENABLE + __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { +#ifdef __HAL_RCC_ADC3_CLK_ENABLE + __HAL_RCC_ADC3_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { +#ifdef __HAL_RCC_ADC4_CLK_ENABLE + __HAL_RCC_ADC4_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = DISABLE; + hadc.Init.LowPowerAutoWait = DISABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 2; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONV_EDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedSingleDiff = ADC_SINGLE_ENDED; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffsetNumber = ADC_OFFSET_NONE; + sConfigInjected.InjectedOffset = 0; + sConfigInjected.InjecOversamplingMode = DISABLE; + sConfigInjected.QueueInjectedContext = DISABLE; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); +#endif + return -1; + } + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + + + if(hadc.Instance == ADC1) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { + // enable interrupt + HAL_NVIC_SetPriority(ADC3_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC3_IRQn); + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { + // enable interrupt + HAL_NVIC_SetPriority(ADC4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC4_IRQn); + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { + // enable interrupt + HAL_NVIC_SetPriority(ADC5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC5_IRQn); + } +#endif + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#ifdef ADC3 + void ADC3_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC4 + void ADC4_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC5 + void ADC5_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h new file mode 100644 index 0000000..0317b74 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h @@ -0,0 +1,19 @@ +#ifndef STM32L4_LOWSIDE_HAL +#define STM32L4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32L4xx) + +#include "stm32l4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32l4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp new file mode 100644 index 0000000..edac641 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp @@ -0,0 +1,98 @@ +#include "../../../hardware_api.h" + +#if defined(STM32L4xx) + +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32l4_hal.h" +#include "stm32l4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_L4 3.3f +#define _ADC_RESOLUTION_L4 4096.0f + + +// array of values of 4 injected channels per adc instance (5) +uint32_t adc_val[5][4]={0}; +// does adc interrupt need a downsample - per adc (5) +bool needs_downsample[5] = {1}; +// downsampling variable - per adc (5) +uint8_t tim_downsample[5] = {0}; + + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET, (int)NOT_SET, (int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_L4) / (_ADC_RESOLUTION_L4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp new file mode 100644 index 0000000..376d9d6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp @@ -0,0 +1,221 @@ +#include "stm32l4_utils.h" + +#if defined(STM32L4xx) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32L4xx_HAL_Driver/Inc/stm32l4xx_hal_adc_ex.h#L210 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJEC_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJEC_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGINJEC_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJEC_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIGINJEC_T6_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGINJEC_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIGINJEC_T15_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIG_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIG_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIG_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIG_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIG_T6_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIG_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIG_T15_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h new file mode 100644 index 0000000..ceef9be --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32L4_UTILS_HAL +#define STM32L4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32L4xx) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp new file mode 100644 index 0000000..7ab370a --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp @@ -0,0 +1,24 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.cpp new file mode 100644 index 0000000..637c8db --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.cpp @@ -0,0 +1,92 @@ +#include "BLDCDriver3PWM.h" + +BLDCDriver3PWM::BLDCDriver3PWM(int phA, int phB, int phC, int en1, int en2, int en3){ + // Pin initialization + pwmA = phA; + pwmB = phB; + pwmC = phC; + + // enable_pin pin + enableA_pin = en1; + enableB_pin = en2; + enableC_pin = en3; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void BLDCDriver3PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enableA_pin) ) digitalWrite(enableA_pin, enable_active_high); + if ( _isset(enableB_pin) ) digitalWrite(enableB_pin, enable_active_high); + if ( _isset(enableC_pin) ) digitalWrite(enableC_pin, enable_active_high); + // set zero to PWM + setPwm(0,0,0); +} + +// disable motor driver +void BLDCDriver3PWM::disable() +{ + // set zero to PWM + setPwm(0, 0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enableA_pin) ) digitalWrite(enableA_pin, !enable_active_high); + if ( _isset(enableB_pin) ) digitalWrite(enableB_pin, !enable_active_high); + if ( _isset(enableC_pin) ) digitalWrite(enableC_pin, !enable_active_high); + +} + +// init hardware pins +int BLDCDriver3PWM::init() { + // PWM pins + pinMode(pwmA, OUTPUT); + pinMode(pwmB, OUTPUT); + pinMode(pwmC, OUTPUT); + if( _isset(enableA_pin)) pinMode(enableA_pin, OUTPUT); + if( _isset(enableB_pin)) pinMode(enableB_pin, OUTPUT); + if( _isset(enableC_pin)) pinMode(enableC_pin, OUTPUT); + + + // sanity check for the voltage limit configuration + if(!_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure3PWM(pwm_frequency, pwmA, pwmB, pwmC); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + + +// Set voltage to the pwm pin +void BLDCDriver3PWM::setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) { + // disable if needed + if( _isset(enableA_pin) && _isset(enableB_pin) && _isset(enableC_pin) ){ + digitalWrite(enableA_pin, sa == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + digitalWrite(enableB_pin, sb == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + digitalWrite(enableC_pin, sc == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + } +} + +// Set voltage to the pwm pin +void BLDCDriver3PWM::setPwm(float Ua, float Ub, float Uc) { + + // limit the voltage in driver + Ua = _constrain(Ua, 0.0f, voltage_limit); + Ub = _constrain(Ub, 0.0f, voltage_limit); + Uc = _constrain(Uc, 0.0f, voltage_limit); + // calculate duty cycle + // limited in [0,1] + dc_a = _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); + dc_b = _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); + dc_c = _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); + + // hardware specific writing + // hardware specific function - depending on driver and mcu + _writeDutyCycle3PWM(dc_a, dc_b, dc_c, params); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.h new file mode 100644 index 0000000..1942f60 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver3PWM.h @@ -0,0 +1,64 @@ +#ifndef BLDCDriver3PWM_h +#define BLDCDriver3PWM_h + +#include "../common/base_classes/BLDCDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 3 pwm bldc driver class +*/ +class BLDCDriver3PWM: public BLDCDriver +{ + public: + /** + BLDCDriver class constructor + @param phA A phase pwm pin + @param phB B phase pwm pin + @param phC C phase pwm pin + @param en1 enable pin (optional input) + @param en2 enable pin (optional input) + @param en3 enable pin (optional input) + */ + BLDCDriver3PWM(int phA,int phB,int phC, int en1 = NOT_SET, int en2 = NOT_SET, int en3 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwmA; //!< phase A pwm pin number + int pwmB; //!< phase B pwm pin number + int pwmC; //!< phase C pwm pin number + int enableA_pin; //!< enable pin number + int enableB_pin; //!< enable pin number + int enableC_pin; //!< enable pin number + bool enable_active_high = true; + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + void setPwm(float Ua, float Ub, float Uc) override; + + /** + * Set phase voltages to the harware + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) override; + private: +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.cpp new file mode 100644 index 0000000..4981858 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.cpp @@ -0,0 +1,103 @@ +#include "BLDCDriver6PWM.h" + +BLDCDriver6PWM::BLDCDriver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int en){ + // Pin initialization + pwmA_h = phA_h; + pwmB_h = phB_h; + pwmC_h = phC_h; + pwmA_l = phA_l; + pwmB_l = phB_l; + pwmC_l = phC_l; + + // enable_pin pin + enable_pin = en; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + + // dead zone initial - 2% + dead_zone = 0.02f; + +} + +// enable motor driver +void BLDCDriver6PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin) ) digitalWrite(enable_pin, enable_active_high); + // set phase state enabled + setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_ON); + // set zero to PWM + setPwm(0, 0, 0); +} + +// disable motor driver +void BLDCDriver6PWM::disable() +{ + // set phase state to disabled + setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_OFF, PhaseState::PHASE_OFF); + // set zero to PWM + setPwm(0, 0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin) ) digitalWrite(enable_pin, !enable_active_high); + +} + +// init hardware pins +int BLDCDriver6PWM::init() { + + // PWM pins + pinMode(pwmA_h, OUTPUT); + pinMode(pwmB_h, OUTPUT); + pinMode(pwmC_h, OUTPUT); + pinMode(pwmA_l, OUTPUT); + pinMode(pwmB_l, OUTPUT); + pinMode(pwmC_l, OUTPUT); + if(_isset(enable_pin)) pinMode(enable_pin, OUTPUT); + + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // set phase state to disabled + phase_state[0] = PhaseState::PHASE_OFF; + phase_state[1] = PhaseState::PHASE_OFF; + phase_state[2] = PhaseState::PHASE_OFF; + + // set zero to PWM + dc_a = dc_b = dc_c = 0; + + // configure 6pwm + // hardware specific function - depending on driver and mcu + params = _configure6PWM(pwm_frequency, dead_zone, pwmA_h,pwmA_l, pwmB_h,pwmB_l, pwmC_h,pwmC_l); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void BLDCDriver6PWM::setPwm(float Ua, float Ub, float Uc) { + // limit the voltage in driver + Ua = _constrain(Ua, 0, voltage_limit); + Ub = _constrain(Ub, 0, voltage_limit); + Uc = _constrain(Uc, 0, voltage_limit); + // calculate duty cycle + // limited in [0,1] + dc_a = _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); + dc_b = _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); + dc_c = _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); + // hardware specific writing + // hardware specific function - depending on driver and mcu + _writeDutyCycle6PWM(dc_a, dc_b, dc_c, phase_state, params); +} + + +// Set the phase state +// actually changing the state is only done on the next call to setPwm, and depends +// on the hardware capabilities of the driver and MCU. +void BLDCDriver6PWM::setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) { + phase_state[0] = sa; + phase_state[1] = sb; + phase_state[2] = sc; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.h new file mode 100644 index 0000000..e8643cc --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/BLDCDriver6PWM.h @@ -0,0 +1,70 @@ +#ifndef BLDCDriver6PWM_h +#define BLDCDriver6PWM_h + +#include "../common/base_classes/BLDCDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 6 pwm bldc driver class +*/ +class BLDCDriver6PWM: public BLDCDriver +{ + public: + /** + BLDCDriver class constructor + @param phA_h A phase pwm pin + @param phA_l A phase pwm pin + @param phB_h B phase pwm pin + @param phB_l A phase pwm pin + @param phC_h C phase pwm pin + @param phC_l A phase pwm pin + @param en enable pin (optional input) + */ + BLDCDriver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int en = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwmA_h,pwmA_l; //!< phase A pwm pin number + int pwmB_h,pwmB_l; //!< phase B pwm pin number + int pwmC_h,pwmC_l; //!< phase C pwm pin number + int enable_pin; //!< enable pin number + bool enable_active_high = true; + + float dead_zone; //!< a percentage of dead-time(zone) (both high and low side in low) for each pwm cycle [0,1] + + PhaseState phase_state[3]; //!< phase state (active / disabled) + + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + void setPwm(float Ua, float Ub, float Uc) override; + + /** + * Set phase voltages to the harware + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.cpp new file mode 100644 index 0000000..dbbf5b8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.cpp @@ -0,0 +1,107 @@ +#include "StepperDriver2PWM.h" + +StepperDriver2PWM::StepperDriver2PWM(int _pwm1, int* _in1, int _pwm2, int* _in2, int en1, int en2){ + // Pin initialization + pwm1 = _pwm1; // phase 1 pwm pin number + dir1a = _in1[0]; // phase 1 INA pin number + dir1b = _in1[1]; // phase 1 INB pin number + pwm2 = _pwm2; // phase 2 pwm pin number + dir2a = _in2[0]; // phase 2 INA pin number + dir2b = _in2[1]; // phase 2 INB pin number + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +StepperDriver2PWM::StepperDriver2PWM(int _pwm1, int _dir1, int _pwm2, int _dir2, int en1, int en2){ + // Pin initialization + pwm1 = _pwm1; // phase 1 pwm pin number + dir1a = _dir1; // phase 1 direction pin + pwm2 = _pwm2; // phase 2 pwm pin number + dir2a = _dir2; // phase 2 direction pin + // these pins are not used + dir1b = NOT_SET; + dir2b = NOT_SET; + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void StepperDriver2PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, HIGH); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, HIGH); + // set zero to PWM + setPwm(0,0); +} + +// disable motor driver +void StepperDriver2PWM::disable() +{ + // set zero to PWM + setPwm(0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, LOW); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, LOW); + +} + +// init hardware pins +int StepperDriver2PWM::init() { + // PWM pins + pinMode(pwm1, OUTPUT); + pinMode(pwm2, OUTPUT); + pinMode(dir1a, OUTPUT); + pinMode(dir2a, OUTPUT); + if( _isset(dir1b) ) pinMode(dir1b, OUTPUT); + if( _isset(dir2b) ) pinMode(dir2b, OUTPUT); + + if( _isset(enable_pin1) ) pinMode(enable_pin1, OUTPUT); + if( _isset(enable_pin2) ) pinMode(enable_pin2, OUTPUT); + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure2PWM(pwm_frequency, pwm1, pwm2); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void StepperDriver2PWM::setPwm(float Ua, float Ub) { + float duty_cycle1(0.0f),duty_cycle2(0.0f); + // limit the voltage in driver + Ua = _constrain(Ua, -voltage_limit, voltage_limit); + Ub = _constrain(Ub, -voltage_limit, voltage_limit); + // hardware specific writing + duty_cycle1 = _constrain(abs(Ua)/voltage_power_supply,0.0f,1.0f); + duty_cycle2 = _constrain(abs(Ub)/voltage_power_supply,0.0f,1.0f); + + // phase 1 direction + digitalWrite(dir1a, Ua >= 0 ? LOW : HIGH); + if( _isset(dir1b) ) digitalWrite(dir1b, Ua >= 0 ? HIGH : LOW ); + // phase 2 direction + digitalWrite(dir2a, Ub >= 0 ? LOW : HIGH); + if( _isset(dir2b) ) digitalWrite(dir2b, Ub >= 0 ? HIGH : LOW ); + + // write to hardware + _writeDutyCycle2PWM(duty_cycle1, duty_cycle2, params); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.h new file mode 100644 index 0000000..b349af0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver2PWM.h @@ -0,0 +1,68 @@ +#ifndef STEPPER_DRIVER_2PWM_h +#define STEPPER_DRIVER_2PWM_h + +#include "../common/base_classes/StepperDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 2 pwm stepper driver class +*/ +class StepperDriver2PWM: public StepperDriver +{ + public: + /** + StepperMotor class constructor + @param pwm1 PWM1 phase pwm pin + @param in1 IN1A phase dir pin + @param pwm2 PWM2 phase pwm pin + @param in2 IN2A phase dir + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver2PWM(int pwm1, int* in1, int pwm2, int* in2, int en1 = NOT_SET, int en2 = NOT_SET); + + /** + StepperMotor class constructor + @param pwm1 PWM1 phase pwm pin + @param dir1 DIR1 phase dir pin + @param pwm2 PWM2 phase pwm pin + @param dir2 DIR2 phase dir pin + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver2PWM(int pwm1, int dir1, int pwm2, int dir2, int en1 = NOT_SET, int en2 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwm1; //!< phase 1 pwm pin number + int dir1a; //!< phase 1 INA pin number + int dir1b; //!< phase 1 INB pin number + int pwm2; //!< phase 2 pwm pin number + int dir2a; //!< phase 2 INA pin number + int dir2b; //!< phase 2 INB pin number + int enable_pin1; //!< enable pin number phase 1 + int enable_pin2; //!< enable pin number phase 2 + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + void setPwm(float Ua, float Ub) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.cpp new file mode 100644 index 0000000..836f547 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.cpp @@ -0,0 +1,81 @@ +#include "StepperDriver4PWM.h" + +StepperDriver4PWM::StepperDriver4PWM(int ph1A,int ph1B,int ph2A,int ph2B,int en1, int en2){ + // Pin initialization + pwm1A = ph1A; + pwm1B = ph1B; + pwm2A = ph2A; + pwm2B = ph2B; + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void StepperDriver4PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, HIGH); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, HIGH); + // set zero to PWM + setPwm(0,0); +} + +// disable motor driver +void StepperDriver4PWM::disable() +{ + // set zero to PWM + setPwm(0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, LOW); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, LOW); + +} + +// init hardware pins +int StepperDriver4PWM::init() { + + // PWM pins + pinMode(pwm1A, OUTPUT); + pinMode(pwm1B, OUTPUT); + pinMode(pwm2A, OUTPUT); + pinMode(pwm2B, OUTPUT); + if( _isset(enable_pin1) ) pinMode(enable_pin1, OUTPUT); + if( _isset(enable_pin2) ) pinMode(enable_pin2, OUTPUT); + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure4PWM(pwm_frequency, pwm1A, pwm1B, pwm2A, pwm2B); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void StepperDriver4PWM::setPwm(float Ualpha, float Ubeta) { + float duty_cycle1A(0.0f),duty_cycle1B(0.0f),duty_cycle2A(0.0f),duty_cycle2B(0.0f); + // limit the voltage in driver + Ualpha = _constrain(Ualpha, -voltage_limit, voltage_limit); + Ubeta = _constrain(Ubeta, -voltage_limit, voltage_limit); + // hardware specific writing + if( Ualpha > 0 ) + duty_cycle1B = _constrain(abs(Ualpha)/voltage_power_supply,0.0f,1.0f); + else + duty_cycle1A = _constrain(abs(Ualpha)/voltage_power_supply,0.0f,1.0f); + + if( Ubeta > 0 ) + duty_cycle2B = _constrain(abs(Ubeta)/voltage_power_supply,0.0f,1.0f); + else + duty_cycle2A = _constrain(abs(Ubeta)/voltage_power_supply,0.0f,1.0f); + // write to hardware + _writeDutyCycle4PWM(duty_cycle1A, duty_cycle1B, duty_cycle2A, duty_cycle2B, params); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.h new file mode 100644 index 0000000..e4b2ee4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/StepperDriver4PWM.h @@ -0,0 +1,55 @@ +#ifndef STEPPER_DRIVER_4PWM_h +#define STEPPER_DRIVER_4PWM_h + +#include "../common/base_classes/StepperDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 4 pwm stepper driver class +*/ +class StepperDriver4PWM: public StepperDriver +{ + public: + /** + StepperMotor class constructor + @param ph1A 1A phase pwm pin + @param ph1B 1B phase pwm pin + @param ph2A 2A phase pwm pin + @param ph2B 2B phase pwm pin + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver4PWM(int ph1A,int ph1B,int ph2A,int ph2B, int en1 = NOT_SET, int en2 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwm1A; //!< phase 1A pwm pin number + int pwm1B; //!< phase 1B pwm pin number + int pwm2A; //!< phase 2A pwm pin number + int pwm2B; //!< phase 2B pwm pin number + int enable_pin1; //!< enable pin number phase 1 + int enable_pin2; //!< enable pin number phase 2 + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + void setPwm(float Ua, float Ub) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_api.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_api.h new file mode 100644 index 0000000..8b47745 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_api.h @@ -0,0 +1,180 @@ +#ifndef HARDWARE_UTILS_DRIVER_H +#define HARDWARE_UTILS_DRIVER_H + +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../communication/SimpleFOCDebug.h" +#include "../common/base_classes/BLDCDriver.h" + + +// these defines determine the polarity of the PWM output. Normally, the polarity is active-high, +// i.e. a high-level PWM output is expected to switch on the MOSFET. But should your driver design +// require inverted polarity, you can change the defines below, or set them via your build environment +// or board definition files. + +// used for 1-PWM, 2-PWM, 3-PWM, and 4-PWM modes +#ifndef SIMPLEFOC_PWM_ACTIVE_HIGH +#define SIMPLEFOC_PWM_ACTIVE_HIGH true +#endif +// used for 6-PWM mode, high-side +#ifndef SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH +#define SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH true +#endif +// used for 6-PWM mode, low-side +#ifndef SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH +#define SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH true +#endif + + + + +// flag returned if driver init fails +#define SIMPLEFOC_DRIVER_INIT_FAILED ((void*)-1) + +// generic implementation of the hardware specific structure +// containing all the necessary driver parameters +// will be returned as a void pointer from the _configurexPWM functions +// will be provided to the _writeDutyCyclexPWM() as a void pointer +typedef struct GenericDriverParams { + int pins[6]; + long pwm_frequency; + float dead_zone; +} GenericDriverParams; + + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA pwm pin + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure1PWM(long pwm_frequency, const int pinA); + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB); + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * @param pinC pinC bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC); + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pin1A pin1A stepper driver + * @param pin1B pin1B stepper driver + * @param pin2A pin2A stepper driver + * @param pin2B pin2B stepper driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B); + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 6PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low - if applicable + * @param pinA_h pinA high-side bldc driver + * @param pinA_l pinA low-side bldc driver + * @param pinB_h pinA high-side bldc driver + * @param pinB_l pinA low-side bldc driver + * @param pinC_h pinA high-side bldc driver + * @param pinC_l pinA low-side bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle1PWM(float dc_a, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param dc_1a duty cycle phase 1A [0, 1] + * @param dc_1b duty cycle phase 1B [0, 1] + * @param dc_2a duty cycle phase 2A [0, 1] + * @param dc_2b duty cycle phase 2B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params); + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 6PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param phase_state pointer to PhaseState[3] array + * @param params the driver parameters + * + */ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params); + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp new file mode 100644 index 0000000..8a7fbbe --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp @@ -0,0 +1,278 @@ +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega2560__) || defined(AVR_ATmega1280) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega2560 or Arduino/ATmega1280") +#pragma message("") + + +#define _PWM_FREQUENCY 32000 +#define _PWM_FREQUENCY_MAX 32000 +#define _PWM_FREQUENCY_MIN 4000 + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin, const long frequency){ + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + // High PWM frequency + // https://sites.google.com/site/qeewiki/books/avr-guide/timers-on-the-ATmega2560 + // https://forum.arduino.cc/index.php?topic=72092.0 + if (pin == 13 || pin == 4 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + } + else if (pin == 12 || pin == 11 ) + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 10 || pin == 9 ) + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 5 || pin == 3 || pin == 2) + if(high_fq) TCCR3B = ((TCCR3B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR3B = ((TCCR3B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 8 || pin == 7 || pin == 6) + if(high_fq) TCCR4B = ((TCCR4B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR4B = ((TCCR4B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 44 || pin == 45 || pin == 46) + if(high_fq) TCCR5B = ((TCCR5B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR5B = ((TCCR5B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure1PWM(long pwm_frequency,const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + _pinHighFrequency(pinB, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + _pinHighFrequency(pinB, pwm_frequency); + _pinHighFrequency(pinC, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + // _syncAllTimers(); + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware specific +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware specific +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A,pwm_frequency); + _pinHighFrequency(pin1B,pwm_frequency); + _pinHighFrequency(pin2A,pwm_frequency); + _pinHighFrequency(pin2B,pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware specific +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +// supports Arduino/ATmega2560 +// https://ww1.microchip.com/downloads/en/devicedoc/atmel-2549-8-bit-avr-microcontroller-atmega640-1280-1281-2560-2561_datasheet.pdf +// https://docs.arduino.cc/hacking/hardware/PinMapping2560 +int _configureComplementaryPair(const int pinH,const int pinL, long frequency) { + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + + // configure pin pairs + if( (pinH == 4 && pinL == 13 ) || (pinH == 13 && pinL == 4 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 13 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set frequency + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if( (pinH == 11 && pinL == 12 ) || (pinH == 12 && pinL == 11 ) ){ + // set frequency + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 11 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 10 && pinL == 9 ) || (pinH == 9 && pinL == 10 ) ){ + // set frequency + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 10 ) TCCR2A = 0b10110000 | (TCCR2A & 0b00001111) ; + else TCCR2A = 0b11100000 | (TCCR2A & 0b00001111) ; + }else if((pinH == 5 && pinL == 2 ) || (pinH == 2 && pinL == 5 ) ){ + // set frequency + if(high_fq) TCCR3B = ((TCCR3B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR3B = ((TCCR3B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 5 ) TCCR3A = 0b10110000 | (TCCR3A & 0b00001111) ; + else TCCR3A = 0b11100000 | (TCCR3A & 0b00001111) ; + }else if((pinH == 6 && pinL == 7 ) || (pinH == 7 && pinL == 6 ) ){ + // set frequency + if(high_fq) TCCR4B = ((TCCR4B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR4B = ((TCCR4B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 6 ) TCCR4A = 0b10110000 | (TCCR4A & 0b00001111) ; + else TCCR4A = 0b11100000 | (TCCR4A & 0b00001111) ; + }else if((pinH == 46 && pinL == 45 ) || (pinH == 45 && pinL == 46 ) ){ + // set frequency + if(high_fq) TCCR5B = ((TCCR5B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR5B = ((TCCR5B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 46 ) TCCR5A = 0b10110000 | (TCCR5A & 0b00001111) ; + else TCCR5A = 0b11100000 | (TCCR5A & 0b00001111) ; + }else{ + return -1; + } + return 0; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l, pwm_frequency); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + // _syncAllTimers(); + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time, PhaseState ps) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + // determine the phase state and set the pwm accordingly + // deactivate phases if needed + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_LO)){ + digitalWrite(pinH, LOW); + }else{ + analogWrite(pinH, pwm_h); + } + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_HI)){ + digitalWrite(pinL, LOW); + }else{ + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); + } + +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[0]); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[1]); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[2]); +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp new file mode 100644 index 0000000..53fb108 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp @@ -0,0 +1,255 @@ +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega328 ATmega168 ATmega328PB") +#pragma message("") + +#define _PWM_FREQUENCY 32000 +#define _PWM_FREQUENCY_MAX 32000 +#define _PWM_FREQUENCY_MIN 4000 + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin, const long frequency){ + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + // High PWM frequency + // https://www.arxterra.com/9-atmega328p-timers/ + if (pin == 5 || pin == 6 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if (pin == 9 || pin == 10 ){ + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if (pin == 3 || pin == 11){ + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + } +} + +void _syncAllTimers(){ + GTCCR = (1<pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware specific +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A,pwm_frequency); + _pinHighFrequency(pin1B,pwm_frequency); + _pinHighFrequency(pin2A,pwm_frequency); + _pinHighFrequency(pin2B,pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware specific +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +int _configureComplementaryPair(const int pinH, const int pinL, long frequency) { + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + + // configure pins + if( (pinH == 5 && pinL == 6 ) || (pinH == 6 && pinL == 5 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 6 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set frequency + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if( (pinH == 9 && pinL == 10 ) || (pinH == 10 && pinL == 9 ) ){ + // set frequency + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 9 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 3 && pinL == 11 ) || (pinH == 11 && pinL == 3 ) ){ + // set frequency + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 11 ) TCCR2A = 0b10110000 | (TCCR2A & 0b00001111) ; + else TCCR2A = 0b11100000 | (TCCR2A & 0b00001111) ; + }else{ + return -1; + } + return 0; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - setting +// - hardware specific +// supports Arduino/ATmega328 +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l, pwm_frequency); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + _syncAllTimers(); + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time, PhaseState ps) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + // determine the phase state and set the pwm accordingly + // deactivate phases if needed + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_LO)){ + digitalWrite(pinH, LOW); + }else{ + analogWrite(pinH, pwm_h); + } + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_HI)){ + digitalWrite(pinL, LOW); + }else{ + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); + } + + +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[0]); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[1]); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[2]); +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp new file mode 100644 index 0000000..b4ad310 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp @@ -0,0 +1,222 @@ + +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega32U4__) + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega32U4") +#pragma message("") + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin){ + // High PWM frequency + // reference: http://r6500.blogspot.com/2014/12/fast-pwm-on-arduino-leonardo.html + if (pin == 3 || pin == 11 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 + } + else if (pin == 9 || pin == 10 ) + TCCR1B = ((TCCR1B & 0b11111000) | 0x01); // set prescaler to 1 + else if (pin == 5 ) + TCCR3B = ((TCCR3B & 0b11111000) | 0x01); // set prescaler to 1 + else if ( pin == 6 || pin == 13 ) { // a bit more complicated 10 bit timer + // PLL Configuration + PLLFRQ= ((PLLFRQ & 0b11001111) | 0x20); // Use 96MHz / 1.5 = 64MHz + TCCR4B = ((TCCR4B & 0b11110000) | 0xB); // configure prescaler to get 64M/2/1024 = 31.25 kHz + TCCR4D = ((TCCR4D & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + + if (pin == 6) TCCR4A = 0x82; // activate channel A - pin 13 + else if (pin == 13) TCCR4C |= 0x09; // Activate channel D - pin 6 + } + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency,const int pinA) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + _pinHighFrequency(pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + _pinHighFrequency(pinB); + _pinHighFrequency(pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, int pinA, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A); + _pinHighFrequency(pin1B); + _pinHighFrequency(pin2A); + _pinHighFrequency(pin2B); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +int _configureComplementaryPair(int pinH, int pinL) { + if( (pinH == 3 && pinL == 11 ) || (pinH == 11 && pinL == 3 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 11 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set prescaler to 1 - 32kHz freq + TCCR0B = ((TCCR0B & 0b11110000) | 0x01); + }else if( (pinH == 9 && pinL == 10 ) || (pinH == 10 && pinL == 9 ) ){ + // set prescaler to 1 - 32kHz freq + TCCR1B = ((TCCR1B & 0b11111000) | 0x01); + // configure complementary pwm on low side + if(pinH == 9 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 6 && pinL == 13 ) || (pinH == 13 && pinL == 6 ) ){ + // PLL Configuration + PLLFRQ= ((PLLFRQ & 0b11001111) | 0x20); // Use 96MHz / 1.5 = 64MHz + TCCR4B = ((TCCR4B & 0b11110000) | 0xB); // configure prescaler to get 64M/2/1024 = 31.25 kHz + TCCR4D = ((TCCR4D & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + + // configure complementary pwm on low side + if(pinH == 13 ){ + TCCR4A = 0x82; // activate channel A - pin 13 + TCCR4C |= 0x0d; // Activate complementary channel D - pin 6 + }else { + TCCR4C |= 0x09; // Activate channel D - pin 6 + TCCR4A = 0xc2; // activate complementary channel A - pin 13 + } + }else{ + return -1; + } + return 0; +} + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + + analogWrite(pinH, pwm_h); + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arudino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + + _UNUSED(phase_state); +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp new file mode 100644 index 0000000..4397114 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp @@ -0,0 +1,449 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(__SAM3X8E__) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Due") +#pragma message("") + + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +#define _PWM_RES_MIN 255 // 50khz + +// pwm frequency and max duty cycle +static unsigned long _pwm_frequency; +static int _max_pwm_value = 1023; + +// array mapping the timer values to the interrupt handlers +static IRQn_Type irq_type[] = {TC0_IRQn, TC0_IRQn, TC1_IRQn, TC1_IRQn, TC2_IRQn, TC2_IRQn, TC3_IRQn, TC3_IRQn, TC4_IRQn, TC4_IRQn, TC5_IRQn, TC5_IRQn, TC6_IRQn, TC6_IRQn, TC7_IRQn, TC7_IRQn, TC8_IRQn, TC8_IRQn}; +// current counter values +static volatile uint32_t pwm_counter_vals[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + +// variables copied from wiring_analog.cpp for arduino due +static uint8_t PWMEnabled = 0; +static uint8_t TCChanEnabled[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; +static const uint32_t channelToChNo[] = { 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2 }; +static const uint32_t channelToAB[] = { 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 }; +static Tc *channelToTC[] = { + TC0, TC0, TC0, TC0, TC0, TC0, + TC1, TC1, TC1, TC1, TC1, TC1, + TC2, TC2, TC2, TC2, TC2, TC2 }; +static const uint32_t channelToId[] = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8 }; + + +// function setting the CMR register +static void TC_SetCMR_ChannelA(Tc *tc, uint32_t chan, uint32_t v){ tc->TC_CHANNEL[chan].TC_CMR = (tc->TC_CHANNEL[chan].TC_CMR & 0xFFF0FFFF) | v;} +static void TC_SetCMR_ChannelB(Tc *tc, uint32_t chan, uint32_t v){ tc->TC_CHANNEL[chan].TC_CMR = (tc->TC_CHANNEL[chan].TC_CMR & 0xF0FFFFFF) | v; } + + +// function which starts and syncs the timers +// if the pin is the true PWM pin this function does not do anything +void syncTimers(uint32_t ulPin1,uint32_t ulPin2, uint32_t ulPin3 = -1, uint32_t ulPin4 = -1){ + uint32_t chNo1,chNo2,chNo3,chNo4; + Tc *chTC1 = nullptr,*chTC2 = nullptr,*chTC3 = nullptr,*chTC4 = nullptr; + + // configure timer channel for the first pin if it is a timer pin + uint32_t attr = g_APinDescription[ulPin1].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel1 = g_APinDescription[ulPin1].ulTCChannel; + chNo1 = channelToChNo[channel1]; + chTC1 = channelToTC[channel1]; + TCChanEnabled[channelToId[channel1]] = 1; + } + + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin2].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel2 = g_APinDescription[ulPin2].ulTCChannel; + chNo2 = channelToChNo[channel2]; + chTC2 = channelToTC[channel2]; + TCChanEnabled[channelToId[channel2]] = 1; + } + if(ulPin3 > 0 ){ + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin3].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel3 = g_APinDescription[ulPin3].ulTCChannel; + chNo3 = channelToChNo[channel3]; + chTC3 = channelToTC[channel3]; + TCChanEnabled[channelToId[channel3]] = 1; + } + } + if(ulPin4 > 0 ){ + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin4].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel4 = g_APinDescription[ulPin4].ulTCChannel; + chNo4 = channelToChNo[channel4]; + chTC4 = channelToTC[channel4]; + TCChanEnabled[channelToId[channel4]] = 1; + } + } + // start timers and make them synced + if(chTC1){ + TC_Start(chTC1, chNo1); + chTC1->TC_BCR = TC_BCR_SYNC; + } + if(chTC2){ + TC_Start(chTC2, chNo2); + chTC2->TC_BCR = TC_BCR_SYNC; + } + if(chTC3 && ulPin3){ + TC_Start(chTC3, chNo3); + chTC3->TC_BCR = TC_BCR_SYNC; + } + if(chTC4 && ulPin4){ + TC_Start(chTC4, chNo4); + chTC4->TC_BCR = TC_BCR_SYNC; + } +} + +// function configuring the pwm frequency for given pin +// possible to supply the pwm pin and the timer pin +void initPWM(uint32_t ulPin, uint32_t pwm_freq){ + // check which pin type + uint32_t attr = g_APinDescription[ulPin].ulPinAttribute; + if ((attr & PIN_ATTR_PWM) == PIN_ATTR_PWM) { // if pwm pin + + if (!PWMEnabled) { + // PWM Startup code + pmc_enable_periph_clk(PWM_INTERFACE_ID); + // this function does not work too well - I'll rewrite it + // PWMC_ConfigureClocks(PWM_FREQUENCY * _max_pwm_value, 0, VARIANT_MCK); + + // finding the divisors an prescalers form FindClockConfiguration function + uint32_t divisors[11] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; + uint8_t divisor = 0; + uint32_t prescaler; + + /* Find prescaler and divisor values */ + prescaler = (VARIANT_MCK / divisors[divisor]) / (pwm_freq*_max_pwm_value); + while ((prescaler > 255) && (divisor < 11)) { + divisor++; + prescaler = (VARIANT_MCK / divisors[divisor]) / (pwm_freq*_max_pwm_value); + } + // update the divisor*prescaler value + prescaler = prescaler | (divisor << 8); + + // now calculate the real resolution timer period necessary (pwm resolution) + // pwm_res = bus_freq / (pwm_freq * (prescaler)) + _max_pwm_value = (double)VARIANT_MCK / (double)pwm_freq / (double)(prescaler); + // set the prescaler value + PWM->PWM_CLK = prescaler; + + PWMEnabled = 1; + } + + uint32_t chan = g_APinDescription[ulPin].ulPWMChannel; + if ((g_pinStatus[ulPin] & 0xF) != PIN_STATUS_PWM) { + // Setup PWM for this pin + PIO_Configure(g_APinDescription[ulPin].pPort, + g_APinDescription[ulPin].ulPinType, + g_APinDescription[ulPin].ulPin, + g_APinDescription[ulPin].ulPinConfiguration); + // PWM_CMR_CALG - center align + // PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKA, PWM_CMR_CALG, 0); + PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKA, 0, 0); + PWMC_SetPeriod(PWM_INTERFACE, chan, _max_pwm_value); + PWMC_SetDutyCycle(PWM_INTERFACE, chan, 0); + PWMC_EnableChannel(PWM_INTERFACE, chan); + g_pinStatus[ulPin] = (g_pinStatus[ulPin] & 0xF0) | PIN_STATUS_PWM; + } + return; + } + + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { // if timer pin + // We use MCLK/2 as clock. + const uint32_t TC = VARIANT_MCK / 2 / pwm_freq ; + // Setup Timer for this pin + ETCChannel channel = g_APinDescription[ulPin].ulTCChannel; + uint32_t chNo = channelToChNo[channel]; + uint32_t chA = channelToAB[channel]; + Tc *chTC = channelToTC[channel]; + uint32_t interfaceID = channelToId[channel]; + + if (!TCChanEnabled[interfaceID]) { + pmc_enable_periph_clk(TC_INTERFACE_ID + interfaceID); + TC_Configure(chTC, chNo, + TC_CMR_TCCLKS_TIMER_CLOCK1 | + TC_CMR_WAVE | // Waveform mode + TC_CMR_WAVSEL_UP_RC | // Counter running up and reset when equals to RC + TC_CMR_EEVT_XC0 | // Set external events from XC0 (this setup TIOB as output) + TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_CLEAR | + TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_CLEAR); + TC_SetRC(chTC, chNo, TC); + } + + // disable the counter on start + if (chA){ + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); + }else{ + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); + } + // configure input-ouput structure + if ((g_pinStatus[ulPin] & 0xF) != PIN_STATUS_PWM) { + PIO_Configure(g_APinDescription[ulPin].pPort, + g_APinDescription[ulPin].ulPinType, + g_APinDescription[ulPin].ulPin, + g_APinDescription[ulPin].ulPinConfiguration); + g_pinStatus[ulPin] = (g_pinStatus[ulPin] & 0xF0) | PIN_STATUS_PWM; + } + // enable interrupts + chTC->TC_CHANNEL[chNo].TC_IER = TC_IER_CPAS // interrupt on RA compare match + | TC_IER_CPBS // interrupt on RB compare match + | TC_IER_CPCS; // interrupt on RC compare match + chTC->TC_CHANNEL[chNo].TC_IDR = ~TC_IER_CPAS // interrupt on RA compare match + & ~TC_IER_CPBS // interrupt on RB compare match + & ~ TC_IER_CPCS; // interrupt on RC compare match + // enable interrupts for this timer + NVIC_EnableIRQ(irq_type[channel]); + return; + } +} + +// pwm setting function +// it sets the duty cycle for pwm pin or timer pin +void setPwm(uint32_t ulPin, uint32_t ulValue) { + // check pin type + uint32_t attr = g_APinDescription[ulPin].ulPinAttribute; + if ((attr & PIN_ATTR_PWM) == PIN_ATTR_PWM) { // if pwm + uint32_t chan = g_APinDescription[ulPin].ulPWMChannel; + PWMC_SetDutyCycle(PWM_INTERFACE, chan, ulValue); + return; + } + + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { // if timer pin + // get the timer variables + ETCChannel channel = g_APinDescription[ulPin].ulTCChannel; + Tc *chTC = channelToTC[channel]; + uint32_t chNo = channelToChNo[channel]; + if(!ulValue) { + // if the value 0 disable counter + if (channelToAB[channel]) + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_CLEAR); + else + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_CLEAR); + }else{ + // if the value not zero + // calculate clock + const uint32_t TC = VARIANT_MCK / 2 / _pwm_frequency; + // Map value to Timer ranges 0..max_duty_cycle => 0..TC + // Setup Timer for this pin + ulValue = ulValue * TC ; + pwm_counter_vals[channel] = ulValue / _max_pwm_value; + // enable counter + if (channelToAB[channel]) + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); + else + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); + } + + return; + } +} + +// interrupt handlers for seamless pwm duty-cycle setting +void TC0_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 0); + // update the counters + if(pwm_counter_vals[0]) TC_SetRA(TC0, 0, pwm_counter_vals[0]); + if(pwm_counter_vals[1]) TC_SetRB(TC0, 0, pwm_counter_vals[1]); +} + +void TC1_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 1); + // update the counters + if(pwm_counter_vals[2]) TC_SetRA(TC0, 1, pwm_counter_vals[2]); + if(pwm_counter_vals[3]) TC_SetRB(TC0, 1, pwm_counter_vals[3]); +} + +void TC2_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 2); + // update the counters + if(pwm_counter_vals[4]) TC_SetRA(TC0, 2, pwm_counter_vals[4]); + if(pwm_counter_vals[5]) TC_SetRB(TC0, 2, pwm_counter_vals[5]); +} +void TC3_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 0); + // update the counters + if(pwm_counter_vals[6]) TC_SetRA(TC1, 0, pwm_counter_vals[6]); + if(pwm_counter_vals[7]) TC_SetRB(TC1, 0, pwm_counter_vals[7]); +} + +void TC4_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 1); + // update the counters + if(pwm_counter_vals[8]) TC_SetRA(TC1, 1, pwm_counter_vals[8]); + if(pwm_counter_vals[9]) TC_SetRB(TC1, 1, pwm_counter_vals[9]); +} + +void TC5_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 2); + // update the counters + if(pwm_counter_vals[10]) TC_SetRA(TC1, 2, pwm_counter_vals[10]); + if(pwm_counter_vals[11]) TC_SetRB(TC1, 2, pwm_counter_vals[11]); +} +void TC6_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 0); + // update the counters + if(pwm_counter_vals[12]) TC_SetRA(TC2, 0, pwm_counter_vals[12]); + if(pwm_counter_vals[13]) TC_SetRB(TC2, 0, pwm_counter_vals[13]); +} + +void TC7_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 1); + // update the counters + if(pwm_counter_vals[14]) TC_SetRA(TC2, 1, pwm_counter_vals[14]); + if(pwm_counter_vals[15]) TC_SetRB(TC2, 1, pwm_counter_vals[15]); +} + +void TC8_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 2); + // update the counters + if(pwm_counter_vals[16]) TC_SetRA(TC2, 2, pwm_counter_vals[16]); + if(pwm_counter_vals[17]) TC_SetRB(TC2, 2, pwm_counter_vals[17]); +} + + + + + +// implementation of the hardware_api.cpp +// --------------------------------------------------------------------------------------------------------------------------------- + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware specific +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + initPWM(pinC, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB, pinC); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// Configuring PWM frequency, resolution and alignment +//- Stepper driver - 2PWM setting +// - hardware specific +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency)) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency)) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + initPWM(pinC, _pwm_frequency); + initPWM(pinD, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB, pinC, pinD); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_a); + setPwm(p->pins[1], _max_pwm_value*dc_b); + setPwm(p->pins[2], _max_pwm_value*dc_c); +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_1a); + setPwm(p->pins[1], _max_pwm_value*dc_1b); + setPwm(p->pins[2], _max_pwm_value*dc_2a); + setPwm(p->pins[3], _max_pwm_value*dc_2b); +} + + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - Stepper driver - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_a); + setPwm(p->pins[1], _max_pwm_value*dc_b); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h new file mode 100644 index 0000000..1049787 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h @@ -0,0 +1,96 @@ +#ifndef ESP32_DRIVER_MCPWM_H +#define ESP32_DRIVER_MCPWM_H + +#include "../../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + + + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP32 MCPWM driver") +#pragma message("") + + +#include "driver/mcpwm.h" +#include "soc/mcpwm_reg.h" +#include "soc/mcpwm_struct.h" + +// empty motor slot +#define _EMPTY_SLOT -20 +#define _TAKEN_SLOT -21 + +// ABI bus frequency - would be better to take it from somewhere +// but I did nto find a good exposed variable +#define _MCPWM_FREQ 160e6f + +// preferred pwm resolution default +#define _PWM_RES_DEF 4096 +// min resolution +#define _PWM_RES_MIN 3000 +// max resolution +#define _PWM_RES_MAX 8000 +// pwm frequency +#define _PWM_FREQUENCY 25000 // default +#define _PWM_FREQUENCY_MAX 50000 // mqx + +// structure containing motor slot configuration +// this library supports up to 4 motors +typedef struct { + int pinA; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator; + mcpwm_io_signals_t mcpwm_a; + mcpwm_io_signals_t mcpwm_b; + mcpwm_io_signals_t mcpwm_c; +} bldc_3pwm_motor_slots_t; + +typedef struct { + int pin1A; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + mcpwm_io_signals_t mcpwm_1a; + mcpwm_io_signals_t mcpwm_1b; + mcpwm_io_signals_t mcpwm_2a; + mcpwm_io_signals_t mcpwm_2b; +} stepper_4pwm_motor_slots_t; + +typedef struct { + int pin1pwm; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator; + mcpwm_io_signals_t mcpwm_a; + mcpwm_io_signals_t mcpwm_b; +} stepper_2pwm_motor_slots_t; + +typedef struct { + int pinAH; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + mcpwm_io_signals_t mcpwm_ah; + mcpwm_io_signals_t mcpwm_bh; + mcpwm_io_signals_t mcpwm_ch; + mcpwm_io_signals_t mcpwm_al; + mcpwm_io_signals_t mcpwm_bl; + mcpwm_io_signals_t mcpwm_cl; +} bldc_6pwm_motor_slots_t; + + +typedef struct ESP32MCPWMDriverParams { + long pwm_frequency; + mcpwm_dev_t* mcpwm_dev; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + float deadtime; +} ESP32MCPWMDriverParams; + + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp new file mode 100644 index 0000000..a454c05 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp @@ -0,0 +1,185 @@ +#include "../../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && ( !defined(SOC_MCPWM_SUPPORTED) || defined(SIMPLEFOC_ESP32_USELEDC) ) + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP32 LEDC driver") +#pragma message("") + +#include "driver/ledc.h" + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 38000 // 38khz max to be able to have 10 bit pwm resolution +#define _PWM_RES_BIT 10 // 10 bir resolution +#define _PWM_RES 1023 // 2^10-1 = 1023-1 + + +// empty motor slot +#define _EMPTY_SLOT -20 +#define _TAKEN_SLOT -21 + +// figure out how many ledc channels are avaible +// esp32 - 2x8=16 +// esp32s2 - 8 +// esp32c3 - 6 +#include "soc/soc_caps.h" +#ifdef SOC_LEDC_SUPPORT_HS_MODE +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1) +#else +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM) +#endif + + +// current channel stack index +// support for multiple motors +// esp32 has 16 channels +// esp32s2 has 8 channels +// esp32c3 has 6 channels +int channel_index = 0; + + + + +typedef struct ESP32LEDCDriverParams { + int channels[6]; + long pwm_frequency; +} ESP32LEDCDriverParams; + + + + + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin, const int channel){ + ledcSetup(channel, freq, _PWM_RES_BIT ); + ledcAttachPin(pin, channel); +} + + + + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 1 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + + + + + +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 2 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 3 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + int ch3 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + _setHighFrequency(pwm_frequency, pinC, ch3); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2, ch3 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 4 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + int ch3 = channel_index++; + int ch4 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + _setHighFrequency(pwm_frequency, pinC, ch3); + _setHighFrequency(pwm_frequency, pinD, ch4); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2, ch3, ch4 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); +} + + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_b, 0, _PWM_RES)); +} + + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_b, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[2], _constrain(_PWM_RES*dc_c, 0, _PWM_RES)); +} + + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_1a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_1b, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[2], _constrain(_PWM_RES*dc_2a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[3], _constrain(_PWM_RES*dc_2b, 0, _PWM_RES)); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp new file mode 100644 index 0000000..0dc23c1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp @@ -0,0 +1,431 @@ +#include "esp32_driver_mcpwm.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + +#ifndef SIMPLEFOC_ESP32_HW_DEADTIME + #define SIMPLEFOC_ESP32_HW_DEADTIME true // TODO: Change to false when sw-deadtime & phase_state is approved ready for general use. +#endif + +// define bldc motor slots array +bldc_3pwm_motor_slots_t esp32_bldc_3pwm_motor_slots[4] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM0A, MCPWM1A, MCPWM2A}, // 1st motor will be MCPWM0 channel A + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_B, MCPWM0B, MCPWM1B, MCPWM2B}, // 2nd motor will be MCPWM0 channel B + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM0A, MCPWM1A, MCPWM2A}, // 3rd motor will be MCPWM1 channel A + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_B, MCPWM0B, MCPWM1B, MCPWM2B} // 4th motor will be MCPWM1 channel B + }; + +// define stepper motor slots array +bldc_6pwm_motor_slots_t esp32_bldc_6pwm_motor_slots[2] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM2A, MCPWM0B, MCPWM1B, MCPWM2B}, // 1st motor will be on MCPWM0 + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM2A, MCPWM0B, MCPWM1B, MCPWM2B}, // 1st motor will be on MCPWM0 + }; + +// define 4pwm stepper motor slots array +stepper_4pwm_motor_slots_t esp32_stepper_4pwm_motor_slots[2] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM0B, MCPWM1B}, // 1st motor will be on MCPWM0 + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM0B, MCPWM1B}, // 1st motor will be on MCPWM1 + }; + +// define 2pwm stepper motor slots array +stepper_2pwm_motor_slots_t esp32_stepper_2pwm_motor_slots[4] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM0A, MCPWM1A}, // 1st motor will be MCPWM0 channel A + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_B, MCPWM0B, MCPWM1B}, // 2nd motor will be MCPWM0 channel B + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM0A, MCPWM1A}, // 3rd motor will be MCPWM1 channel A + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_B, MCPWM0B, MCPWM1B} // 4th motor will be MCPWM1 channel B + }; + + +// configuring high frequency pwm timer +// a lot of help from this post from Paul Gauld +// https://hackaday.io/project/169905-esp-32-bldc-robot-actuator-controller +// a short tutorial for v2.0.1+ +// https://kzhead.info/sun/q6uFktWgkYeMeZ8/esp32-arduino.html +// +void _configureTimerFrequency(long pwm_frequency, mcpwm_dev_t* mcpwm_num, mcpwm_unit_t mcpwm_unit, float dead_zone = NOT_SET){ + + mcpwm_config_t pwm_config; + pwm_config.counter_mode = MCPWM_UP_DOWN_COUNTER; // Up-down counter (triangle wave) + pwm_config.duty_mode = (_isset(dead_zone) || SIMPLEFOC_PWM_ACTIVE_HIGH == true) ? MCPWM_DUTY_MODE_0 : MCPWM_DUTY_MODE_1; // Normally Active HIGH (MCPWM_DUTY_MODE_0) + pwm_config.frequency = 2*pwm_frequency; // set the desired freq - just a placeholder for now https://github.com/simplefoc/Arduino-FOC/issues/76 + mcpwm_init(mcpwm_unit, MCPWM_TIMER_0, &pwm_config); //Configure PWM0A & PWM0B with above settings + mcpwm_init(mcpwm_unit, MCPWM_TIMER_1, &pwm_config); //Configure PWM1A & PWM1B with above settings + mcpwm_init(mcpwm_unit, MCPWM_TIMER_2, &pwm_config); //Configure PWM2A & PWM2B with above settings + + if (_isset(dead_zone)){ + // dead zone is configured + + // When using hardware deadtime, setting the phase_state parameter is not supported. + #if SIMPLEFOC_ESP32_HW_DEADTIME == true + float dead_time = (float)(_MCPWM_FREQ / (pwm_frequency)) * dead_zone; + mcpwm_deadtime_type_t pwm_mode; + if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true)) {pwm_mode = MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE;} // Normal, noninverting driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false)){pwm_mode = MCPWM_ACTIVE_HIGH_MODE;} // Inverted lowside driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true)) {pwm_mode = MCPWM_ACTIVE_LOW_MODE;} // Inverted highside driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false)){pwm_mode = MCPWM_ACTIVE_LOW_COMPLIMENT_MODE;} // Inverted low- & highside driver. Caution: This may short the FETs on reset of the ESP32, as both pins get pulled low! + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_0, pwm_mode, dead_time/2.0, dead_time/2.0); + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_1, pwm_mode, dead_time/2.0, dead_time/2.0); + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_2, pwm_mode, dead_time/2.0, dead_time/2.0); + #else // Software deadtime + for (int i = 0; i < 3; i++){ + if (SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_A, MCPWM_DUTY_MODE_0);} // Normal, noninverted highside + else if (SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_A, MCPWM_DUTY_MODE_1);} // Inverted highside driver + + if (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_B, MCPWM_DUTY_MODE_1);} // Normal, complementary lowside + else if (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_B, MCPWM_DUTY_MODE_0);} // Inverted lowside driver + } + #endif + + } + _delay(100); + + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_0); + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_1); + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_2); + + // manual configuration due to the lack of config flexibility in mcpwm_init() + mcpwm_num->clk_cfg.clk_prescale = 0; + // calculate prescaler and period + // step 1: calculate the prescaler using the default pwm resolution + // prescaler = bus_freq / (pwm_freq * default_pwm_res) - 1 + int prescaler = ceil((double)_MCPWM_FREQ / (double)_PWM_RES_DEF / 2.0f / (double)pwm_frequency) - 1; + // constrain prescaler + prescaler = _constrain(prescaler, 0, 128); + // now calculate the real resolution timer period necessary (pwm resolution) + // pwm_res = bus_freq / (pwm_freq * (prescaler + 1)) + int resolution_corrected = (double)_MCPWM_FREQ / 2.0f / (double)pwm_frequency / (double)(prescaler + 1); + // if pwm resolution too low lower the prescaler + if(resolution_corrected < _PWM_RES_MIN && prescaler > 0 ) + resolution_corrected = (double)_MCPWM_FREQ / 2.0f / (double)pwm_frequency / (double)(--prescaler + 1); + resolution_corrected = _constrain(resolution_corrected, _PWM_RES_MIN, _PWM_RES_MAX); + + // set prescaler + mcpwm_num->timer[0].timer_cfg0.timer_prescale = prescaler; + mcpwm_num->timer[1].timer_cfg0.timer_prescale = prescaler; + mcpwm_num->timer[2].timer_cfg0.timer_prescale = prescaler; + _delay(1); + //set period + mcpwm_num->timer[0].timer_cfg0.timer_period = resolution_corrected; + mcpwm_num->timer[1].timer_cfg0.timer_period = resolution_corrected; + mcpwm_num->timer[2].timer_cfg0.timer_period = resolution_corrected; + _delay(1); + mcpwm_num->timer[0].timer_cfg0.timer_period_upmethod = 0; + mcpwm_num->timer[1].timer_cfg0.timer_period_upmethod = 0; + mcpwm_num->timer[2].timer_cfg0.timer_period_upmethod = 0; + _delay(1); + // _delay(1); + //restart the timers + mcpwm_start(mcpwm_unit, MCPWM_TIMER_0); + mcpwm_start(mcpwm_unit, MCPWM_TIMER_1); + mcpwm_start(mcpwm_unit, MCPWM_TIMER_2); + _delay(1); + + mcpwm_sync_config_t sync_conf = { + .sync_sig = MCPWM_SELECT_TIMER0_SYNC, + .timer_val = 0, + .count_direction = MCPWM_TIMER_DIRECTION_UP + }; + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_0, &sync_conf); + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_1, &sync_conf); + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_2, &sync_conf); + + // Enable sync event for all timers to be the TEZ of timer0 + mcpwm_set_timer_sync_output(mcpwm_unit, MCPWM_TIMER_0, MCPWM_SWSYNC_SOURCE_TEZ); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +// supports Arudino/ATmega328, STM32 and ESP32 +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + + stepper_2pwm_motor_slots_t m_slot = {}; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm = pinA; + m_slot = esp32_stepper_2pwm_motor_slots[slot_num]; + break; + } + } + + // disable all the slots with the same MCPWM + // disable 3pwm bldc motor which would go in the same slot + esp32_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + if( slot_num < 2 ){ + // slot 0 of the 4pwm stepper + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slot 1 of the stepper + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_b, pinB); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +// supports Arudino/ATmega328, STM32 and ESP32 +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + + bldc_3pwm_motor_slots_t m_slot = {}; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(esp32_bldc_3pwm_motor_slots[slot_num].pinA == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_bldc_3pwm_motor_slots[slot_num].pinA = pinA; + m_slot = esp32_bldc_3pwm_motor_slots[slot_num]; + break; + } + } + // disable all the slots with the same MCPWM + // disable 2pwm steppr motor which would go in the same slot + esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm = _TAKEN_SLOT; + if( slot_num < 2 ){ + // slot 0 of the 4pwm stepper + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slot 1 of the stepper + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_b, pinB); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_c, pinC); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + stepper_4pwm_motor_slots_t m_slot = {}; + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(esp32_stepper_4pwm_motor_slots[slot_num].pin1A == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_stepper_4pwm_motor_slots[slot_num].pin1A = pinA; + m_slot = esp32_stepper_4pwm_motor_slots[slot_num]; + break; + } + } + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slots 0 and 1 of the 2pwm stepper taken + esp32_stepper_2pwm_motor_slots[0].pin1pwm = _TAKEN_SLOT; + esp32_stepper_2pwm_motor_slots[1].pin1pwm = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slots 2 and 3 of the 2pwm stepper taken + esp32_stepper_2pwm_motor_slots[2].pin1pwm = _TAKEN_SLOT; + esp32_stepper_2pwm_motor_slots[3].pin1pwm = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_1a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_1b, pinB); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_2a, pinC); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_2b, pinD); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator1, + .mcpwm_operator2 = m_slot.mcpwm_operator2 + }; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_b*100.0); +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_b*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_c*100.0); +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_1a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_1b*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator2, dc_2a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator2, dc_2b*100.0); +} + + + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = 20000; // default frequency 20khz - centered pwm has twice lower frequency + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max - centered pwm has twice lower frequency + bldc_6pwm_motor_slots_t m_slot = {}; + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(esp32_bldc_6pwm_motor_slots[slot_num].pinAH == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_bldc_6pwm_motor_slots[slot_num].pinAH = pinA_h; + m_slot = esp32_bldc_6pwm_motor_slots[slot_num]; + break; + } + } + // if no slots available + if(slot_num >= 2) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + } + + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_ah, pinA_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_al, pinA_l); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_bh, pinB_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_bl, pinB_l); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_ch, pinC_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_cl, pinC_l); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit, dead_zone); + // return + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator1, + .mcpwm_operator2 = m_slot.mcpwm_operator2, + .deadtime = _isset(dead_zone) ? dead_zone : 0 + }; + return params; +} + + + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + // set the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100.0] + #if SIMPLEFOC_ESP32_HW_DEADTIME == true + // Hardware deadtime does deadtime insertion internally + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_A, dc_a*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_B, dc_a*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_A, dc_b*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_B, dc_b*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_A, dc_c*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_B, dc_c*100.0f); + _UNUSED(phase_state); + #else + float deadtime = 0.5f*((ESP32MCPWMDriverParams*)params)->deadtime; + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_A, (phase_state[0] == PHASE_ON || phase_state[0] == PHASE_HI) ? _constrain(dc_a-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_B, (phase_state[0] == PHASE_ON || phase_state[0] == PHASE_LO) ? _constrain(dc_a+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_A, (phase_state[1] == PHASE_ON || phase_state[1] == PHASE_HI) ? _constrain(dc_b-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_B, (phase_state[1] == PHASE_ON || phase_state[1] == PHASE_LO) ? _constrain(dc_b+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_A, (phase_state[2] == PHASE_ON || phase_state[2] == PHASE_HI) ? _constrain(dc_c-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_B, (phase_state[2] == PHASE_ON || phase_state[2] == PHASE_LO) ? _constrain(dc_c+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + #endif +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp new file mode 100644 index 0000000..23d9411 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp @@ -0,0 +1,121 @@ +#include "../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP8266) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP8266") +#pragma message("") + + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin){ + analogWrite(pin, 0); + analogWriteFreq(freq); +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + _setHighFrequency(pwm_frequency, pinD); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp new file mode 100644 index 0000000..b6bc2f0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp @@ -0,0 +1,125 @@ + +#include "../hardware_api.h" + +// if the mcu doen't have defiend analogWrite +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && !defined(analogWrite) + __attribute__((weak)) void analogWrite(uint8_t pin, int value){ }; +#endif + +// function setting the high pwm frequency to the supplied pin +// - Stepper motor - 1PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure1PWM(long pwm_frequency, const int pinA) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +__attribute__((weak)) void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + _UNUSED(pwm_frequency); + _UNUSED(dead_zone); + _UNUSED(pinA_h); + _UNUSED(pinA_l); + _UNUSED(pinB_h); + _UNUSED(pinB_l); + _UNUSED(pinC_h); + _UNUSED(pinC_l); + + return SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 1PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +__attribute__((weak)) void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _UNUSED(dc_a); + _UNUSED(dc_b); + _UNUSED(dc_c); + _UNUSED(phase_state); + _UNUSED(params); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp new file mode 100644 index 0000000..a20b828 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp @@ -0,0 +1,397 @@ + +#include "../hardware_api.h" + +#if defined(NRF52_SERIES) + +#pragma message("") +#pragma message("SimpleFOC: compiling for NRF52") +#pragma message("") + + +#define PWM_CLK (16000000) +#define PWM_FREQ (40000) +#define PWM_RESOLUTION (PWM_CLK/PWM_FREQ) +#define PWM_MAX_FREQ (62500) +#define DEAD_ZONE (250) // in ns +#define DEAD_TIME (DEAD_ZONE / (PWM_RESOLUTION * 0.25 * 62.5)) // 62.5ns resolution of PWM + +#ifdef NRF_PWM3 +#define PWM_COUNT 4 +#else +#define PWM_COUNT 3 +#endif + +// empty motor slot +#define _EMPTY_SLOT (-0xAA) +#define _TAKEN_SLOT (-0x55) + +int pwm_range; + + +static NRF_PWM_Type* pwms[PWM_COUNT] = { + NRF_PWM0, + NRF_PWM1, + NRF_PWM2, + #ifdef NRF_PWM3 + NRF_PWM3 + #endif +}; + +typedef struct { + int pinA; + NRF_PWM_Type* mcpwm; + uint16_t mcpwm_channel_sequence[4]; +} bldc_3pwm_motor_slots_t; + +typedef struct { + int pin1A; + NRF_PWM_Type* mcpwm; + uint16_t mcpwm_channel_sequence[4]; +} stepper_motor_slots_t; + +typedef struct { + int pinAH; + NRF_PWM_Type* mcpwm1; + NRF_PWM_Type* mcpwm2; + uint16_t mcpwm_channel_sequence[8]; +} bldc_6pwm_motor_slots_t; + +// define bldc motor slots array +bldc_3pwm_motor_slots_t nrf52_bldc_3pwm_motor_slots[4] = { + {_EMPTY_SLOT, pwms[0], {0,0,0,0}},// 1st motor will be PWM0 + {_EMPTY_SLOT, pwms[1], {0,0,0,0}},// 2nd motor will be PWM1 + {_EMPTY_SLOT, pwms[2], {0,0,0,0}},// 3rd motor will be PWM2 + {_EMPTY_SLOT, pwms[3], {0,0,0,0}} // 4th motor will be PWM3 + }; + +// define stepper motor slots array +stepper_motor_slots_t nrf52_stepper_motor_slots[4] = { + {_EMPTY_SLOT, pwms[0], {0,0,0,0}},// 1st motor will be on PWM0 + {_EMPTY_SLOT, pwms[1], {0,0,0,0}},// 1st motor will be on PWM1 + {_EMPTY_SLOT, pwms[2], {0,0,0,0}},// 1st motor will be on PWM2 + {_EMPTY_SLOT, pwms[3], {0,0,0,0}} // 1st motor will be on PWM3 + }; + +// define BLDC motor slots array +bldc_6pwm_motor_slots_t nrf52_bldc_6pwm_motor_slots[2] = { + {_EMPTY_SLOT, pwms[0], pwms[1], {0,0,0,0,0,0,0,0}},// 1st motor will be on PWM0 & PWM1 + {_EMPTY_SLOT, pwms[2], pwms[3], {0,0,0,0,0,0,0,0}} // 2nd motor will be on PWM1 & PWM2 + }; + + + +typedef struct NRF52DriverParams { + union { + bldc_3pwm_motor_slots_t* slot3pwm; + bldc_6pwm_motor_slots_t* slot6pwm; + stepper_motor_slots_t* slotstep; + } slot; + long pwm_frequency; + float dead_time; +} NRF52DriverParams; + + + + +// configuring high frequency pwm timer +void _configureHwPwm(NRF_PWM_Type* mcpwm1, NRF_PWM_Type* mcpwm2){ + + mcpwm1->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos); + mcpwm1->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos); + mcpwm1->MODE = (PWM_MODE_UPDOWN_UpAndDown << PWM_MODE_UPDOWN_Pos); + mcpwm1->COUNTERTOP = pwm_range; //pwm freq. + mcpwm1->LOOP = (PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos); + mcpwm1->DECODER = ((uint32_t)PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos) | ((uint32_t)PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos); + mcpwm1->SEQ[0].REFRESH = 0; + mcpwm1->SEQ[0].ENDDELAY = 0; + + if(mcpwm1 != mcpwm2){ + mcpwm2->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos); + mcpwm2->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos); + mcpwm2->MODE = (PWM_MODE_UPDOWN_UpAndDown << PWM_MODE_UPDOWN_Pos); + mcpwm2->COUNTERTOP = pwm_range; //pwm freq. + mcpwm2->LOOP = (PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos); + mcpwm2->DECODER = ((uint32_t)PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos) | ((uint32_t)PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos); + mcpwm2->SEQ[0].REFRESH = 0; + mcpwm2->SEQ[0].ENDDELAY = 0; + }else{ + mcpwm1->MODE = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos); + } +} + + + +// can we support it using the generic driver on this MCU? Commented out to fall back to generic driver for 2-pwm +// void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { +// return SIMPLEFOC_DRIVER_INIT_FAILED; // not supported +// } + + + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz for a resolution of 800 + else pwm_frequency = _constrain(pwm_frequency, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + + int pA = digitalPinToPinName(pinA); //g_ADigitalPinMap[pinA]; + int pB = digitalPinToPinName(pinB); //g_ADigitalPinMap[pinB]; + int pC = digitalPinToPinName(pinC); //g_ADigitalPinMap[pinC]; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(nrf52_bldc_3pwm_motor_slots[slot_num].pinA == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = pinA; + break; + } + } + // if no slots available + if(slot_num >= 4) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if(slot_num < 2){ + // slot 0 of the stepper + nrf52_stepper_motor_slots[slot_num].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~1UL; + }else{ + // slot 1 of the stepper + nrf52_stepper_motor_slots[slot_num].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~2UL; + } + + // configure pwm outputs + + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[0] = pA; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[1] = pB; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[2] = pC; + + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->SEQ[0].PTR = (uint32_t)&nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->SEQ[0].CNT = 4; + + // configure the pwm + _configureHwPwm(nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm, nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slot3pwm = &(nrf52_bldc_3pwm_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + return params; +} + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC, const int pinD) { + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz for a resolution of 800 + else pwm_frequency = _constrain(pwm_frequency, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + + int pA = digitalPinToPinName(pinA); //g_ADigitalPinMap[pinA]; + int pB = digitalPinToPinName(pinB); //g_ADigitalPinMap[pinB]; + int pC = digitalPinToPinName(pinC); //g_ADigitalPinMap[pinC]; + int pD = digitalPinToPinName(pinD); //g_ADigitalPinMap[pinD]; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(nrf52_stepper_motor_slots[slot_num].pin1A == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_stepper_motor_slots[slot_num].pin1A = pinA; + break; + } + } + // if no slots available + if (slot_num >= 4) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if (slot_num < 2){ + // slots 0 and 1 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~1UL; + }else{ + // slots 2 and 3 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~2UL; + } + + // configure pwm outputs + + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[0] = pA; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[1] = pB; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[2] = pC; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[3] = pD; + + nrf52_stepper_motor_slots[slot_num].mcpwm->SEQ[0].PTR = (uint32_t)&nrf52_stepper_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_stepper_motor_slots[slot_num].mcpwm->SEQ[0].CNT = 4; + + // configure the pwm + _configureHwPwm(nrf52_stepper_motor_slots[slot_num].mcpwm, nrf52_stepper_motor_slots[slot_num].mcpwm); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slotstep = &(nrf52_stepper_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,range] + bldc_3pwm_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slot3pwm; + p->mcpwm_channel_sequence[0] = (int)(dc_a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(dc_b * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[2] = (int)(dc_c * pwm_range) | 0x8000; + + p->mcpwm->TASKS_SEQSTART[0] = 1; +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + + stepper_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slotstep; + p->mcpwm_channel_sequence[0] = (int)(dc_1a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(dc_1b * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[2] = (int)(dc_2a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[3] = (int)(dc_2b * pwm_range) | 0x8000; + + p->mcpwm->TASKS_SEQSTART[0] = 1; +} + +/* Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +*/ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz - centered pwm has twice lower frequency for a resolution of 400 + else pwm_frequency = _constrain(pwm_frequency*2, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max => 31.25kHz for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + pwm_range /= 2; // scale the frequency (centered PWM) + + float dead_time; + if (dead_zone != NOT_SET){ + dead_time = dead_zone/2; + }else{ + dead_time = DEAD_TIME/2; + } + + int pA_l = digitalPinToPinName(pinA_l); //g_ADigitalPinMap[pinA_l]; + int pA_h = digitalPinToPinName(pinA_h); //g_ADigitalPinMap[pinA_h]; + int pB_l = digitalPinToPinName(pinB_l); //g_ADigitalPinMap[pinB_l]; + int pB_h = digitalPinToPinName(pinB_h); //g_ADigitalPinMap[pinB_h]; + int pC_l = digitalPinToPinName(pinC_l); //g_ADigitalPinMap[pinC_l]; + int pC_h = digitalPinToPinName(pinC_h); //g_ADigitalPinMap[pinC_h]; + + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(nrf52_bldc_6pwm_motor_slots[slot_num].pinAH == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_bldc_6pwm_motor_slots[slot_num].pinAH = pinA_h; + break; + } + } + // if no slots available + if(slot_num >= 2) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + nrf52_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slot 0 and 1 of the stepper + nrf52_stepper_motor_slots[0].pin1A = _TAKEN_SLOT; + nrf52_stepper_motor_slots[1].pin1A = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + nrf52_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slot 1 of the stepper + nrf52_stepper_motor_slots[2].pin1A = _TAKEN_SLOT; + nrf52_stepper_motor_slots[3].pin1A = _TAKEN_SLOT; + } + + // Configure pwm outputs + + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[0] = pA_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[1] = pA_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[2] = pB_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[3] = pB_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->SEQ[0].PTR = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->SEQ[0].CNT = 4; + + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->PSEL.OUT[0] = pC_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->PSEL.OUT[1] = pC_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->SEQ[0].PTR = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm_channel_sequence[4]; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->SEQ[0].CNT = 4; + + // Initializing the PPI peripheral for sync the pwm slots + + NRF_PPI->CH[slot_num].EEP = (uint32_t)&NRF_EGU0->EVENTS_TRIGGERED[0]; + NRF_PPI->CH[slot_num].TEP = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->TASKS_SEQSTART[0]; + NRF_PPI->FORK[slot_num].TEP = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->TASKS_SEQSTART[0]; + NRF_PPI->CHEN = 1UL << slot_num; + + // configure the pwm type + _configureHwPwm(nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1, nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slot6pwm = &(nrf52_bldc_6pwm_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + params->dead_time = dead_time; + return params; +} + + + + +/* Function setting the duty cycle to the pwm pin +// - BLDC driver - 6PWM setting +// - hardware specific +*/ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + bldc_6pwm_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slot6pwm; + float dead_time = ((NRF52DriverParams*)params)->dead_time; + p->mcpwm_channel_sequence[0] = (int)(_constrain(dc_a-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(_constrain(dc_a+dead_time,0,1)*pwm_range); + p->mcpwm_channel_sequence[2] = (int)(_constrain(dc_b-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[3] = (int)(_constrain(dc_b+dead_time,0,1)*pwm_range); + p->mcpwm_channel_sequence[4] = (int)(_constrain(dc_c-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[5] = (int)(_constrain(dc_c+dead_time,0,1)*pwm_range); + NRF_EGU0->TASKS_TRIGGER[0] = 1; + + _UNUSED(phase_state); +} + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp new file mode 100644 index 0000000..ad16646 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp @@ -0,0 +1,545 @@ + +#include "../hardware_api.h" + +#if defined(TARGET_PORTENTA_H7) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Portenta_H7") +#pragma message("") + + +#include "pwmout_api.h" +#include "pinDefinitions.h" +#include "platform/mbed_critical.h" +#include "platform/mbed_power_mgmt.h" +#include "platform/mbed_assert.h" +#include "PeripheralPins.h" +#include "pwmout_device.h" + +// default pwm parameters +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + + +// // 6pwm parameters +// #define _HARDWARE_6PWM 1 +// #define _SOFTWARE_6PWM 0 +// #define _ERROR_6PWM -1 + + + +typedef struct PortentaDriverParams { + pwmout_t pins[4]; + long pwm_frequency; +// float dead_zone; +} PortentaDriverParams; + + + +/* Convert STM32 Cube HAL channel to LL channel */ +uint32_t _TIM_ChannelConvert_HAL2LL(uint32_t channel, pwmout_t *obj) +{ +#if !defined(PWMOUT_INVERTED_NOT_SUPPORTED) + if (obj->inverted) { + switch (channel) { + case TIM_CHANNEL_1 : + return LL_TIM_CHANNEL_CH1N; + case TIM_CHANNEL_2 : + return LL_TIM_CHANNEL_CH2N; + case TIM_CHANNEL_3 : + return LL_TIM_CHANNEL_CH3N; +#if defined(LL_TIM_CHANNEL_CH4N) + case TIM_CHANNEL_4 : + return LL_TIM_CHANNEL_CH4N; +#endif + default : /* Optional */ + return 0; + } + } else +#endif + { + switch (channel) { + case TIM_CHANNEL_1 : + return LL_TIM_CHANNEL_CH1; + case TIM_CHANNEL_2 : + return LL_TIM_CHANNEL_CH2; + case TIM_CHANNEL_3 : + return LL_TIM_CHANNEL_CH3; + case TIM_CHANNEL_4 : + return LL_TIM_CHANNEL_CH4; + default : /* Optional */ + return 0; + } + } +} + + + +// int _pwm_init(pwmout_t *obj, uint32_t pin, long frequency){ +// return _pwm_init(obj, digitalPinToPinName(pin), frequency); +// } + +int _pwm_init(pwmout_t *obj, uint32_t pin, long frequency){ + int peripheral = (int)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM); + int function = (int)pinmap_find_function(digitalPinToPinName(pin), PinMap_PWM); + + const PinMap static_pinmap = {digitalPinToPinName(pin), peripheral, function}; + + pwmout_init_direct(obj, &static_pinmap); + + TIM_HandleTypeDef TimHandle; + TimHandle.Instance = (TIM_TypeDef *)(obj->pwm); + RCC_ClkInitTypeDef RCC_ClkInitStruct; + uint32_t PclkFreq = 0; + uint32_t APBxCLKDivider = RCC_HCLK_DIV1; + uint8_t i = 0; + + + __HAL_TIM_DISABLE(&TimHandle); + + // Get clock configuration + // Note: PclkFreq contains here the Latency (not used after) + HAL_RCC_GetClockConfig(&RCC_ClkInitStruct, &PclkFreq); + + /* Parse the pwm / apb mapping table to find the right entry */ + while (pwm_apb_map_table[i].pwm != obj->pwm) i++; + // sanity check + if (pwm_apb_map_table[i].pwm == 0) return -1; + + + if (pwm_apb_map_table[i].pwmoutApb == PWMOUT_ON_APB1) { + PclkFreq = HAL_RCC_GetPCLK1Freq(); + APBxCLKDivider = RCC_ClkInitStruct.APB1CLKDivider; + } else { +#if !defined(PWMOUT_APB2_NOT_SUPPORTED) + PclkFreq = HAL_RCC_GetPCLK2Freq(); + APBxCLKDivider = RCC_ClkInitStruct.APB2CLKDivider; +#endif + } + + long period_us = 500000.0/((float)frequency); + /* By default use, 1us as SW pre-scaler */ + obj->prescaler = 1; + // TIMxCLK = PCLKx when the APB prescaler = 1 else TIMxCLK = 2 * PCLKx + if (APBxCLKDivider == RCC_HCLK_DIV1) { + TimHandle.Init.Prescaler = (((PclkFreq) / 1000000)) - 1; // 1 us tick + } else { + TimHandle.Init.Prescaler = (((PclkFreq * 2) / 1000000)) - 1; // 1 us tick + } + TimHandle.Init.Period = (period_us - 1); + + /* In case period or pre-scalers are out of range, loop-in to get valid values */ + while ((TimHandle.Init.Period > 0xFFFF) || (TimHandle.Init.Prescaler > 0xFFFF)) { + obj->prescaler = obj->prescaler * 2; + if (APBxCLKDivider == RCC_HCLK_DIV1) { + TimHandle.Init.Prescaler = (((PclkFreq) / 1000000) * obj->prescaler) - 1; + } else { + TimHandle.Init.Prescaler = (((PclkFreq * 2) / 1000000) * obj->prescaler) - 1; + } + TimHandle.Init.Period = (period_us - 1) / obj->prescaler; + /* Period decreases and prescaler increases over loops, so check for + * possible out of range cases */ + if ((TimHandle.Init.Period < 0xFFFF) && (TimHandle.Init.Prescaler > 0xFFFF)) { + break; + } + } + + TimHandle.Init.ClockDivision = 0; + TimHandle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; // center aligned + + if (HAL_TIM_PWM_Init(&TimHandle) != HAL_OK) { + return -1; + } + + TIM_OC_InitTypeDef sConfig; + // Configure channels + sConfig.OCMode = TIM_OCMODE_PWM1; + sConfig.Pulse = obj->pulse / obj->prescaler; + sConfig.OCPolarity = TIM_OCPOLARITY_HIGH; + sConfig.OCFastMode = TIM_OCFAST_DISABLE; +#if defined(TIM_OCIDLESTATE_RESET) + sConfig.OCIdleState = TIM_OCIDLESTATE_RESET; +#endif +#if defined(TIM_OCNIDLESTATE_RESET) + sConfig.OCNPolarity = TIM_OCNPOLARITY_HIGH; + sConfig.OCNIdleState = TIM_OCNIDLESTATE_RESET; +#endif + + int channel = 0; + switch (obj->channel) { + case 1: + channel = TIM_CHANNEL_1; + break; + case 2: + channel = TIM_CHANNEL_2; + break; + case 3: + channel = TIM_CHANNEL_3; + break; + case 4: + channel = TIM_CHANNEL_4; + break; + default: + return -1; + } + + if (LL_TIM_CC_IsEnabledChannel(TimHandle.Instance, _TIM_ChannelConvert_HAL2LL(channel, obj)) == 0) { + // If channel is not enabled, proceed to channel configuration + if (HAL_TIM_PWM_ConfigChannel(&TimHandle, &sConfig, channel) != HAL_OK) { + return -1; + } + } + + // Save for future use + obj->period = period_us; +#if !defined(PWMOUT_INVERTED_NOT_SUPPORTED) + if (obj->inverted) { + HAL_TIMEx_PWMN_Start(&TimHandle, channel); + } else +#endif + { + HAL_TIM_PWM_Start(&TimHandle, channel); + } + + return 0; +} + +// setting pwm to hardware pin - instead analogWrite() +void _pwm_write(pwmout_t *obj, float value){ + TIM_HandleTypeDef TimHandle; + int channel = 0; + + TimHandle.Instance = (TIM_TypeDef *)(obj->pwm); + + if (value < (float)0.0) { + value = 0.0; + } else if (value > (float)1.0) { + value = 1.0; + } + + obj->pulse = (uint32_t)((float)obj->period * value + 0.5); + + switch (obj->channel) { + case 1: + channel = TIM_CHANNEL_1; + break; + case 2: + channel = TIM_CHANNEL_2; + break; + case 3: + channel = TIM_CHANNEL_3; + break; + case 4: + channel = TIM_CHANNEL_4; + break; + default: + return; + } + + // If channel already enabled, only update compare value to avoid glitch + __HAL_TIM_SET_COMPARE(&TimHandle, channel, obj->pulse / obj->prescaler); +} + +// init low side pin +// HardwareTimer* _initPinPWMLow(uint32_t PWM_freq, int ulPin) +// { +// PinName pin = digitalPinToPinName(ulPin); +// TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM); +// uint32_t index = get_timer_index(Instance); + +// if (HardwareTimer_Handle[index] == NULL) { +// HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM)); +// HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; +// HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); +// TIM_OC_InitTypeDef sConfigOC = TIM_OC_InitTypeDef(); +// sConfigOC.OCMode = TIM_OCMODE_PWM2; +// sConfigOC.Pulse = 100; +// sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW; +// sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; +// sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; +// sConfigOC.OCIdleState = TIM_OCIDLESTATE_SET; +// sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; +// uint32_t channel = STM_PIN_CHANNEL(pinmap_function(pin, PinMap_PWM)); +// HAL_TIM_PWM_ConfigChannel(&(HardwareTimer_Handle[index]->handle), &sConfigOC, channel); +// } +// HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); +// uint32_t channel = STM_PIN_CHANNEL(pinmap_function(pin, PinMap_PWM)); +// HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM2, pin); +// HT->setOverflow(PWM_freq, HERTZ_FORMAT); +// HT->pause(); +// HT->refresh(); +// return HT; +// } + + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2){ + TIM_HandleTypeDef TimHandle1, TimHandle2; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); +} + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2, pwmout_t *t3){ + TIM_HandleTypeDef TimHandle1, TimHandle2, TimHandle3; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + TimHandle3.Instance = (TIM_TypeDef *)(t3->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_DISABLE(&TimHandle3); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle3); +} + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2, pwmout_t *t3, pwmout_t *t4){ + TIM_HandleTypeDef TimHandle1, TimHandle2, TimHandle3, TimHandle4; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + TimHandle3.Instance = (TIM_TypeDef *)(t3->pwm); + TimHandle4.Instance = (TIM_TypeDef *)(t4->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_DISABLE(&TimHandle3); + __HAL_TIM_DISABLE(&TimHandle4); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle3); + __HAL_TIM_ENABLE(&TimHandle4); +} + +// // configure hardware 6pwm interface only one timer with inverted channels +// HardwareTimer* _initHardware6PWMInterface(uint32_t PWM_freq, float dead_zone, int pinA_h, int pinA_l, int pinB_h, int pinB_l, int pinC_h, int pinC_l) +// { +// PinName uhPinName = digitalPinToPinName(pinA_h); +// PinName ulPinName = digitalPinToPinName(pinA_l); +// PinName vhPinName = digitalPinToPinName(pinB_h); +// PinName vlPinName = digitalPinToPinName(pinB_l); +// PinName whPinName = digitalPinToPinName(pinC_h); +// PinName wlPinName = digitalPinToPinName(pinC_l); + +// TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(uhPinName, PinMap_PWM); + +// uint32_t index = get_timer_index(Instance); + +// if (HardwareTimer_Handle[index] == NULL) { +// HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef *)pinmap_peripheral(uhPinName, PinMap_PWM)); +// HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; +// HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); +// ((HardwareTimer *)HardwareTimer_Handle[index]->__this)->setOverflow(PWM_freq, HERTZ_FORMAT); +// } +// HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(uhPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, uhPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(ulPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, ulPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(vhPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, vhPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(vlPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, vlPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(whPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, whPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(wlPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, wlPinName); + +// // dead time is set in nanoseconds +// uint32_t dead_time_ns = (float)(1e9f/PWM_freq)*dead_zone; +// uint32_t dead_time = __LL_TIM_CALC_DEADTIME(SystemCoreClock, LL_TIM_GetClockDivision(HT->getHandle()->Instance), dead_time_ns); +// LL_TIM_OC_SetDeadTime(HT->getHandle()->Instance, dead_time); // deadtime is non linear! +// LL_TIM_CC_EnableChannel(HT->getHandle()->Instance, LL_TIM_CHANNEL_CH1 | LL_TIM_CHANNEL_CH1N | LL_TIM_CHANNEL_CH2 | LL_TIM_CHANNEL_CH2N | LL_TIM_CHANNEL_CH3 | LL_TIM_CHANNEL_CH3N); + +// HT->pause(); +// HT->refresh(); +// HT->resume(); +// return HT; +// } + + +// // returns 0 if each pair of pwm channels has same channel +// // returns 1 all the channels belong to the same timer - hardware inverted channels +// // returns -1 if neither - avoid configuring - error!!! +// int _interfaceType(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ +// PinName nameAH = digitalPinToPinName(pinA_h); +// PinName nameAL = digitalPinToPinName(pinA_l); +// PinName nameBH = digitalPinToPinName(pinB_h); +// PinName nameBL = digitalPinToPinName(pinB_l); +// PinName nameCH = digitalPinToPinName(pinC_h); +// PinName nameCL = digitalPinToPinName(pinC_l); +// int tim1 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameAH, PinMap_PWM)); +// int tim2 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameAL, PinMap_PWM)); +// int tim3 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameBH, PinMap_PWM)); +// int tim4 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameBL, PinMap_PWM)); +// int tim5 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameCH, PinMap_PWM)); +// int tim6 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameCL, PinMap_PWM)); +// if(tim1 == tim2 && tim2==tim3 && tim3==tim4 && tim4==tim5 && tim5==tim6) +// return _HARDWARE_6PWM; // hardware 6pwm interface - only on timer +// else if(tim1 == tim2 && tim3==tim4 && tim5==tim6) +// return _SOFTWARE_6PWM; // software 6pwm interface - each pair of high-low side same timer +// else +// return _ERROR_6PWM; // might be error avoid configuration +// } + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1])); + core_util_critical_section_exit(); + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + _pwm_init(&(params->pins[2]), pinC, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1]), &(params->pins[2])); + core_util_critical_section_exit(); + + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + _pwm_init(&(params->pins[2]), pinC, (long)pwm_frequency); + _pwm_init(&(params->pins[3]), pinD, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1]), &(params->pins[2]), &(params->pins[3])); + core_util_critical_section_exit(); + + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +//- hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_b); + core_util_critical_section_exit(); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +//- hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_b); + _pwm_write(&(((PortentaDriverParams*)params)->pins[2]), (float)dc_c); + core_util_critical_section_exit(); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +//- hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_1a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_1b); + _pwm_write(&(((PortentaDriverParams*)params)->pins[2]), (float)dc_2a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[3]), (float)dc_2b); + core_util_critical_section_exit(); +} + + +// 6-PWM currently not supported, defer to generic, which also doesn't support it ;-) + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +//void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + // if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + // else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to |%0kHz max + // // center-aligned frequency is uses two periods + // pwm_frequency *=2; + + // // find configuration + // int config = _interfaceType(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // // configure accordingly + // switch(config){ + // case _ERROR_6PWM: + // return -1; // fail + // break; + // case _HARDWARE_6PWM: + // _initHardware6PWMInterface(pwm_frequency, dead_zone, pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // break; + // case _SOFTWARE_6PWM: + // HardwareTimer* HT1 = _initPinPWMHigh(pwm_frequency, pinA_h); + // _initPinPWMLow(pwm_frequency, pinA_l); + // HardwareTimer* HT2 = _initPinPWMHigh(pwm_frequency,pinB_h); + // _initPinPWMLow(pwm_frequency, pinB_l); + // HardwareTimer* HT3 = _initPinPWMHigh(pwm_frequency,pinC_h); + // _initPinPWMLow(pwm_frequency, pinC_l); + // _alignPWMTimers(HT1, HT2, HT3); + // break; + // } +// return -1; // success +// } + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +//void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, void* params){ + // // find configuration + // int config = _interfaceType(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // // set pwm accordingly + // switch(config){ + // case _HARDWARE_6PWM: + // _setPwm(pinA_h, _PWM_RANGE*dc_a, _PWM_RESOLUTION); + // _setPwm(pinB_h, _PWM_RANGE*dc_b, _PWM_RESOLUTION); + // _setPwm(pinC_h, _PWM_RANGE*dc_c, _PWM_RESOLUTION); + // break; + // case _SOFTWARE_6PWM: + // _setPwm(pinA_l, _constrain(dc_a + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinA_h, _constrain(dc_a - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinB_l, _constrain(dc_b + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinB_h, _constrain(dc_b - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinC_l, _constrain(dc_c + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinC_h, _constrain(dc_c - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // break; + // } +//} +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp new file mode 100644 index 0000000..f90a4c5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp @@ -0,0 +1,609 @@ + +#include "./renesas.h" + +#if defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_UNOR4_MINIMA) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Renesas (UNO R4)") +#pragma message("") + + + +#include "communication/SimpleFOCDebug.h" +#include "FspTimer.h" + +#define GPT_OPEN (0x00475054ULL) + +/* + We use the GPT timers, there are 2 channels (32 bit) + 6 channels (16 bit) + Each channel has 2 outputs (GTIOCAx and GTIOCBx) which can be complimentary. + + So each timer channel can handle one half-bridge, using either a single (3-PWM) or + two complimentary PWM signals (6-PWM). + + For 1-PWM through 4-PWM, we need as many channels as PWM signals, and we can use + either output A or B of the timer (we can set the polarity) - but not both. + + For 6-PWM we need 3 channels, and use both outputs A and B of each channel, then + we can do hardware dead-time. + Or we can use seperate channels for high and low side, with software dead-time. + Each phase can be either hardware (1 channel) or software (2 channels) dead-time + individually, they don't all have to be one or the other. + + Selected channels can be started together, so we can keep the phases in sync for + low-side current sensing and software 6-PWM. + + The event system should permit low side current sensing without needing interrupts, + but this will be handled by the current sense driver. + + Supported: + - arbitrary PWM frequencies between 1Hz (minimum we can set with our integer based API) + and around 48kHz (more would be possible but the range will be low) + - PWM range at 24kHz (default) is 1000 + - PWM range at 48kHz is 500 + - polarity setting is supported, in all modes + - phase state setting is supported, in 3-PWM, 6-PWM hardware dead-time and 6-PWM software dead-time + + TODOs: + - change setDutyCycle to use register access for speed + - add event system support for low-side current sensing + - perhaps add support to reserve timers used also in + Arduino Pwm.h code, for compatibility with analogWrite() + - check if there is a better way for phase-state setting + */ + + +// track which channels are used +bool channel_used[GPT_HOWMANY] = { false }; + + +struct RenesasTimerConfig { + timer_cfg_t timer_cfg; + gpt_instance_ctrl_t ctrl; + gpt_extended_cfg_t ext_cfg; + gpt_extended_pwm_cfg_t pwm_cfg; + gpt_io_pin_t duty_pin; +}; + +struct ClockDivAndRange { + timer_source_div_t clk_div; + uint32_t range; +}; + +ClockDivAndRange getClockDivAndRange(uint32_t pwm_frequency, uint8_t timer_channel) { + ClockDivAndRange result; + uint32_t max_count = (timer_channel < GTP32_HOWMANY)? 4294967295 : 65535; + uint32_t freq_hz = R_FSP_SystemClockHzGet(FSP_PRIV_CLOCK_PCLKD); + float range = (float) freq_hz / ((float) pwm_frequency * 2.0f); + if(range / 1.0 < max_count) { + result.range = (uint32_t) (range / 1.0); + result.clk_div = TIMER_SOURCE_DIV_1; + } + else if (range / 2.0 < max_count) { + result.range = (uint32_t) (range / 2.0); + result.clk_div = TIMER_SOURCE_DIV_2; + } + else if(range / 4.0 < max_count) { + result.range = (uint32_t) (range / 4.0); + result.clk_div = TIMER_SOURCE_DIV_4; + } + else if(range / 8.0 < max_count) { + result.range = (uint32_t) (range / 8.0 ); + result.clk_div = TIMER_SOURCE_DIV_8; + } + else if(range / 16.0 < max_count) { + result.range = (uint32_t) (range / 16.0 ); + result.clk_div = TIMER_SOURCE_DIV_16; + } + else if (range / 32.0 < max_count) { + result.range = (uint32_t) (range / 32.0 ); + result.clk_div = TIMER_SOURCE_DIV_32; + } + else if(range / 64.0 < max_count) { + result.range = (uint32_t) (range / 64.0 ); + result.clk_div = TIMER_SOURCE_DIV_64; + } + else if(range / 128.0 < max_count) { + result.range = (uint32_t) (range / 128.0 ); + result.clk_div = TIMER_SOURCE_DIV_128; + } + else if(range / 256.0 < max_count) { + result.range = (uint32_t) (range / 256.0 ); + result.clk_div = TIMER_SOURCE_DIV_256; + } + else if(range / 512.0 < max_count) { + result.range = (uint32_t) (range / 512.0 ); + result.clk_div = TIMER_SOURCE_DIV_512; + } + else if(range / 1024.0 < max_count) { + result.range = (uint32_t) (range / 1024.0 ); + result.clk_div = TIMER_SOURCE_DIV_1024; + } + else { + SimpleFOCDebug::println("DRV: PWM frequency too low"); + } + return result; +}; + + +bool configureTimerPin(RenesasHardwareDriverParams* params, uint8_t index, bool active_high, bool complementary = false, bool complementary_active_high = true) { + uint8_t pin = params->pins[index]; + uint8_t pin_C; + std::array pinCfgs = getPinCfgs(pin, PIN_CFG_REQ_PWM); + std::array pinCfgs_C; + if(pinCfgs[0] == 0) { + SIMPLEFOC_DEBUG("DRV: no PWM on pin ", pin); + return false; + } + if (IS_PIN_AGT_PWM(pinCfgs[0])) { + SIMPLEFOC_DEBUG("DRV: AGT timer not supported"); + return false; + } + // get the timer channel + uint8_t timer_channel = GET_CHANNEL(pinCfgs[0]); + // check its not used + if (channel_used[timer_channel]) { + SIMPLEFOC_DEBUG("DRV: channel in use"); + return false; + } + + if (complementary) { + pin_C = params->pins[index+1]; + pinCfgs_C = getPinCfgs(pin_C, PIN_CFG_REQ_PWM); + if(pinCfgs_C[0] == 0) { + SIMPLEFOC_DEBUG("DRV: no PWM on pin ", pin_C); + return false; + } + if (IS_PIN_AGT_PWM(pinCfgs_C[0]) || GET_CHANNEL(pinCfgs_C[0])!=timer_channel) { + SIMPLEFOC_DEBUG("DRV: comp. channel different"); + return false; + } + } + TimerPWMChannel_t pwm_output = IS_PWM_ON_A(pinCfgs[0]) ? CHANNEL_A : CHANNEL_B; + if (complementary) { + TimerPWMChannel_t pwm_output_C = IS_PWM_ON_A(pinCfgs_C[0]) ? CHANNEL_A : CHANNEL_B; + if (pwm_output_C != CHANNEL_A || pwm_output != CHANNEL_B) { + SIMPLEFOC_DEBUG("DRV: output A/B mismatch"); + return false; + } + } + + // configure GPIO pin + fsp_err_t err = R_IOPORT_PinCfg(&g_ioport_ctrl, g_pin_cfg[pin].pin, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_GPT1)); + if ((err == FSP_SUCCESS) && complementary) + err = R_IOPORT_PinCfg(&g_ioport_ctrl, g_pin_cfg[pin_C].pin, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_GPT1)); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: pin config failed"); + return false; + } + + + // configure timer channel - frequency / top value + ClockDivAndRange timings = getClockDivAndRange(params->pwm_frequency, timer_channel); + #if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("---PWM Config---"); + SimpleFOCDebug::println("DRV: pwm pin: ", pin); + if (complementary) + SimpleFOCDebug::println("DRV: compl. pin: ", pin_C); + SimpleFOCDebug::println("DRV: pwm channel: ", timer_channel); + SimpleFOCDebug::print("DRV: pwm A/B: "); SimpleFOCDebug::println(complementary?"A+B":((pwm_output==CHANNEL_A)?"A":"B")); + SimpleFOCDebug::println("DRV: pwm freq: ", (int)params->pwm_frequency); + SimpleFOCDebug::println("DRV: pwm range: ", (int)timings.range); + SimpleFOCDebug::println("DRV: pwm clkdiv: ", timings.clk_div); + #endif + + RenesasTimerConfig* t = new RenesasTimerConfig(); + // configure timer channel - count mode + t->timer_cfg.channel = timer_channel; + t->timer_cfg.mode = TIMER_MODE_TRIANGLE_WAVE_SYMMETRIC_PWM; + t->timer_cfg.source_div = timings.clk_div; + t->timer_cfg.period_counts = timings.range; + t->timer_cfg.duty_cycle_counts = 0; + t->timer_cfg.p_callback = nullptr; + t->timer_cfg.p_context = nullptr; + t->timer_cfg.p_extend = &(t->ext_cfg); + t->timer_cfg.cycle_end_ipl = BSP_IRQ_DISABLED; + t->timer_cfg.cycle_end_irq = FSP_INVALID_VECTOR; + + t->ext_cfg.p_pwm_cfg = &(t->pwm_cfg); + t->pwm_cfg.trough_ipl = BSP_IRQ_DISABLED; + t->pwm_cfg.trough_irq = FSP_INVALID_VECTOR; + t->pwm_cfg.poeg_link = GPT_POEG_LINK_POEG0; + t->pwm_cfg.output_disable = GPT_OUTPUT_DISABLE_NONE; + t->pwm_cfg.adc_trigger = GPT_ADC_TRIGGER_NONE; + t->pwm_cfg.dead_time_count_up = 0; + t->pwm_cfg.dead_time_count_down = 0; + t->pwm_cfg.adc_a_compare_match = 0; + t->pwm_cfg.adc_b_compare_match = 0; + t->pwm_cfg.interrupt_skip_source = GPT_INTERRUPT_SKIP_SOURCE_NONE; + t->pwm_cfg.interrupt_skip_count = GPT_INTERRUPT_SKIP_COUNT_0; + t->pwm_cfg.interrupt_skip_adc = GPT_INTERRUPT_SKIP_ADC_NONE; + t->pwm_cfg.gtioca_disable_setting = GPT_GTIOC_DISABLE_PROHIBITED; + t->pwm_cfg.gtiocb_disable_setting = GPT_GTIOC_DISABLE_PROHIBITED; + // configure dead-time if both outputs are used + if (complementary) { + uint32_t dt = params->dead_zone * timings.range; + t->pwm_cfg.dead_time_count_up = dt; + t->pwm_cfg.dead_time_count_down = dt; + } + + // configure timer channel - outputs and polarity + t->ext_cfg.gtior_setting.gtior = 0L; + if (!complementary) { + if(pwm_output == CHANNEL_A) { + t->duty_pin = GPT_IO_PIN_GTIOCA; + t->ext_cfg.gtioca.output_enabled = true; + t->ext_cfg.gtiocb.output_enabled = false; + t->ext_cfg.gtior_setting.gtior_b.gtioa = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.oadflt = active_high ? 0x00 : 0x01; + // t->ext_cfg.gtior_setting.gtior_b.oahld = 0x0; + // t->ext_cfg.gtior_setting.gtior_b.oadf = 0x0; + // t->ext_cfg.gtior_setting.gtior_b.nfaen = 0x0; + } + else { + t->duty_pin = GPT_IO_PIN_GTIOCB; + t->ext_cfg.gtiocb.output_enabled = true; + t->ext_cfg.gtioca.output_enabled = false; + t->ext_cfg.gtior_setting.gtior_b.gtiob = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.obdflt = active_high ? 0x00 : 0x01; + } + } + else { + t->duty_pin = GPT_IO_PIN_GTIOCA_AND_GTIOCB; + t->ext_cfg.gtioca.output_enabled = true; + t->ext_cfg.gtiocb.output_enabled = true; + t->ext_cfg.gtior_setting.gtior_b.gtioa = 0x03 | (!complementary_active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.oadflt = !complementary_active_high ? 0x00 : 0x01; + t->ext_cfg.gtior_setting.gtior_b.gtiob = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.obdflt = active_high ? 0x00 : 0x01; + } + + // lets stop the timer in case its running + if (GPT_OPEN == t->ctrl.open) { + if (R_GPT_Stop(&(t->ctrl)) != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: timer stop failed"); + return false; + } + } + + memset(&(t->ctrl), 0, sizeof(gpt_instance_ctrl_t)); + err = R_GPT_Open(&(t->ctrl),&(t->timer_cfg)); + if ((err != FSP_ERR_ALREADY_OPEN) && (err != FSP_SUCCESS)) { + SIMPLEFOC_DEBUG("DRV: open failed"); + return false; + } + #if defined(SIMPLEFOC_RESENSAS_DEBUG) + if (err == FSP_ERR_ALREADY_OPEN) { + SimpleFOCDebug::println("DRV: timer already open"); + } + #endif + err = R_GPT_PeriodSet(&(t->ctrl), t->timer_cfg.period_counts); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: period set failed"); + return false; + } + err = R_GPT_OutputEnable(&(t->ctrl), t->duty_pin); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: pin enable failed"); + return false; + } + + channel_used[timer_channel] = true; + params->timer_config[index] = t; + params->channels[index] = timer_channel; + if (complementary) { + params->timer_config[index+1] = t; + params->channels[index+1] = timer_channel; + } + + return true; +} + + +// start the timer channels for the motor, synchronously +bool startTimerChannels(RenesasHardwareDriverParams* params, int num_channels) { + uint32_t mask = 0; + for (int i = 0; i < num_channels; i++) { + // RenesasTimerConfig* t = params->timer_config[i]; + // if (R_GPT_Start(&(t->ctrl)) != FSP_SUCCESS) { + // SIMPLEFOC_DEBUG("DRV: timer start failed"); + // return false; + // } + mask |= (1 << params->channels[i]); +#if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("DRV: starting timer: ", params->channels[i]); +#endif + } + params->timer_config[0]->ctrl.p_reg->GTSTR |= mask; + #if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("DRV: timers started"); + #endif + return true; +} + + +// check if the given pins are on the same timer channel +bool isHardware6Pwm(const int pin1, const int pin2) { + std::array pinCfgs1 = getPinCfgs(pin1, PIN_CFG_REQ_PWM); + std::array pinCfgs2 = getPinCfgs(pin2, PIN_CFG_REQ_PWM); + if(pinCfgs1[0] == 0 || pinCfgs2[0] == 0) + return false; + if (IS_PIN_AGT_PWM(pinCfgs1[0]) || IS_PIN_AGT_PWM(pinCfgs2[0])) + return false; + uint8_t timer_channel1 = GET_CHANNEL(pinCfgs1[0]); + uint8_t timer_channel2 = GET_CHANNEL(pinCfgs2[0]); + return timer_channel1==timer_channel2; +} + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 1); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; params->pins[1] = pinB; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (!success) + success &= startTimerChannels(params, 2); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; params->pins[1] = pinB; params->pins[2] = pinC; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 3); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pin1A; params->pins[1] = pin1B; params->pins[2] = pin2A; params->pins[3] = pin2B; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 3, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 4); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA_h; params->pins[1] = pinA_l; params->pins[2] = pinB_h; params->pins[3] = pinB_l; params->pins[4] = pinC_h; params->pins[5] = pinC_l; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + params->dead_zone = (dead_zone==NOT_SET)?RENESAS_DEFAULT_DEAD_ZONE:dead_zone; + + bool success = true; + if (isHardware6Pwm(pinA_h, pinA_l)) + success &= configureTimerPin(params, 0, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 0, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); // reverse polarity on low side gives desired active high/low behaviour + } + + if (isHardware6Pwm(pinB_h, pinB_l)) + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 3, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + } + + if (isHardware6Pwm(pinC_h, pinC_l)) + success &= configureTimerPin(params, 4, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 4, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 5, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + } + + if (success) + success = startTimerChannels(params, 6); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + duty_cycle_counts = (uint32_t)(dc_c * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_1a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_1b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + duty_cycle_counts = (uint32_t)(dc_2a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[3]; + duty_cycle_counts = (uint32_t)(dc_2b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + + +void _setSinglePhaseState(RenesasTimerConfig* hi, RenesasTimerConfig* lo, PhaseState state) { + gpt_gtior_setting_t gtior; + gtior.gtior = hi->ctrl.p_reg->GTIOR; + bool on = (state==PHASE_ON) || (state==PHASE_HI); + + if (hi->duty_pin == GPT_IO_PIN_GTIOCA_AND_GTIOCB) { + bool ch = false; + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + ch = true; + } // B is high side + on = (state==PHASE_ON) || (state==PHASE_LO); + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + ch = true; + } + if (ch) + hi->ctrl.p_reg->GTIOR = gtior.gtior; + return; + } + + if (hi->duty_pin == GPT_IO_PIN_GTIOCA) { + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + hi->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + else if (hi->duty_pin == GPT_IO_PIN_GTIOCB) { + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + hi->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + + gtior.gtior = lo->ctrl.p_reg->GTIOR; + on = (state==PHASE_ON) || (state==PHASE_LO); + if (lo->duty_pin == GPT_IO_PIN_GTIOCA) { + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + lo->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + else if (lo->duty_pin == GPT_IO_PIN_GTIOCB) { + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + lo->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + +} + + +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + RenesasTimerConfig* t1 = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + uint32_t dt = (uint32_t)(((RenesasHardwareDriverParams*)params)->dead_zone * (float)(t->timer_cfg.period_counts)); + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + bool hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[0] == ((RenesasHardwareDriverParams*)params)->channels[1]; + uint32_t dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[0]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts - dt_act, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + t1 = ((RenesasHardwareDriverParams*)params)->timer_config[3]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[2] == ((RenesasHardwareDriverParams*)params)->channels[3]; + dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[1]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts - dt_act, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + + t = ((RenesasHardwareDriverParams*)params)->timer_config[4]; + t1 = ((RenesasHardwareDriverParams*)params)->timer_config[5]; + duty_cycle_counts = (uint32_t)(dc_c * (float)(t->timer_cfg.period_counts)); + hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[4] == ((RenesasHardwareDriverParams*)params)->channels[5]; + dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[2]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h new file mode 100644 index 0000000..91bacdc --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h @@ -0,0 +1,28 @@ +#pragma once + + +#include "../../hardware_api.h" + + +#if defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_UNOR4_MINIMA) + +// uncomment to enable debug output from Renesas driver +// can set this as build-flag in Arduino IDE or PlatformIO +#define SIMPLEFOC_RENESAS_DEBUG + +#define RENESAS_DEFAULT_PWM_FREQUENCY 24000 +#define RENESAS_DEFAULT_DEAD_ZONE 0.05f + +struct RenesasTimerConfig; + +typedef struct RenesasHardwareDriverParams { + uint8_t pins[6]; + uint8_t channels[6]; + RenesasTimerConfig* timer_config[6]; + long pwm_frequency; + float dead_zone; +} RenesasHardwareDriverParams; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp new file mode 100644 index 0000000..eee5797 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp @@ -0,0 +1,237 @@ + +/** + * Support for the RP2040 MCU, as found on the Raspberry Pi Pico. + */ +#if defined(TARGET_RP2040) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for RP2040") +#pragma message("") + + +#define SIMPLEFOC_DEBUG_RP2040 + +#include "../../hardware_api.h" +#include "./rp2040_mcu.h" +#include "hardware/pwm.h" +#include "hardware/clocks.h" + +#define _PWM_FREQUENCY 24000 +#define _PWM_FREQUENCY_MAX 66000 +#define _PWM_FREQUENCY_MIN 1 + + + +// until I can figure out if this can be quickly read from some register, keep it here. +// it also serves as a marker for what slices are already used. +uint16_t wrapvalues[NUM_PWM_SLICES]; + + +// TODO add checks which channels are already used... + +void setupPWM(int pin, long pwm_frequency, bool invert, RP2040DriverParams* params, uint8_t index) { + gpio_set_function(pin, GPIO_FUNC_PWM); + uint slice = pwm_gpio_to_slice_num(pin); + uint chan = pwm_gpio_to_channel(pin); + params->pins[index] = pin; + params->slice[index] = slice; + params->chan[index] = chan; + uint32_t sysclock_hz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS) * 1000; + uint32_t factor = 4096 * 2 * pwm_frequency; + uint32_t div = sysclock_hz / factor; + if (sysclock_hz % factor !=0) div+=1; + if (div < 16) div = 16; + uint32_t wrapvalue = (sysclock_hz * 8) / div / pwm_frequency - 1; +#ifdef SIMPLEFOC_DEBUG_RP2040 + SimpleFOCDebug::print("Configuring pin "); + SimpleFOCDebug::print(pin); + SimpleFOCDebug::print(" slice "); + SimpleFOCDebug::print((int)slice); + SimpleFOCDebug::print(" channel "); + SimpleFOCDebug::print((int)chan); + SimpleFOCDebug::print(" frequency "); + SimpleFOCDebug::print((int)pwm_frequency); + SimpleFOCDebug::print(" divisor "); + SimpleFOCDebug::print((int)(div>>4)); + SimpleFOCDebug::print("."); + SimpleFOCDebug::print((int)(div&0xF)); + SimpleFOCDebug::print(" top value "); + SimpleFOCDebug::println((int)wrapvalue); +#endif + if (wrapvalue < 999) + SimpleFOCDebug::println("Warning: PWM resolution is low."); + pwm_set_clkdiv_int_frac(slice, div>>4, div&0xF); + pwm_set_phase_correct(slice, true); + pwm_set_wrap(slice, wrapvalue); + wrapvalues[slice] = wrapvalue; + if (invert) { + if (chan==0) + hw_write_masked(&pwm_hw->slice[slice].csr, 0x1 << PWM_CH0_CSR_A_INV_LSB, PWM_CH0_CSR_A_INV_BITS); + else + hw_write_masked(&pwm_hw->slice[slice].csr, 0x1 << PWM_CH0_CSR_B_INV_LSB, PWM_CH0_CSR_B_INV_BITS); + } + pwm_set_chan_level(slice, chan, 0); // switch off initially +} + + +void syncSlices() { + for (uint i=0;ipwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + syncSlices(); + return params; +} + + + +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pinB, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + syncSlices(); + return params; +} + + + +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pinB, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + setupPWM(pinC, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 2); + syncSlices(); + return params; +} + + + + +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pin1A, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pin1B, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + setupPWM(pin2A, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 2); + setupPWM(pin2B, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 3); + syncSlices(); + return params; +} + + +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // non-PIO solution... + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + params->dead_zone = dead_zone; + setupPWM(pinA_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 0); + setupPWM(pinB_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 2); + setupPWM(pinC_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 4); + setupPWM(pinA_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 1); + setupPWM(pinB_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 3); + setupPWM(pinC_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 5); + syncSlices(); + return params; +} + + + + + +void writeDutyCycle(float val, uint slice, uint chan) { + pwm_set_chan_level(slice, chan, (wrapvalues[slice]+1) * val); +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); +} + + + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); +} + + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + writeDutyCycle(dc_c, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); +} + + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params) { + writeDutyCycle(dc_1a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_1b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + writeDutyCycle(dc_2a, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + writeDutyCycle(dc_2b, ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); +} + +inline float swDti(float val, float dt) { + float ret = dt+val; + if (ret>1.0) ret = 1.0f; + return ret; +} + +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params) { + if (phase_state[0]==PhaseState::PHASE_ON || phase_state[0]==PhaseState::PHASE_HI) + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + if (phase_state[0]==PhaseState::PHASE_ON || phase_state[0]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_a, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + + if (phase_state[1]==PhaseState::PHASE_ON || phase_state[1]==PhaseState::PHASE_HI) + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + if (phase_state[1]==PhaseState::PHASE_ON || phase_state[1]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_b, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); + + if (phase_state[2]==PhaseState::PHASE_ON || phase_state[2]==PhaseState::PHASE_HI) + writeDutyCycle(dc_c, ((RP2040DriverParams*)params)->slice[4], ((RP2040DriverParams*)params)->chan[4]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[4], ((RP2040DriverParams*)params)->chan[4]); + if (phase_state[2]==PhaseState::PHASE_ON || phase_state[2]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_c, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[5], ((RP2040DriverParams*)params)->chan[5]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[5], ((RP2040DriverParams*)params)->chan[5]); + + _UNUSED(phase_state); +} + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h new file mode 100644 index 0000000..bbfb387 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h @@ -0,0 +1,22 @@ + + +#pragma once + +#include "Arduino.h" + +#if defined(TARGET_RP2040) + + + +typedef struct RP2040DriverParams { + int pins[6]; + uint slice[6]; + uint chan[6]; + long pwm_frequency; + float dead_zone; +} RP2040DriverParams; + + + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp new file mode 100644 index 0000000..d59a309 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp @@ -0,0 +1,353 @@ + + + +#include "./samd_mcu.h" + + +#ifdef _SAMD21_ + + +#pragma message("") +#pragma message("SimpleFOC: compiling for SAMD21") +#pragma message("") + + +#ifndef TCC3_CH0 +#define TCC3_CH0 NOT_ON_TIMER +#endif +#ifndef TCC3_CH1 +#define TCC3_CH1 NOT_ON_TIMER +#endif +#ifndef TCC3_CH2 +#define TCC3_CH2 NOT_ON_TIMER +#endif +#ifndef TCC3_CH3 +#define TCC3_CH3 NOT_ON_TIMER +#endif +#ifndef TCC3_CH4 +#define TCC3_CH4 NOT_ON_TIMER +#endif +#ifndef TCC3_CH5 +#define TCC3_CH5 NOT_ON_TIMER +#endif +#ifndef TCC3_CH6 +#define TCC3_CH6 NOT_ON_TIMER +#endif +#ifndef TCC3_CH7 +#define TCC3_CH7 NOT_ON_TIMER +#endif +#ifndef TC6_CH0 +#define TC6_CH0 NOT_ON_TIMER +#endif +#ifndef TC6_CH1 +#define TC6_CH1 NOT_ON_TIMER +#endif +#ifndef TC7_CH0 +#define TC7_CH0 NOT_ON_TIMER +#endif +#ifndef TC7_CH1 +#define TC7_CH1 NOT_ON_TIMER +#endif + + + +#define NUM_WO_ASSOCIATIONS 48 + +/* + * For SAM D21 A/B/C/D Variant Devices and SAM DA1 A/B Variant Devices + * Good for SAMD2xE, SAMD2xG and SAMD2xJ devices. Other SAMD21s currently not supported in arduino anyway? + * + * Note: only the pins which have timers associated are listed in this table. + * You can use the values from g_APinDescription.ulPort and g_APinDescription.ulPin to find the correct row in the table. + * + * See Microchip Technology datasheet DS40001882F-page 30 + */ +struct wo_association WO_associations[] = { + + { PORTA, 0, TCC2_CH0, 0, NOT_ON_TIMER, 0}, + { PORTA, 1, TCC2_CH1, 1, NOT_ON_TIMER, 0}, + { PORTA, 2, NOT_ON_TIMER, 0, TCC3_CH0, 0}, + { PORTA, 3, NOT_ON_TIMER, 0, TCC3_CH1, 1}, + // PB04, PB05, PB06, PB07 - no timers + { PORTB, 8, TC4_CH0, 0, TCC3_CH6, 6}, + { PORTB, 9, TC4_CH1, 1, TCC3_CH7, 7}, + { PORTA, 4, TCC0_CH0, 0, TCC3_CH2, 2}, + { PORTA, 5, TCC0_CH1, 1, TCC3_CH3, 3}, + { PORTA, 6, TCC1_CH0, 0, TCC3_CH4, 4}, + { PORTA, 7, TCC1_CH1, 1, TCC3_CH5, 5}, + { PORTA, 8, TCC0_CH0, 0, TCC1_CH2, 2}, + { PORTA, 9, TCC0_CH1, 1, TCC1_CH3, 3}, + { PORTA, 10, TCC1_CH0, 0, TCC0_CH2, 2}, + { PORTA, 11, TCC1_CH1, 1, TCC0_CH3, 3}, + { PORTB, 10, TC5_CH0, 0, TCC0_CH4, 4}, + { PORTB, 11, TC5_CH1, 1, TCC0_CH5, 5}, + { PORTB, 12, TC4_CH0, 0, TCC0_CH6, 6}, + { PORTB, 13, TC4_CH1, 1, TCC0_CH7, 7}, + { PORTB, 14, TC5_CH0, 0, NOT_ON_TIMER, 0}, + { PORTB, 15, TC5_CH1, 1, NOT_ON_TIMER, 0}, + { PORTA, 12, TCC2_CH0, 0, TCC0_CH6, 6}, + { PORTA, 13, TCC2_CH1, 1, TCC0_CH7, 7}, + { PORTA, 14, TC3_CH0, 0, TCC0_CH4, 4}, + { PORTA, 15, TC3_CH1, 1, TCC0_CH5, 5}, + { PORTA, 16, TCC2_CH0, 0, TCC0_CH6, 6}, + { PORTA, 17, TCC2_CH1, 1, TCC0_CH7, 7}, + { PORTA, 18, TC3_CH0, 0, TCC0_CH2, 2}, + { PORTA, 19, TC3_CH1, 1, TCC0_CH3, 3}, + { PORTB, 16, TC6_CH0, 0, TCC0_CH4, 4}, + { PORTB, 17, TC6_CH1, 1, TCC0_CH5, 5}, + { PORTA, 20, TC7_CH0, 0, TCC0_CH6, 6}, + { PORTA, 21, TC7_CH1, 1, TCC0_CH7, 7}, + { PORTA, 22, TC4_CH0, 0, TCC0_CH4, 4}, + { PORTA, 23, TC4_CH1, 1, TCC0_CH5, 5}, + { PORTA, 24, TC5_CH0, 0, TCC1_CH2, 2}, + { PORTA, 25, TC5_CH1, 1, TCC1_CH3, 3}, + { PORTB, 22, TC7_CH0, 0, TCC3_CH0, 0}, + { PORTB, 23, TC7_CH1, 1, TCC3_CH1, 1}, + { PORTA, 27, NOT_ON_TIMER, 0, TCC3_CH6, 6}, + { PORTA, 28, NOT_ON_TIMER, 0, TCC3_CH7, 7}, + { PORTA, 30, TCC1_CH0, 0, TCC3_CH4, 4}, + { PORTA, 31, TCC1_CH1, 1, TCC3_CH5, 5}, + { PORTB, 30, TCC0_CH0, 0, TCC1_CH2, 2}, + { PORTB, 31, TCC0_CH1, 1, TCC1_CH3, 3}, + { PORTB, 0, TC7_CH0, 0, NOT_ON_TIMER, 0}, + { PORTB, 1, TC7_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 2, TC6_CH0, 0, TCC3_CH2, 2}, + { PORTB, 3, TC6_CH1, 1, TCC3_CH3, 3} +}; +wo_association ASSOCIATION_NOT_FOUND = { NOT_A_PORT, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}; + + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin) { + for (int i=0;i>pin_position)&0x01)==0x1?PIO_TIMER_ALT:PIO_TIMER; +} + + + + + +void syncTCC(Tcc* TCCx) { + while (TCCx->SYNCBUSY.reg & TCC_SYNCBUSY_MASK); // Wait for synchronization of registers between the clock domains +} + + + +/** + * Configure Clock 4 - we want all simplefoc PWMs to use the same clock. This ensures that + * any compatible pin combination can be used without having to worry about configuring different + * clocks. + */ +void configureSAMDClock() { + + // TODO investigate using the FDPLL96M clock to get 96MHz timer clocks... this + // would enable 48KHz PWM clocks, and setting the frequency between 24Khz with resolution 2000, to 48KHz with resolution 1000 + + if (!SAMDClockConfigured) { + SAMDClockConfigured = true; // mark clock as configured + for (int i=0;iSTATUS.bit.SYNCBUSY); // Wait for synchronization + + REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW + GCLK_GENCTRL_GENEN | // Enable GCLK4 + GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source + // GCLK_GENCTRL_SRC_FDPLL | // Set the 96MHz clock source + GCLK_GENCTRL_ID(4); // Select GCLK4 + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured clock..."); +#endif + } +} + + + + +/** + * Configure a TCC unit + * pwm_frequency is fixed at 24kHz for now. We could go slower, but going + * faster won't be possible without sacrificing resolution. + */ +void configureTCC(tccConfiguration& tccConfig, long pwm_frequency, bool negate, float hw6pwm) { + + long pwm_resolution = (24000000) / pwm_frequency; + if (pwm_resolution>SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION) + pwm_resolution = SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION; + if (pwm_resolution>1) { + case 0: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TCC0_TCC1); break; //GCLK_CLKCTRL_ID_TCC0_TCC1; + case 1: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TCC2_TC3); break; //GCLK_CLKCTRL_ID_TCC2_TC3; + case 2: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TC4_TC5); break; //GCLK_CLKCTRL_ID_TC4_TC5; + case 3: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TC6_TC7); break; + default: return; + } + + // Feed GCLK4 to TCC + REG_GCLK_CLKCTRL = (uint16_t) GCLK_CLKCTRL_CLKEN | // Enable GCLK4 + GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4 + GCLK_CLKCTRL_ID_ofthistcc; // Feed GCLK4 to tcc + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization + + tccConfigured[tccConfig.tcc.tccn] = true; + + if (tccConfig.tcc.tccn>=TCC_INST_NUM) { + Tc* tc = (Tc*)GetTC(tccConfig.tcc.chaninfo); + // disable + tc->COUNT8.CTRLA.bit.ENABLE = 0; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // unfortunately we need the 8-bit counter mode to use the PER register... + tc->COUNT8.CTRLA.reg |= TC_CTRLA_MODE_COUNT8 | TC_CTRLA_WAVEGEN_NPWM ; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // meaning prescaler of 8, since TC-Unit has no up/down mode, and has resolution of 250 rather than 1000... + tc->COUNT8.CTRLA.bit.PRESCALER = TC_CTRLA_PRESCALER_DIV8_Val ; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // period is 250, period cannot be higher than 256! + tc->COUNT8.PER.reg = SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // initial duty cycle is 0 + tc->COUNT8.CC[tccConfig.tcc.chan].reg = 0; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // enable + tc->COUNT8.CTRLA.bit.ENABLE = 1; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Initialized TC ", tccConfig.tcc.tccn); +#endif + } + else { + Tcc* tcc = (Tcc*)GetTC(tccConfig.tcc.chaninfo); + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + tcc->WAVE.reg |= TCC_WAVE_POL(0xF)|TCC_WAVEB_WAVEGENB_DSBOTH; // Set wave form configuration + while ( tcc->SYNCBUSY.bit.WAVE == 1 ); // wait for sync + + if (hw6pwm>0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + tcc->PER.reg = pwm_resolution - 1; // Set counter Top using the PER register + while ( tcc->SYNCBUSY.bit.PER == 1 ); // wait for sync + + // set all channels to 0% + uint8_t chanCount = (tccConfig.tcc.tccn==1||tccConfig.tcc.tccn==2)?2:4; + for (int i=0;iCC[i].reg = 0; // start off at 0% duty cycle + uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+i); + while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + } + + // Enable TC + tcc->CTRLA.reg |= TCC_CTRLA_ENABLE | TCC_CTRLA_PRESCALER_DIV1; //48Mhz/1=48Mhz/2(up/down)=24MHz/1024=24KHz + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + } + else if (tccConfig.tcc.tccnCTRLA.bit.ENABLE = 0; + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + if (hw6pwm>0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + tcc->CTRLA.bit.ENABLE = 1; + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: (Re-)Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + + +} + + + + + +void writeSAMDDutyCycle(tccConfiguration* info, float dc) { + uint8_t tccn = GetTCNumber(info->tcc.chaninfo); + uint8_t chan = GetTCChannelNumber(info->tcc.chaninfo); + if (tccntcc.chaninfo); + // set via CC +// tcc->CC[chan].reg = (uint32_t)((SIMPLEFOC_SAMD_PWM_RESOLUTION-1) * dc); +// uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+chan); +// while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + // set via CCB + //while ( (tcc->SYNCBUSY.vec.CC & (0x1< 0 ); + tcc->CCB[chan].reg = (uint32_t)((info->pwm_res-1) * dc); +// while ( (tcc->SYNCBUSY.vec.CCB & (0x1< 0 ); +// tcc->STATUS.vec.CCBV |= (0x1<SYNCBUSY.bit.STATUS > 0 ); +// tcc->CTRLBSET.reg |= TCC_CTRLBSET_CMD(TCC_CTRLBSET_CMD_UPDATE_Val); +// while ( tcc->SYNCBUSY.bit.CTRLB > 0 ); + } + else { + Tc* tc = (Tc*)GetTC(info->tcc.chaninfo); + tc->COUNT8.CC[chan].reg = (uint8_t)((SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1) * dc); + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + } +} + + + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp new file mode 100644 index 0000000..71bf0b8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp @@ -0,0 +1,351 @@ + + +#include "./samd_mcu.h" + + +#if defined(_SAMD51_)||defined(_SAME51_) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for SAMD51/SAME51") +#pragma message("") + + + +// expected frequency on DPLL, since we don't configure it ourselves. Typically this is the CPU frequency. +// for custom boards or overclockers you can override it using this define. +#ifndef SIMPLEFOC_SAMD51_DPLL_FREQ +#define SIMPLEFOC_SAMD51_DPLL_FREQ 120000000 +#endif + + +#ifndef TCC3_CH0 +#define TCC3_CH0 NOT_ON_TIMER +#define TCC3_CH1 NOT_ON_TIMER +#endif + +#ifndef TCC4_CH0 +#define TCC4_CH0 NOT_ON_TIMER +#define TCC4_CH1 NOT_ON_TIMER +#endif + + +#ifndef TC4_CH0 +#define TC4_CH0 NOT_ON_TIMER +#define TC4_CH1 NOT_ON_TIMER +#endif + +#ifndef TC5_CH0 +#define TC5_CH0 NOT_ON_TIMER +#define TC5_CH1 NOT_ON_TIMER +#endif + +#ifndef TC6_CH0 +#define TC6_CH0 NOT_ON_TIMER +#define TC6_CH1 NOT_ON_TIMER +#endif + +#ifndef TC7_CH0 +#define TC7_CH0 NOT_ON_TIMER +#define TC7_CH1 NOT_ON_TIMER +#endif + + + +// TCC# Channels WO_NUM Counter size Fault Dithering Output matrix DTI SWAP Pattern generation +// 0 6 8 24-bit Yes Yes Yes Yes Yes Yes +// 1 4 8 24-bit Yes Yes Yes Yes Yes Yes +// 2 3 3 16-bit Yes - Yes - - - +// 3 2 2 16-bit Yes - - - - - +// 4 2 2 16-bit Yes - - - - - + + +#define NUM_WO_ASSOCIATIONS 72 + +struct wo_association WO_associations[] = { + + { PORTB, 9, TC4_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 4, TC0_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 5, TC0_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 6, TC1_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 7, TC1_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTC, 4, NOT_ON_TIMER, 0, TCC0_CH0, 0, NOT_ON_TIMER, 0}, + // PC05, PC06, PC07 -> no timers + { PORTA, 8, TC0_CH0, 0, TCC0_CH0, 0, TCC1_CH0, 4}, + { PORTA, 9, TC0_CH1, 1, TCC0_CH1, 1, TCC1_CH1, 5}, + { PORTA, 10, TC1_CH0, 0, TCC0_CH2, 2, TCC1_CH2, 6}, + { PORTA, 11, TC1_CH1, 1, TCC0_CH3, 3, TCC1_CH3, 7}, + { PORTB, 10, TC5_CH0, 0, TCC0_CH4, 4, TCC1_CH0, 0}, + { PORTB, 11, TC5_CH1, 1, TCC0_CH5, 5, TCC1_CH1, 1}, + { PORTB, 12, TC4_CH0, 0, TCC3_CH0, 0, TCC0_CH0, 0}, + { PORTB, 13, TC4_CH1, 1, TCC3_CH1, 1, TCC0_CH1, 1}, + { PORTB, 14, TC5_CH0, 0, TCC4_CH0, 0, TCC0_CH2, 2}, + { PORTB, 15, TC5_CH1, 1, TCC4_CH1, 1, TCC0_CH3, 3}, + { PORTD, 8, NOT_ON_TIMER, 0, TCC0_CH1, 1, NOT_ON_TIMER, 0}, + { PORTD, 9, NOT_ON_TIMER, 0, TCC0_CH2, 2, NOT_ON_TIMER, 0}, + { PORTD, 10, NOT_ON_TIMER, 0, TCC0_CH3, 3, NOT_ON_TIMER, 0}, + { PORTD, 11, NOT_ON_TIMER, 0, TCC0_CH4, 4, NOT_ON_TIMER, 0}, + { PORTD, 12, NOT_ON_TIMER, 0, TCC0_CH5, 5, NOT_ON_TIMER, 0}, + { PORTC, 10, NOT_ON_TIMER, 0, TCC0_CH0, 0, TCC1_CH0, 4}, + { PORTC, 11, NOT_ON_TIMER, 0, TCC0_CH1, 1, TCC1_CH1, 5}, + { PORTC, 12, NOT_ON_TIMER, 0, TCC0_CH2, 2, TCC1_CH2, 6}, + { PORTC, 13, NOT_ON_TIMER, 0, TCC0_CH3, 3, TCC1_CH3, 7}, + { PORTC, 14, NOT_ON_TIMER, 0, TCC0_CH4, 4, TCC1_CH0, 0}, + { PORTC, 15, NOT_ON_TIMER, 0, TCC0_CH5, 5, TCC1_CH1, 1}, + { PORTA, 12, TC2_CH0, 0, TCC0_CH0, 6, TCC1_CH2, 2}, + { PORTA, 13, TC2_CH1, 1, TCC0_CH1, 7, TCC1_CH3, 3}, + { PORTA, 14, TC3_CH0, 0, TCC2_CH0, 0, TCC1_CH2, 2}, + { PORTA, 15, TC3_CH1, 1, TCC2_CH1, 1, TCC1_CH3, 3}, + { PORTA, 16, TC2_CH0, 0, TCC1_CH0, 0, TCC0_CH4, 4}, + { PORTA, 17, TC2_CH1, 1, TCC1_CH1, 1, TCC0_CH5, 5}, + { PORTA, 18, TC3_CH0, 0, TCC1_CH2, 2, TCC0_CH0, 6}, + { PORTA, 19, TC3_CH1, 1, TCC1_CH3, 3, TCC0_CH1, 7}, + { PORTC, 16, NOT_ON_TIMER, 0, TCC0_CH0, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTC, 17, NOT_ON_TIMER, 0, TCC0_CH1, 1, NOT_ON_TIMER, 0}, // PDEC1 + { PORTC, 18, NOT_ON_TIMER, 0, TCC0_CH2, 2, NOT_ON_TIMER, 0}, // PDEC2 + { PORTC, 19, NOT_ON_TIMER, 0, TCC0_CH3, 3, NOT_ON_TIMER, 0}, + { PORTC, 20, NOT_ON_TIMER, 0, TCC0_CH4, 4, NOT_ON_TIMER, 0}, + { PORTC, 21, NOT_ON_TIMER, 0, TCC0_CH5, 5, NOT_ON_TIMER, 0}, + { PORTC, 22, NOT_ON_TIMER, 0, TCC0_CH0, 6, NOT_ON_TIMER, 0}, + { PORTC, 23, NOT_ON_TIMER, 0, TCC0_CH1, 7, NOT_ON_TIMER, 0}, + { PORTD, 20, NOT_ON_TIMER, 0, TCC1_CH0, 0, NOT_ON_TIMER, 0}, + { PORTD, 21, NOT_ON_TIMER, 0, TCC1_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 16, TC6_CH0, 0, TCC3_CH0, 0, TCC0_CH4, 4}, + { PORTB, 17, TC6_CH1, 1, TCC3_CH1, 1, TCC0_CH5, 5}, + { PORTB, 18, NOT_ON_TIMER, 0, TCC1_CH0, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTB, 19, NOT_ON_TIMER, 0, TCC1_CH1, 1, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 20, NOT_ON_TIMER, 0, TCC1_CH2, 2, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 21, NOT_ON_TIMER, 0, TCC1_CH3, 3, NOT_ON_TIMER, 0}, + { PORTA, 20, TC7_CH0, 0, TCC1_CH0, 4, TCC0_CH0, 0}, + { PORTA, 21, TC7_CH1, 1, TCC1_CH1, 5, TCC0_CH1, 1}, + { PORTA, 22, TC4_CH0, 0, TCC1_CH2, 6, TCC0_CH2, 2}, + { PORTA, 23, TC4_CH1, 1, TCC1_CH3, 7, TCC0_CH3, 3}, + { PORTA, 24, TC5_CH0, 0, TCC2_CH2, 2, NOT_ON_TIMER, 0}, // PDEC0 + { PORTA, 25, TC5_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 22, TC7_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 23, TC7_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTB, 24, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 25, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 26, NOT_ON_TIMER, 0, TCC1_CH2, 2, NOT_ON_TIMER, 0}, + { PORTB, 27, NOT_ON_TIMER, 0, TCC1_CH3, 3, NOT_ON_TIMER, 0}, + { PORTB, 28, NOT_ON_TIMER, 0, TCC1_CH0, 4, NOT_ON_TIMER, 0}, + { PORTB, 29, NOT_ON_TIMER, 0, TCC1_CH1, 5, NOT_ON_TIMER, 0}, + // PC24-PC28, PA27, RESET -> no TC/TCC peripherals + { PORTA, 30, TC6_CH0, 0, TCC2_CH0, 0, NOT_ON_TIMER, 0}, + { PORTA, 31, TC6_CH1, 1, TCC2_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 30, TC0_CH0, 0, TCC4_CH0, 0, TCC0_CH0, 6}, + { PORTB, 31, TC0_CH1, 1, TCC4_CH1, 1, TCC0_CH1, 7}, + // PC30, PC31 -> no TC/TCC peripherals + { PORTB, 0, TC7_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTB, 1, TC7_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTB, 2, TC6_CH0, 0, TCC2_CH2, 2, NOT_ON_TIMER, 0}, + +}; + +wo_association ASSOCIATION_NOT_FOUND = { NOT_A_PORT, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}; + +#ifndef TCC3_CC_NUM +uint8_t TCC_CHANNEL_COUNT[] = { TCC0_CC_NUM, TCC1_CC_NUM, TCC2_CC_NUM }; +#else +uint8_t TCC_CHANNEL_COUNT[] = { TCC0_CC_NUM, TCC1_CC_NUM, TCC2_CC_NUM, TCC3_CC_NUM, TCC4_CC_NUM }; +#endif + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin) { + for (int i=0;i>pin_position)&0x01)==0x1?PIO_TCC_PDEC:PIO_TIMER_ALT; +} + + + +void syncTCC(Tcc* TCCx) { + while (TCCx->SYNCBUSY.reg & TCC_SYNCBUSY_MASK); +} + + + + +void writeSAMDDutyCycle(tccConfiguration* info, float dc) { + uint8_t tccn = GetTCNumber(info->tcc.chaninfo); + uint8_t chan = GetTCChannelNumber(info->tcc.chaninfo); + if (tccntcc.chaninfo); + // set via CCBUF +// while ( (tcc->SYNCBUSY.vec.CC & (0x1< 0 ); + tcc->CCBUF[chan].reg = (uint32_t)((info->pwm_res-1) * dc); // TODO pwm frequency! + } + else { + // we don't support the TC channels on SAMD51, isn't worth it. + } +} + + +#define DPLL_CLOCK_NUM 2 // use GCLK2 +#define PWM_CLOCK_NUM 3 // use GCLK3 + + +/** + * Configure Clock 4 - we want all simplefoc PWMs to use the same clock. This ensures that + * any compatible pin combination can be used without having to worry about configuring different + * clocks. + */ +void configureSAMDClock() { + if (!SAMDClockConfigured) { + SAMDClockConfigured = true; // mark clock as configured + for (int i=0;iGENCTRL[DPLL_CLOCK_NUM].reg = GCLK_GENCTRL_GENEN | GCLK_GENCTRL_DIV(1) + // | GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL_Val); + // while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<PCHCTRL[1].reg = GCLK_PCHCTRL_GEN(DPLL_CLOCK_NUM)|GCLK_PCHCTRL_CHEN; + // while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<Dpll[0].DPLLCTRLA.bit.ENABLE = 0; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + // OSCCTRL->Dpll[0].DPLLCTRLB.bit.REFCLK = OSCCTRL_DPLLCTRLB_REFCLK_GCLK_Val; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + // OSCCTRL->Dpll[0].DPLLRATIO.reg = 3; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + // OSCCTRL->Dpll[0].DPLLCTRLA.bit.ENABLE = 1; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + GCLK->GENCTRL[PWM_CLOCK_NUM].bit.GENEN = 0; + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<GENCTRL[PWM_CLOCK_NUM].reg = GCLK_GENCTRL_GENEN | GCLK_GENCTRL_DIV(1) | GCLK_GENCTRL_IDC + //| GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL_Val); + | GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DPLL0_Val); + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<PCHCTRL[GCLK_CLKCTRL_ID_ofthistcc].reg = GCLK_PCHCTRL_GEN(PWM_CLOCK_NUM)|GCLK_PCHCTRL_CHEN; + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<CTRLA.bit.ENABLE = 0; //switch off tcc + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + // work out pwm resolution for desired frequency and constrain to max/min values + long pwm_resolution = (SIMPLEFOC_SAMD51_DPLL_FREQ/2) / pwm_frequency; + if (pwm_resolution>SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION) + pwm_resolution = SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION; + if (pwm_resolution0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + if (!tccConfigured[tccConfig.tcc.tccn]) { + tcc->WAVE.reg |= TCC_WAVE_POL(0xF)|TCC_WAVE_WAVEGEN_DSTOP; // Set wave form configuration - TODO check this... why set like this? + while ( tcc->SYNCBUSY.bit.WAVE == 1 ); // wait for sync + + tcc->PER.reg = pwm_resolution - 1; // Set counter Top using the PER register + while ( tcc->SYNCBUSY.bit.PER == 1 ); // wait for sync + + // set all channels to 0% + for (int i=0;iCC[i].reg = 0; // start off at 0% duty cycle + uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+i); + while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + } + } + + // Enable TCC + tcc->CTRLA.reg |= TCC_CTRLA_ENABLE | TCC_CTRLA_PRESCALER_DIV1; //48Mhz/1=48Mhz/2(up/down)=24MHz/1024=24KHz + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: (Re-)Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + else if (tccConfig.tcc.tccn>=TCC_INST_NUM) { + //Tc* tc = (Tc*)GetTC(tccConfig.tcc.chaninfo); + + // disable + // tc->COUNT8.CTRLA.bit.ENABLE = 0; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // unfortunately we need the 8-bit counter mode to use the PER register... + // tc->COUNT8.CTRLA.reg |= TC_CTRLA_MODE_COUNT8 | TC_CTRLA_WAVEGEN_NPWM ; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // meaning prescaler of 8, since TC-Unit has no up/down mode, and has resolution of 250 rather than 1000... + // tc->COUNT8.CTRLA.bit.PRESCALER = TC_CTRLA_PRESCALER_DIV8_Val ; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // period is 250, period cannot be higher than 256! + // tc->COUNT8.PER.reg = SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // initial duty cycle is 0 + // tc->COUNT8.CC[tccConfig.tcc.chan].reg = 0; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // enable + // tc->COUNT8.CTRLA.bit.ENABLE = 1; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + + #ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Not initialized: TC ", tccConfig.tcc.tccn); + SIMPLEFOC_DEBUG("SAMD: TC units not supported on SAMD51"); + #endif + } + + // set as configured + tccConfigured[tccConfig.tcc.tccn] = true; + + +} + + + + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp new file mode 100644 index 0000000..f697891 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp @@ -0,0 +1,914 @@ + + + +#include "./samd_mcu.h" + +#if defined(_SAMD21_)||defined(_SAMD51_)||defined(_SAME51_) + + + +/** + * Global state + */ +tccConfiguration tccPinConfigurations[SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS]; +uint8_t numTccPinConfigurations = 0; +bool SAMDClockConfigured = false; +bool tccConfigured[TCC_INST_NUM+TC_INST_NUM]; + + + + + +/** + * Attach the TCC to the pin + */ +bool attachTCC(tccConfiguration& tccConfig) { + if (numTccPinConfigurations>=SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS) + return false; + pinMode(tccConfig.pin, OUTPUT); + + pinPeripheral(tccConfig.pin, tccConfig.peripheral); + tccPinConfigurations[numTccPinConfigurations++] = tccConfig; + return true; +} + + + + + +int getPermutationNumber(int pins) { + int num = 1; + for (int i=0;i=TCC_INST_NUM) + return false; + + if (pinAh.tcc.chan==pinBh.tcc.chan || pinAh.tcc.chan==pinBl.tcc.chan || pinAh.tcc.chan==pinCh.tcc.chan || pinAh.tcc.chan==pinCl.tcc.chan) + return false; + if (pinBh.tcc.chan==pinCh.tcc.chan || pinBh.tcc.chan==pinCl.tcc.chan) + return false; + if (pinAl.tcc.chan==pinBh.tcc.chan || pinAl.tcc.chan==pinBl.tcc.chan || pinAl.tcc.chan==pinCh.tcc.chan || pinAl.tcc.chan==pinCl.tcc.chan) + return false; + if (pinBl.tcc.chan==pinCh.tcc.chan || pinBl.tcc.chan==pinCl.tcc.chan) + return false; + + if (pinAh.tcc.chan!=pinAl.tcc.chan || pinBh.tcc.chan!=pinBl.tcc.chan || pinCh.tcc.chan!=pinCl.tcc.chan) + return false; + if (pinAh.wo==pinAl.wo || pinBh.wo==pinBl.wo || pinCh.wo!=pinCl.wo) + return false; + + return true; +} + + + + +bool checkPeripheralPermutationCompatible(tccConfiguration pins[], uint8_t num) { + for (int i=0;i=TCC_INST_NUM || pinAl.tcc.tccn>=TCC_INST_NUM || pinBh.tcc.tccn>=TCC_INST_NUM + || pinBl.tcc.tccn>=TCC_INST_NUM || pinCh.tcc.tccn>=TCC_INST_NUM || pinCl.tcc.tccn>=TCC_INST_NUM) + return false; + + // check we're not in use + if (inUse(pinAh) || inUse(pinAl) || inUse(pinBh) || inUse(pinBl) || inUse(pinCh) || inUse(pinCl)) + return false; + + // check pins are all different tccs/channels + if (pinAh.tcc.chaninfo==pinBh.tcc.chaninfo || pinAh.tcc.chaninfo==pinBl.tcc.chaninfo || pinAh.tcc.chaninfo==pinCh.tcc.chaninfo || pinAh.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinAl.tcc.chaninfo==pinBh.tcc.chaninfo || pinAl.tcc.chaninfo==pinBl.tcc.chaninfo || pinAl.tcc.chaninfo==pinCh.tcc.chaninfo || pinAl.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinBh.tcc.chaninfo==pinCh.tcc.chaninfo || pinBh.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinBl.tcc.chaninfo==pinCh.tcc.chaninfo || pinBl.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + + // check H/L pins are on same timer + if (pinAh.tcc.tccn!=pinAl.tcc.tccn || pinBh.tcc.tccn!=pinBl.tcc.tccn || pinCh.tcc.tccn!=pinCl.tcc.tccn) + return false; + + // check H/L pins aren't on both the same timer and wo + if (pinAh.wo==pinAl.wo || pinBh.wo==pinBl.wo || pinCh.wo==pinCl.wo) + return false; + + return true; +} + + + + + +int checkHardware6PWM(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + for (int i=0;i<64;i++) { + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(i, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(i, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(i, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(i, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(i, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(i, 5)); + if (checkPeripheralPermutationSameTCC6(pinAh, pinAl, pinBh, pinBl, pinCh, pinCl)) + return i; + } + return -1; +} + + + + +int checkSoftware6PWM(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + for (int i=0;i<64;i++) { + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(i, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(i, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(i, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(i, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(i, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(i, 5)); + if (checkPeripheralPermutationCompatible6(pinAh, pinAl, pinBh, pinBl, pinCh, pinCl)) + return i; + } + return -1; +} + + + +int scorePermutation(tccConfiguration pins[], uint8_t num) { + uint32_t usedtccs = 0; + for (int i=0;i>1; + } + for (int i=0;i>1; + } + return score; +} + + + + + + + + +int checkPermutations(uint8_t num, int pins[], bool (*checkFunc)(tccConfiguration[], uint8_t) ) { + tccConfiguration tccConfs[num]; + int best = -1; + int bestscore = 1000000; + for (int i=0;i<(0x1<pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pinB)->pwm_res = tccConfs[1].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + SAMDHardwareDriverParams* params = new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA), getTccPinConfiguration(pinB) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; // Someone with a stepper-setup who can test it? + return params; +} + + + + + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 3PWM setting + * - hardware specific + * + * SAMD21 will support up to 2 BLDC motors in 3-PWM: + * one on TCC0 using 3 of the channels 0,1,2 or 3 + * one on TCC3 using 3 of the channels 0,1,2 or 3 + * i.e. 8 different pins can be used, but only 4 different signals (WO[x]) on those 8 pins + * WO[0] and WO[4] are the same + * WO[1] and WO[5] are the same + * WO[2] and WO[6] are the same + * WO[3] and WO[7] are the same + * + * If you're on the Arduino Nano33 IoT, please see the Nano33 IoT pinout diagram to see which TCC0/WO[x] + * signal is on which pin of the Nano. You can drive one motor on TCC0. For other boards, consult their documentation. + * + * Note: + * That's if we want to keep the signals strictly in sync. + * + * If we can accept out-of-sync PWMs on the different phases, we could drive up to 4 BLDCs in 3-PWM mode, + * using all the TCC channels. (TCC0 & TCC3 - 4 channels each, TCC1 & TCC2 - 2 channels each) + * + * All channels will use the same resolution, prescaler and clock, but they will have different start-times leading + * to misaligned signals. + * + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * @param pinC pinC bldc driver + */ +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC) { +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int pins[3] = { pinA, pinB, pinC }; + int compatibility = checkPermutations(3, pins, checkPeripheralPermutationCompatible); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + + tccConfiguration tccConfs[3] = { getTCCChannelNr(pinA, getPeripheralOfPermutation(compatibility, 0)), + getTCCChannelNr(pinB, getPeripheralOfPermutation(compatibility, 1)), + getTCCChannelNr(pinC, getPeripheralOfPermutation(compatibility, 2)) }; + + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: score=", scorePermutation(tccConfs, 3)); + printTCCConfiguration(tccConfs[0]); + printTCCConfiguration(tccConfs[1]); + printTCCConfiguration(tccConfs[2]); +#endif + + // attach pins to timer peripherals + attachTCC(tccConfs[0]); // in theory this can fail, but there is no way to signal it... + attachTCC(tccConfs[1]); + attachTCC(tccConfs[2]); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + + // set up clock - Note: if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API-changes in SimpleFOC... + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC (waveform, top-value, pre-scaler = frequency) + configureTCC(tccConfs[0], pwm_frequency); + configureTCC(tccConfs[1], pwm_frequency); + configureTCC(tccConfs[2], pwm_frequency); + getTccPinConfiguration(pinA)->pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pinB)->pwm_res = tccConfs[1].pwm_res; + getTccPinConfiguration(pinC)->pwm_res = tccConfs[2].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA), getTccPinConfiguration(pinB), getTccPinConfiguration(pinC) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; + +} + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pin1A pin1A stepper driver + * @param pin1B pin1B stepper driver + * @param pin2A pin2A stepper driver + * @param pin2B pin2B stepper driver + */ +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int pins[4] = { pin1A, pin1B, pin2A, pin2B }; + int compatibility = checkPermutations(4, pins, checkPeripheralPermutationCompatible); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + + tccConfiguration tccConfs[4] = { getTCCChannelNr(pin1A, getPeripheralOfPermutation(compatibility, 0)), + getTCCChannelNr(pin1B, getPeripheralOfPermutation(compatibility, 1)), + getTCCChannelNr(pin2A, getPeripheralOfPermutation(compatibility, 2)), + getTCCChannelNr(pin2B, getPeripheralOfPermutation(compatibility, 3)) }; + + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: score=", scorePermutation(tccConfs, 4)); + printTCCConfiguration(tccConfs[0]); + printTCCConfiguration(tccConfs[1]); + printTCCConfiguration(tccConfs[2]); + printTCCConfiguration(tccConfs[3]); +#endif + + // attach pins to timer peripherals + attachTCC(tccConfs[0]); // in theory this can fail, but there is no way to signal it... + attachTCC(tccConfs[1]); + attachTCC(tccConfs[2]); + attachTCC(tccConfs[3]); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + + // set up clock - Note: if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API-changes in SimpleFOC... + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC (waveform, top-value, pre-scaler = frequency) + configureTCC(tccConfs[0], pwm_frequency); + configureTCC(tccConfs[1], pwm_frequency); + configureTCC(tccConfs[2], pwm_frequency); + configureTCC(tccConfs[3], pwm_frequency); + getTccPinConfiguration(pin1A)->pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pin2A)->pwm_res = tccConfs[1].pwm_res; + getTccPinConfiguration(pin1B)->pwm_res = tccConfs[2].pwm_res; + getTccPinConfiguration(pin2B)->pwm_res = tccConfs[3].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pin1A), getTccPinConfiguration(pin1B), getTccPinConfiguration(pin2A), getTccPinConfiguration(pin2B) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; +} + + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 6PWM setting + * - hardware specific + * + * SAMD21 will support up to 2 BLDC motors in 6-PWM: + * one on TCC0 using 3 of the channels 0,1,2 or 3 + * one on TCC3 using 3 of the channels 0,1,2 or 3 + * i.e. 6 out of 8 pins must be used, in the following high/low side pairs: + * WO[0] & WO[4] (high side & low side) + * WO[1] & WO[5] + * WO[2] & WO[6] + * WO[3] & WO[7] + * + * If you're on the Arduino Nano33 IoT, please see the Nano33 IoT pinout diagram to see which TCC0/WO[x] + * signal is on which pin of the Nano. You can drive 1 BLDC on TCC0. For other boards, consult their documentation. + * + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low - if applicable + * @param pinA_h pinA high-side bldc driver + * @param pinA_l pinA low-side bldc driver + * @param pinB_h pinA high-side bldc driver + * @param pinB_l pinA low-side bldc driver + * @param pinC_h pinA high-side bldc driver + * @param pinC_l pinA low-side bldc driver + * + * @return 0 if config good, -1 if failed + */ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // we want to use a TCC channel with 1 non-inverted and 1 inverted output for each phase, with dead-time insertion +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int compatibility = checkHardware6PWM(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + if (compatibility<0) { + compatibility = checkSoftware6PWM(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + } + + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(compatibility, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(compatibility, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(compatibility, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(compatibility, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(compatibility, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(compatibility, 5)); + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: "); + printTCCConfiguration(pinAh); + printTCCConfiguration(pinAl); + printTCCConfiguration(pinBh); + printTCCConfiguration(pinBl); + printTCCConfiguration(pinCh); + printTCCConfiguration(pinCl); +#endif + + // attach pins to timer peripherals + bool allAttached = true; + allAttached |= attachTCC(pinAh); // in theory this can fail, but there is no way to signal it... + allAttached |= attachTCC(pinAl); + allAttached |= attachTCC(pinBh); + allAttached |= attachTCC(pinBl); + allAttached |= attachTCC(pinCh); + allAttached |= attachTCC(pinCl); + if (!allAttached) + return SIMPLEFOC_DRIVER_INIT_FAILED; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + // set up clock - if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API changes in SimpleFOC driver API + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC(s) + configureTCC(pinAh, pwm_frequency, false, (pinAh.tcc.chaninfo==pinAl.tcc.chaninfo)?dead_zone:-1); + if ((pinAh.tcc.chaninfo!=pinAl.tcc.chaninfo)) + configureTCC(pinAl, pwm_frequency, true, -1.0); + configureTCC(pinBh, pwm_frequency, false, (pinBh.tcc.chaninfo==pinBl.tcc.chaninfo)?dead_zone:-1); + if ((pinBh.tcc.chaninfo!=pinBl.tcc.chaninfo)) + configureTCC(pinBl, pwm_frequency, true, -1.0); + configureTCC(pinCh, pwm_frequency, false, (pinCh.tcc.chaninfo==pinCl.tcc.chaninfo)?dead_zone:-1); + if ((pinCh.tcc.chaninfo!=pinCl.tcc.chaninfo)) + configureTCC(pinCl, pwm_frequency, true, -1.0); + getTccPinConfiguration(pinA_h)->pwm_res = pinAh.pwm_res; + getTccPinConfiguration(pinA_l)->pwm_res = pinAh.pwm_res; // use the high phase resolution, in case we didn't set it + getTccPinConfiguration(pinB_h)->pwm_res = pinBh.pwm_res; + getTccPinConfiguration(pinB_l)->pwm_res = pinBh.pwm_res; + getTccPinConfiguration(pinC_h)->pwm_res = pinCh.pwm_res; + getTccPinConfiguration(pinC_l)->pwm_res = pinCh.pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA_h), getTccPinConfiguration(pinA_l), getTccPinConfiguration(pinB_h), getTccPinConfiguration(pinB_l), getTccPinConfiguration(pinC_h), getTccPinConfiguration(pinC_l) }, + .pwm_frequency = (uint32_t)pwm_frequency, + .dead_zone = dead_zone, + }; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param pinA phase A hardware pin number + * @param pinB phase B hardware pin number + */ +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_b); + return; +} + + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param pinA phase A hardware pin number + * @param pinB phase B hardware pin number + * @param pinC phase C hardware pin number + */ +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params) { + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_b); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[2], dc_c); + return; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param dc_1a duty cycle phase 1A [0, 1] + * @param dc_1b duty cycle phase 1B [0, 1] + * @param dc_2a duty cycle phase 2A [0, 1] + * @param dc_2b duty cycle phase 2B [0, 1] + * @param pin1A phase 1A hardware pin number + * @param pin1B phase 1B hardware pin number + * @param pin2A phase 2A hardware pin number + * @param pin2B phase 2B hardware pin number + */ +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_1a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_1b); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[2], dc_2a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[3], dc_2b); + return; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 6PWM setting + * - hardware specific + * + * Note: dead-time must be setup in advance, so parameter "dead_zone" is ignored + * the low side pins are automatically driven by the SAMD DTI module, so it is enough to set the high-side + * duty cycle. + * No sanity checks are perfomed to ensure the pinA, pinB, pinC are the same pins you used in configure method... + * so use appropriately. + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low + * @param pinA_h phase A high-side hardware pin number + * @param pinA_l phase A low-side hardware pin number + * @param pinB_h phase B high-side hardware pin number + * @param pinB_l phase B low-side hardware pin number + * @param pinC_h phase C high-side hardware pin number + * @param pinC_l phase C low-side hardware pin number + * + */ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + SAMDHardwareDriverParams* p = (SAMDHardwareDriverParams*)params; + tccConfiguration* tcc1 = p->tccPinConfigurations[0]; + tccConfiguration* tcc2 = p->tccPinConfigurations[1]; + uint32_t pwm_res =p->tccPinConfigurations[0]->pwm_res; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + // low-side on a different pin of same TCC - do dead-time in software... + float ls = dc_a+(p->dead_zone * (pwm_res-1)); // TODO resolution!!! + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_a); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_a); // dead-time is done is hardware, no need to set low side pin explicitly + + tcc1 = p->tccPinConfigurations[2]; + tcc2 = p->tccPinConfigurations[3]; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + float ls = dc_b+(p->dead_zone * (pwm_res-1)); + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_b); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_b); + + tcc1 = p->tccPinConfigurations[4]; + tcc2 = p->tccPinConfigurations[5]; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + float ls = dc_c+(p->dead_zone * (pwm_res-1)); + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_c); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_c); + return; + + _UNUSED(phase_state); +} + + + + +#ifdef SIMPLEFOC_SAMD_DEBUG + +/** + * Prints a table of pin assignments for your SAMD MCU. Very useful since the + * board pinout descriptions and variant.cpp are usually quite wrong, and this + * saves you hours of cross-referencing with the datasheet. + */ +void printAllPinInfos() { + SimpleFOCDebug::println(); + for (uint8_t pin=0;pin=TCC_INST_NUM) + SimpleFOCDebug::print(": TC Peripheral"); + else + SimpleFOCDebug::print(": TCC Peripheral"); + switch (info.peripheral) { + case PIO_TIMER: + SimpleFOCDebug::print(" E "); break; + case PIO_TIMER_ALT: + SimpleFOCDebug::print(" F "); break; +#if defined(_SAMD51_)||defined(_SAME51_) + case PIO_TCC_PDEC: + SimpleFOCDebug::print(" G "); break; +#endif + default: + SimpleFOCDebug::print(" ? "); break; + } + if (info.tcc.tccn>=0) { + SimpleFOCDebug::print(info.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(info.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(info.wo); + SimpleFOCDebug::println("]"); + } + else + SimpleFOCDebug::println(" None"); +} + + + +#endif + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h new file mode 100644 index 0000000..74004d6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h @@ -0,0 +1,127 @@ + +#ifndef SAMD_MCU_H +#define SAMD_MCU_H + + +// uncomment to enable debug output from SAMD driver +// can set this as build-flag in Arduino IDE or PlatformIO +// #define SIMPLEFOC_SAMD_DEBUG + +#include "../../hardware_api.h" + + +#if defined(__SAME51J19A__) || defined(__ATSAME51J19A__) +#ifndef _SAME51_ +#define _SAME51_ +#endif +#endif + + +#if defined(_SAMD21_)||defined(_SAMD51_)||defined(_SAME51_) + + +#include "Arduino.h" +#include "variant.h" +#include "wiring_private.h" + + +#ifndef SIMPLEFOC_SAMD_PWM_RESOLUTION +#define SIMPLEFOC_SAMD_PWM_RESOLUTION 1000 +#endif + +#define SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ 24000 +// arbitrary maximum. On SAMD51 with 120MHz clock this means 2kHz minimum pwm frequency +#define SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION 30000 +// lets not go too low - 400 with clock speed of 120MHz on SAMD51 means 150kHz maximum PWM frequency... +// 400 with 48MHz clock on SAMD21 means 60kHz maximum PWM frequency... +#define SIMPLEFOC_SAMD_MIN_PWM_RESOLUTION 400 +// this is the most we can support on the TC units +#define SIMPLEFOC_SAMD_PWM_TC_RESOLUTION 250 + +#ifndef SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS +#define SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS 24 +#endif + + + +struct tccConfiguration { + uint8_t pin; + EPioType peripheral; // 1=true, 0=false + uint8_t wo; + union tccChanInfo { + struct { + int8_t chan; + int8_t tccn; + }; + uint16_t chaninfo; + } tcc; + uint16_t pwm_res; +}; + + + + + + +struct wo_association { + EPortType port; + uint32_t pin; + ETCChannel tccE; + uint8_t woE; + ETCChannel tccF; + uint8_t woF; +#if defined(_SAMD51_)||defined(_SAME51_) + ETCChannel tccG; + uint8_t woG; +#endif +}; + + + +typedef struct SAMDHardwareDriverParams { + tccConfiguration* tccPinConfigurations[6]; + uint32_t pwm_frequency; + float dead_zone; +} SAMDHardwareDriverParams; + + + + +#if defined(_SAMD21_) +#define NUM_PIO_TIMER_PERIPHERALS 2 +#elif defined(_SAMD51_)||defined(_SAME51_) +#define NUM_PIO_TIMER_PERIPHERALS 3 +#endif + + + +/** + * Global state + */ +extern struct wo_association WO_associations[]; +extern uint8_t TCC_CHANNEL_COUNT[]; +extern tccConfiguration tccPinConfigurations[SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS]; +extern uint8_t numTccPinConfigurations; +extern bool SAMDClockConfigured; +extern bool tccConfigured[TCC_INST_NUM+TC_INST_NUM]; + + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin); +void writeSAMDDutyCycle(tccConfiguration* info, float dc); +void configureSAMDClock(); +void configureTCC(tccConfiguration& tccConfig, long pwm_frequency, bool negate=false, float hw6pwm=-1); +__inline__ void syncTCC(Tcc* TCCx) __attribute__((always_inline, unused)); +EPioType getPeripheralOfPermutation(int permutation, int pin_position); + +#ifdef SIMPLEFOC_SAMD_DEBUG +void printTCCConfiguration(tccConfiguration& info); +void printAllPinInfos(); +#endif + + + +#endif + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp new file mode 100644 index 0000000..65dad9f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp @@ -0,0 +1,952 @@ + +#include "../../hardware_api.h" +#include "stm32_mcu.h" + +#if defined(_STM32_DEF_) + +#pragma message("") +#pragma message("SimpleFOC: compiling for STM32") +#pragma message("") + + +//#define SIMPLEFOC_STM32_DEBUG + +#ifdef SIMPLEFOC_STM32_DEBUG +void printTimerCombination(int numPins, PinMap* timers[], int score); +int getTimerNumber(int timerIndex); +#endif + + + + +#ifndef SIMPLEFOC_STM32_MAX_PINTIMERSUSED +#define SIMPLEFOC_STM32_MAX_PINTIMERSUSED 12 +#endif +int numTimerPinsUsed; +PinMap* timerPinsUsed[SIMPLEFOC_STM32_MAX_PINTIMERSUSED]; + + + + + + +// setting pwm to hardware pin - instead analogWrite() +void _setPwm(HardwareTimer *HT, uint32_t channel, uint32_t value, int resolution) +{ + // TODO - remove commented code + // PinName pin = digitalPinToPinName(ulPin); + // TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM); + // uint32_t index = get_timer_index(Instance); + // HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + + HT->setCaptureCompare(channel, value, (TimerCompareFormat_t)resolution); +} + + + + +int getLLChannel(PinMap* timer) { +#if defined(TIM_CCER_CC1NE) + if (STM_PIN_INVERTED(timer->function)) { + switch (STM_PIN_CHANNEL(timer->function)) { + case 1: return LL_TIM_CHANNEL_CH1N; + case 2: return LL_TIM_CHANNEL_CH2N; + case 3: return LL_TIM_CHANNEL_CH3N; +#if defined(LL_TIM_CHANNEL_CH4N) + case 4: return LL_TIM_CHANNEL_CH4N; +#endif + default: return -1; + } + } else +#endif + { + switch (STM_PIN_CHANNEL(timer->function)) { + case 1: return LL_TIM_CHANNEL_CH1; + case 2: return LL_TIM_CHANNEL_CH2; + case 3: return LL_TIM_CHANNEL_CH3; + case 4: return LL_TIM_CHANNEL_CH4; + default: return -1; + } + } + return -1; +} + + + + + +// init pin pwm +HardwareTimer* _initPinPWM(uint32_t PWM_freq, PinMap* timer) { + // sanity check + if (timer==NP) + return NP; + uint32_t index = get_timer_index((TIM_TypeDef*)timer->peripheral); + bool init = false; + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)timer->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + init = true; + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + uint32_t channel = STM_PIN_CHANNEL(timer->function); + HT->pause(); + if (init) + HT->setOverflow(PWM_freq, HERTZ_FORMAT); + HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM1, timer->pin); + #if SIMPLEFOC_PWM_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Configuring high timer ", (int)getTimerNumber(get_timer_index(HardwareTimer_Handle[index]->handle.Instance))); + SIMPLEFOC_DEBUG("STM32-DRV: Configuring high channel ", (int)channel); +#endif + return HT; +} + + + + + + + +// init high side pin +HardwareTimer* _initPinPWMHigh(uint32_t PWM_freq, PinMap* timer) { + HardwareTimer* HT = _initPinPWM(PWM_freq, timer); + #if SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH==false && SIMPLEFOC_PWM_ACTIVE_HIGH==true + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif + return HT; +} + +// init low side pin +HardwareTimer* _initPinPWMLow(uint32_t PWM_freq, PinMap* timer) +{ + uint32_t index = get_timer_index((TIM_TypeDef*)timer->peripheral); + + bool init = false; + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)timer->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + init = true; + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + uint32_t channel = STM_PIN_CHANNEL(timer->function); + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Configuring low timer ", (int)getTimerNumber(get_timer_index(HardwareTimer_Handle[index]->handle.Instance))); + SIMPLEFOC_DEBUG("STM32-DRV: Configuring low channel ", (int)channel); +#endif + + HT->pause(); + if (init) + HT->setOverflow(PWM_freq, HERTZ_FORMAT); + // sets internal fields of HT, but we can't set polarity here + HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM2, timer->pin); + #if SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif + return HT; +} + + + +// align the timers to end the init +void _alignPWMTimers(HardwareTimer *HT1, HardwareTimer *HT2, HardwareTimer *HT3) +{ + HT1->pause(); + HT1->refresh(); + HT2->pause(); + HT2->refresh(); + HT3->pause(); + HT3->refresh(); + HT1->resume(); + HT2->resume(); + HT3->resume(); +} + + + + +// align the timers to end the init +void _alignPWMTimers(HardwareTimer *HT1, HardwareTimer *HT2, HardwareTimer *HT3, HardwareTimer *HT4) +{ + HT1->pause(); + HT1->refresh(); + HT2->pause(); + HT2->refresh(); + HT3->pause(); + HT3->refresh(); + HT4->pause(); + HT4->refresh(); + HT1->resume(); + HT2->resume(); + HT3->resume(); + HT4->resume(); +} + + +// align the timers to end the init +void _stopTimers(HardwareTimer **timers_to_stop, int timer_num) +{ + // TODO - stop each timer only once + // stop timers + for (int i=0; i < timer_num; i++) { + if(timers_to_stop[i] == NP) return; + timers_to_stop[i]->pause(); + timers_to_stop[i]->refresh(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Stopping timer ", getTimerNumber(get_timer_index(timers_to_stop[i]->getHandle()->Instance))); + #endif + } +} + +// align the timers to end the init +void _startTimers(HardwareTimer **timers_to_start, int timer_num) +{ + // TODO - sart each timer only once + // sart timers + for (int i=0; i < timer_num; i++) { + if(timers_to_start[i] == NP) return; + timers_to_start[i]->resume(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Starting timer ", getTimerNumber(get_timer_index(timers_to_start[i]->getHandle()->Instance))); + #endif + } +} + +void _alignTimersNew() { + int numTimers = 0; + HardwareTimer *timers[numTimerPinsUsed]; + + // reset timer counters + for (int i=0; iperipheral); + HardwareTimer *timer = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + bool found = false; + for (int j=0; jpause(); + timers[i]->refresh(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Restarting timer ", getTimerNumber(get_timer_index(timers[i]->getHandle()->Instance))); + #endif + } + + for (int i=0; iresume(); + } + +} + + + + + +// configure hardware 6pwm for a complementary pair of channels +STM32DriverParams* _initHardware6PWMPair(long PWM_freq, float dead_zone, PinMap* pinH, PinMap* pinL, STM32DriverParams* params, int paramsPos) { + // sanity check + if (pinH==NP || pinL==NP) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + +#if defined(STM32L0xx) // L0 boards dont have hardware 6pwm interface + return SIMPLEFOC_DRIVER_INIT_FAILED; // return nothing +#endif + + uint32_t channel1 = STM_PIN_CHANNEL(pinH->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinL->function); + + // more sanity check + if (channel1!=channel2 || pinH->peripheral!=pinL->peripheral) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + uint32_t index = get_timer_index((TIM_TypeDef*)pinH->peripheral); + + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)pinH->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + // HardwareTimer_Handle[index]->handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + ((HardwareTimer *)HardwareTimer_Handle[index]->__this)->setOverflow((uint32_t)PWM_freq, HERTZ_FORMAT); + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + + HT->setMode(channel1, TIMER_OUTPUT_COMPARE_PWM1, pinH->pin); + HT->setMode(channel2, TIMER_OUTPUT_COMPARE_PWM1, pinL->pin); + + // dead time is set in nanoseconds + uint32_t dead_time_ns = (float)(1e9f/PWM_freq)*dead_zone; + uint32_t dead_time = __LL_TIM_CALC_DEADTIME(SystemCoreClock, LL_TIM_GetClockDivision(HT->getHandle()->Instance), dead_time_ns); + if (dead_time>255) dead_time = 255; + if (dead_time==0 && dead_zone>0) { + dead_time = 255; // LL_TIM_CALC_DEADTIME returns 0 if dead_time_ns is too large + SIMPLEFOC_DEBUG("STM32-DRV: WARN: dead time too large, setting to max"); + } + LL_TIM_OC_SetDeadTime(HT->getHandle()->Instance, dead_time); // deadtime is non linear! + #if SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(pinH), LL_TIM_OCPOLARITY_LOW); + #endif + #if SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(pinL), LL_TIM_OCPOLARITY_LOW); + #endif + LL_TIM_CC_EnableChannel(HT->getHandle()->Instance, getLLChannel(pinH) | getLLChannel(pinL)); + HT->pause(); + + // make sure timer output goes to LOW when timer channels are temporarily disabled + LL_TIM_SetOffStates(HT->getHandle()->Instance, LL_TIM_OSSI_DISABLE, LL_TIM_OSSR_ENABLE); + + params->timers[paramsPos] = HT; + params->timers[paramsPos+1] = HT; + params->channels[paramsPos] = channel1; + params->channels[paramsPos+1] = channel2; + return params; +} + + + + +STM32DriverParams* _initHardware6PWMInterface(long PWM_freq, float dead_zone, PinMap* pinA_h, PinMap* pinA_l, PinMap* pinB_h, PinMap* pinB_l, PinMap* pinC_h, PinMap* pinC_l) { + STM32DriverParams* params = new STM32DriverParams { + .timers = { NP, NP, NP, NP, NP, NP }, + .channels = { 0, 0, 0, 0, 0, 0 }, + .pwm_frequency = PWM_freq, + .dead_zone = dead_zone, + .interface_type = _HARDWARE_6PWM + }; + + if (_initHardware6PWMPair(PWM_freq, dead_zone, pinA_h, pinA_l, params, 0) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + if(_initHardware6PWMPair(PWM_freq, dead_zone, pinB_h, pinB_l, params, 2) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + if (_initHardware6PWMPair(PWM_freq, dead_zone, pinC_h, pinC_l, params, 4) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + return params; +} + + + + + + +/* + timer combination scoring function + assigns a score, and also checks the combination is valid + returns <0 if combination is invalid, >=0 if combination is valid. lower (but positive) score is better + for 6 pwm, hardware 6-pwm is preferred over software 6-pwm + hardware 6-pwm is possible if each low channel is the inverted counterpart of its high channel + inverted channels are not allowed except when using hardware 6-pwm (in theory they could be but lets not complicate things) +*/ +int scoreCombination(int numPins, PinMap* pinTimers[]) { + // check already used - TODO move this to outer loop also... + for (int i=0; iperipheral == timerPinsUsed[i]->peripheral + && STM_PIN_CHANNEL(pinTimers[i]->function) == STM_PIN_CHANNEL(timerPinsUsed[i]->function)) + return -2; // bad combination - timer channel already used + } + + // TODO LPTIM and HRTIM should be ignored for now + + // check for inverted channels + if (numPins < 6) { + for (int i=0; ifunction)) + return -3; // bad combination - inverted channel used in non-hardware 6pwm + } + } + // check for duplicated channels + for (int i=0; iperipheral == pinTimers[j]->peripheral + && STM_PIN_CHANNEL(pinTimers[i]->function) == STM_PIN_CHANNEL(pinTimers[j]->function) + && STM_PIN_INVERTED(pinTimers[i]->function) == STM_PIN_INVERTED(pinTimers[j]->function)) + return -4; // bad combination - duplicated channel + } + } + int score = 0; + for (int i=0; iperipheral == pinTimers[j]->peripheral) + found = true; + } + if (!found) score++; + } + if (numPins==6) { + // check for inverted channels - best: 1 timer, 3 channels, 3 matching inverted channels + // >1 timer, 3 channels, 3 matching inverted channels + // 1 timer, 6 channels (no inverted channels) + // >1 timer, 6 channels (no inverted channels) + // check for inverted high-side channels - TODO is this a configuration we should allow? what if all 3 high side channels are inverted and the low-side non-inverted? + if (STM_PIN_INVERTED(pinTimers[0]->function) || STM_PIN_INVERTED(pinTimers[2]->function) || STM_PIN_INVERTED(pinTimers[4]->function)) + return -5; // bad combination - inverted channel used on high-side channel + if (pinTimers[0]->peripheral == pinTimers[1]->peripheral + && pinTimers[2]->peripheral == pinTimers[3]->peripheral + && pinTimers[4]->peripheral == pinTimers[5]->peripheral + && STM_PIN_CHANNEL(pinTimers[0]->function) == STM_PIN_CHANNEL(pinTimers[1]->function) + && STM_PIN_CHANNEL(pinTimers[2]->function) == STM_PIN_CHANNEL(pinTimers[3]->function) + && STM_PIN_CHANNEL(pinTimers[4]->function) == STM_PIN_CHANNEL(pinTimers[5]->function) + && STM_PIN_INVERTED(pinTimers[1]->function) && STM_PIN_INVERTED(pinTimers[3]->function) && STM_PIN_INVERTED(pinTimers[5]->function)) { + // hardware 6pwm, score <10 + + // TODO F37xxx doesn't support dead-time insertion, it has no TIM1/TIM8 + // F301, F302 --> 6 channels, but only 1-3 have dead-time insertion + // TIM2/TIM3/TIM4/TIM5 don't do dead-time insertion + // TIM15/TIM16/TIM17 do dead-time insertion only on channel 1 + + // TODO check these defines + #if defined(STM32F4xx_HAL_TIM_H) || defined(STM32F3xx_HAL_TIM_H) || defined(STM32F2xx_HAL_TIM_H) || defined(STM32F1xx_HAL_TIM_H) || defined(STM32F100_HAL_TIM_H) || defined(STM32FG0x1_HAL_TIM_H) || defined(STM32G0x0_HAL_TIM_H) + if (STM_PIN_CHANNEL(pinTimers[0]->function)>3 || STM_PIN_CHANNEL(pinTimers[2]->function)>3 || STM_PIN_CHANNEL(pinTimers[4]->function)>3 ) + return -8; // channel 4 does not have dead-time insertion + #endif + #ifdef STM32G4xx_HAL_TIM_H + if (STM_PIN_CHANNEL(pinTimers[0]->function)>4 || STM_PIN_CHANNEL(pinTimers[2]->function)>4 || STM_PIN_CHANNEL(pinTimers[4]->function)>4 ) + return -8; // channels 5 & 6 do not have dead-time insertion + #endif + } + else { + // check for inverted low-side channels + if (STM_PIN_INVERTED(pinTimers[1]->function) || STM_PIN_INVERTED(pinTimers[3]->function) || STM_PIN_INVERTED(pinTimers[5]->function)) + return -6; // bad combination - inverted channel used on low-side channel in software 6-pwm + if (pinTimers[0]->peripheral != pinTimers[1]->peripheral + || pinTimers[2]->peripheral != pinTimers[3]->peripheral + || pinTimers[4]->peripheral != pinTimers[5]->peripheral) + return -7; // bad combination - non-matching timers for H/L side in software 6-pwm + score += 10; // software 6pwm, score >10 + } + } + return score; +} + + + + +int findIndexOfFirstPinMapEntry(int pin) { + PinName pinName = digitalPinToPinName(pin); + int i = 0; + while (PinMap_TIM[i].pin!=NC) { + if (pinName == PinMap_TIM[i].pin) + return i; + i++; + } + return -1; +} + + +int findIndexOfLastPinMapEntry(int pin) { + PinName pinName = digitalPinToPinName(pin); + int i = 0; + while (PinMap_TIM[i].pin!=NC) { + if ( pinName == (PinMap_TIM[i].pin & ~ALTX_MASK) + && pinName != (PinMap_TIM[i+1].pin & ~ALTX_MASK)) + return i; + i++; + } + return -1; +} + + + + + + +#define NOT_FOUND 1000 + +int findBestTimerCombination(int numPins, int index, int pins[], PinMap* pinTimers[]) { + PinMap* searchArray[numPins]; + for (int j=0;j=0 && score= 0) { + #ifdef SIMPLEFOC_STM32_DEBUG + SimpleFOCDebug::print("STM32-DRV: best: "); + printTimerCombination(numPins, pinTimers, bestScore); + #endif + } + return bestScore; +}; + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + if (numTimerPinsUsed+1 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[1] = { pinA }; + PinMap* pinTimers[1] = { NP }; + if (findBestTimerCombination(1, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]);\ + // allign the timers + _alignTimersNew(); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1 }, + .channels = { channel1 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + return params; +} + + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if (numTimerPinsUsed+2 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[2] = { pinA, pinB }; + PinMap* pinTimers[2] = { NP, NP }; + if (findBestTimerCombination(2, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + // allign the timers + _alignPWMTimers(HT1, HT2, HT2); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2 }, + .channels = { channel1, channel2 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + return params; +} + + + + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if (numTimerPinsUsed+3 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[3] = { pinA, pinB, pinC }; + PinMap* pinTimers[3] = { NP, NP, NP }; + if (findBestTimerCombination(3, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWM(pwm_frequency, pinTimers[2]); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2, HT3 }, + .channels = { channel1, channel2, channel3 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[2]; + + _alignTimersNew(); + + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if (numTimerPinsUsed+4 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[4] = { pinA, pinB, pinC, pinD }; + PinMap* pinTimers[4] = { NP, NP, NP, NP }; + if (findBestTimerCombination(4, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWM(pwm_frequency, pinTimers[2]); + HardwareTimer* HT4 = _initPinPWM(pwm_frequency, pinTimers[3]); + // allign the timers + _alignPWMTimers(HT1, HT2, HT3, HT4); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + uint32_t channel4 = STM_PIN_CHANNEL(pinTimers[3]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2, HT3, HT4 }, + .channels = { channel1, channel2, channel3, channel4 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[2]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[3]; + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - DC motor - 1PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +//- hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_b, _PWM_RESOLUTION); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +//- hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_b, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_c, _PWM_RESOLUTION); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +//- hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_1a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_1b, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_2a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], _PWM_RANGE*dc_2b, _PWM_RESOLUTION); +} + + + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + if (numTimerPinsUsed+6 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to |%0kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + // find configuration + int pins[6] = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }; + PinMap* pinTimers[6] = { NP, NP, NP, NP, NP, NP }; + int score = findBestTimerCombination(6, pins, pinTimers); + + STM32DriverParams* params; + // configure accordingly + if (score<0) + params = (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + else if (score<10) // hardware pwm + params = _initHardware6PWMInterface(pwm_frequency, dead_zone, pinTimers[0], pinTimers[1], pinTimers[2], pinTimers[3], pinTimers[4], pinTimers[5]); + else { // software pwm + HardwareTimer* HT1 = _initPinPWMHigh(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWMLow(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWMHigh(pwm_frequency, pinTimers[2]); + HardwareTimer* HT4 = _initPinPWMLow(pwm_frequency, pinTimers[3]); + HardwareTimer* HT5 = _initPinPWMHigh(pwm_frequency, pinTimers[4]); + HardwareTimer* HT6 = _initPinPWMLow(pwm_frequency, pinTimers[5]); + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + uint32_t channel4 = STM_PIN_CHANNEL(pinTimers[3]->function); + uint32_t channel5 = STM_PIN_CHANNEL(pinTimers[4]->function); + uint32_t channel6 = STM_PIN_CHANNEL(pinTimers[5]->function); + params = new STM32DriverParams { + .timers = { HT1, HT2, HT3, HT4, HT5, HT6 }, + .channels = { channel1, channel2, channel3, channel4, channel5, channel6 }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone, + .interface_type = _SOFTWARE_6PWM + }; + } + if (score>=0) { + for (int i=0; i<6; i++) + timerPinsUsed[numTimerPinsUsed++] = pinTimers[i]; + _alignTimersNew(); + } + return params; // success +} + + + +void _setSinglePhaseState(PhaseState state, HardwareTimer *HT, int channel1,int channel2) { + _UNUSED(channel2); + switch (state) { + case PhaseState::PHASE_OFF: + // Due to a weird quirk in the arduino timer API, pauseChannel only disables the complementary channel (e.g. CC1NE). + // To actually make the phase floating, we must also set pwm to 0. + HT->pauseChannel(channel1); + break; + default: + HT->resumeChannel(channel1); + break; + } +} + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState* phase_state, void* params){ + switch(((STM32DriverParams*)params)->interface_type){ + case _HARDWARE_6PWM: + // phase a + _setSinglePhaseState(phase_state[0], ((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], ((STM32DriverParams*)params)->channels[1]); + if(phase_state[0] == PhaseState::PHASE_OFF) dc_a = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + // phase b + _setSinglePhaseState(phase_state[1], ((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], ((STM32DriverParams*)params)->channels[3]); + if(phase_state[1] == PhaseState::PHASE_OFF) dc_b = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_b, _PWM_RESOLUTION); + // phase c + _setSinglePhaseState(phase_state[2], ((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], ((STM32DriverParams*)params)->channels[5]); + if(phase_state[2] == PhaseState::PHASE_OFF) dc_c = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], _PWM_RANGE*dc_c, _PWM_RESOLUTION); + break; + case _SOFTWARE_6PWM: + float dead_zone = ((STM32DriverParams*)params)->dead_zone / 2.0f; + if (phase_state[0] == PhaseState::PHASE_ON || phase_state[0] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _constrain(dc_a - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], 0.0f, _PWM_RESOLUTION); + if (phase_state[0] == PhaseState::PHASE_ON || phase_state[0] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _constrain(dc_a + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], 0.0f, _PWM_RESOLUTION); + + if (phase_state[1] == PhaseState::PHASE_ON || phase_state[1] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _constrain(dc_b - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], 0.0f, _PWM_RESOLUTION); + if (phase_state[1] == PhaseState::PHASE_ON || phase_state[1] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], _constrain(dc_b + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], 0.0f, _PWM_RESOLUTION); + + if (phase_state[2] == PhaseState::PHASE_ON || phase_state[2] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], _constrain(dc_c - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], 0.0f, _PWM_RESOLUTION); + if (phase_state[2] == PhaseState::PHASE_ON || phase_state[2] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[5], ((STM32DriverParams*)params)->channels[5], _constrain(dc_c + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[5], ((STM32DriverParams*)params)->channels[5], 0.0f, _PWM_RESOLUTION); + break; + } + _UNUSED(phase_state); +} + + + +#ifdef SIMPLEFOC_STM32_DEBUG + +int getTimerNumber(int timerIndex) { + #if defined(TIM1_BASE) + if (timerIndex==TIMER1_INDEX) return 1; + #endif + #if defined(TIM2_BASE) + if (timerIndex==TIMER2_INDEX) return 2; + #endif + #if defined(TIM3_BASE) + if (timerIndex==TIMER3_INDEX) return 3; + #endif + #if defined(TIM4_BASE) + if (timerIndex==TIMER4_INDEX) return 4; + #endif + #if defined(TIM5_BASE) + if (timerIndex==TIMER5_INDEX) return 5; + #endif + #if defined(TIM6_BASE) + if (timerIndex==TIMER6_INDEX) return 6; + #endif + #if defined(TIM7_BASE) + if (timerIndex==TIMER7_INDEX) return 7; + #endif + #if defined(TIM8_BASE) + if (timerIndex==TIMER8_INDEX) return 8; + #endif + #if defined(TIM9_BASE) + if (timerIndex==TIMER9_INDEX) return 9; + #endif + #if defined(TIM10_BASE) + if (timerIndex==TIMER10_INDEX) return 10; + #endif + #if defined(TIM11_BASE) + if (timerIndex==TIMER11_INDEX) return 11; + #endif + #if defined(TIM12_BASE) + if (timerIndex==TIMER12_INDEX) return 12; + #endif + #if defined(TIM13_BASE) + if (timerIndex==TIMER13_INDEX) return 13; + #endif + #if defined(TIM14_BASE) + if (timerIndex==TIMER14_INDEX) return 14; + #endif + #if defined(TIM15_BASE) + if (timerIndex==TIMER15_INDEX) return 15; + #endif + #if defined(TIM16_BASE) + if (timerIndex==TIMER16_INDEX) return 16; + #endif + #if defined(TIM17_BASE) + if (timerIndex==TIMER17_INDEX) return 17; + #endif + #if defined(TIM18_BASE) + if (timerIndex==TIMER18_INDEX) return 18; + #endif + #if defined(TIM19_BASE) + if (timerIndex==TIMER19_INDEX) return 19; + #endif + #if defined(TIM20_BASE) + if (timerIndex==TIMER20_INDEX) return 20; + #endif + #if defined(TIM21_BASE) + if (timerIndex==TIMER21_INDEX) return 21; + #endif + #if defined(TIM22_BASE) + if (timerIndex==TIMER22_INDEX) return 22; + #endif + return -1; +} + + +void printTimerCombination(int numPins, PinMap* timers[], int score) { + for (int i=0; iperipheral))); + SimpleFOCDebug::print("-CH"); + SimpleFOCDebug::print(STM_PIN_CHANNEL(timers[i]->function)); + if (STM_PIN_INVERTED(timers[i]->function)) + SimpleFOCDebug::print("N"); + } + SimpleFOCDebug::print(" "); + } + SimpleFOCDebug::println("score: ", score); +} + +#endif + + + + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h new file mode 100644 index 0000000..fa6280e --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h @@ -0,0 +1,32 @@ +#ifndef STM32_DRIVER_MCU_DEF +#define STM32_DRIVER_MCU_DEF +#include "../../hardware_api.h" + +#if defined(_STM32_DEF_) + +// default pwm parameters +#define _PWM_RESOLUTION 12 // 12bit +#define _PWM_RANGE 4095.0f // 2^12 -1 = 4095 +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// 6pwm parameters +#define _HARDWARE_6PWM 1 +#define _SOFTWARE_6PWM 0 +#define _ERROR_6PWM -1 + + +typedef struct STM32DriverParams { + HardwareTimer* timers[6] = {NULL}; + uint32_t channels[6]; + long pwm_frequency; + float dead_zone; + uint8_t interface_type; +} STM32DriverParams; + +// timer synchornisation functions +void _stopTimers(HardwareTimer **timers_to_stop, int timer_num=6); +void _startTimers(HardwareTimer **timers_to_start, int timer_num=6); + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp new file mode 100644 index 0000000..3a7b5de --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp @@ -0,0 +1,228 @@ +#include "teensy_mcu.h" + +// if defined +// - Teensy 3.0 MK20DX128 +// - Teensy 3.1/3.2 MK20DX256 +// - Teensy 3.5 MK20DX128 +// - Teensy LC MKL26Z64 +// - Teensy 3.5 MK64FX512 +// - Teensy 3.6 MK66FX1M0 +#if defined(__arm__) && defined(CORE_TEENSY) && (defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MKL26Z64__) || defined(__MK64FX512__) || defined(__MK66FX1M0__)) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Teensy 3.x") +#pragma message("") + +// pin definition from https://github.com/PaulStoffregen/cores/blob/286511f3ec849a6c9e0ec8b73ad6a2fada52e44c/teensy3/pins_teensy.c +#if defined(__MK20DX128__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#elif defined(__MK20DX256__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 32 +#define FTM2_CH1_PIN 25 +#elif defined(__MKL26Z64__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM1_CH0_PIN 16 +#define FTM1_CH1_PIN 17 +#define FTM2_CH0_PIN 3 +#define FTM2_CH1_PIN 4 +#elif defined(__MK64FX512__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 29 +#define FTM2_CH1_PIN 30 +#define FTM3_CH0_PIN 2 +#define FTM3_CH1_PIN 14 +#define FTM3_CH2_PIN 7 +#define FTM3_CH3_PIN 8 +#define FTM3_CH4_PIN 35 +#define FTM3_CH5_PIN 36 +#define FTM3_CH6_PIN 37 +#define FTM3_CH7_PIN 38 +#elif defined(__MK66FX1M0__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 29 +#define FTM2_CH1_PIN 30 +#define FTM3_CH0_PIN 2 +#define FTM3_CH1_PIN 14 +#define FTM3_CH2_PIN 7 +#define FTM3_CH3_PIN 8 +#define FTM3_CH4_PIN 35 +#define FTM3_CH5_PIN 36 +#define FTM3_CH6_PIN 37 +#define FTM3_CH7_PIN 38 +#define TPM1_CH0_PIN 16 +#define TPM1_CH1_PIN 17 +#endif + +int _findTimer( const int Ah, const int Al, const int Bh, const int Bl, const int Ch, const int Cl){ + + if((Ah == FTM0_CH0_PIN && Al == FTM0_CH1_PIN) || + (Ah == FTM0_CH2_PIN && Al == FTM0_CH3_PIN) || + (Ah == FTM0_CH4_PIN && Al == FTM0_CH5_PIN) ){ + if((Bh == FTM0_CH0_PIN && Bl == FTM0_CH1_PIN) || + (Bh == FTM0_CH2_PIN && Bl == FTM0_CH3_PIN) || + (Bh == FTM0_CH4_PIN && Bl == FTM0_CH5_PIN) ){ + if((Ch == FTM0_CH0_PIN && Cl == FTM0_CH1_PIN) || + (Ch == FTM0_CH2_PIN && Cl == FTM0_CH3_PIN) || + (Ch == FTM0_CH4_PIN && Cl == FTM0_CH5_PIN) ){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: Using timer FTM0."); +#endif + // timer FTM0 + return 0; + } + } + } + + #ifdef FTM3_SC // if the board has FTM3 timer + if((Ah == FTM3_CH0_PIN && Al == FTM3_CH1_PIN) || + (Ah == FTM3_CH2_PIN && Al == FTM3_CH3_PIN) || + (Ah == FTM3_CH4_PIN && Al == FTM3_CH5_PIN) ){ + if((Bh == FTM3_CH0_PIN && Bl == FTM3_CH1_PIN) || + (Bh == FTM3_CH2_PIN && Bl == FTM3_CH3_PIN) || + (Bh == FTM3_CH4_PIN && Bl == FTM3_CH5_PIN) ){ + if((Ch == FTM3_CH0_PIN && Cl == FTM3_CH1_PIN) || + (Ch == FTM3_CH2_PIN && Cl == FTM3_CH3_PIN) || + (Ch == FTM3_CH4_PIN && Cl == FTM3_CH5_PIN) ){ + // timer FTM3 +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: Using timer FTM3."); +#endif + return 3; + } + } + } + #endif + +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: ERR: Pins not on timers FTM0 or FTM3!"); +#endif + return -1; + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + unsigned long pwm_freq = 2*pwm_frequency; // center-aligned pwm has 4 times lower freq + _setHighFrequency(pwm_freq, pinA_h); + _setHighFrequency(pwm_freq, pinA_l); + _setHighFrequency(pwm_freq, pinB_h); + _setHighFrequency(pwm_freq, pinB_l); + _setHighFrequency(pwm_freq, pinC_h); + _setHighFrequency(pwm_freq, pinC_l); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h,pinA_l, pinB_h,pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency + }; + + + int timer = _findTimer(pinA_h,pinA_l,pinB_h,pinB_l,pinC_h,pinC_l); + if(timer<0) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // find the best combination of prescalers and counter value + double dead_time = dead_zone/pwm_freq; + int prescaler = 1; // initial prescaler (1,4 or 16) + double count = 1; // inital count (1 - 63) + for (; prescaler<=16; prescaler*=4){ + count = dead_time*((double)F_CPU)/((double)prescaler); + if(count < 64) break; // found the solution + } + count = _constrain(count, 1, 63); + + // configure the timer + if(timer==0){ + // Configure FTM0 + // // inverting and deadtime insertion for FTM1 + FTM0_COMBINE = 0x00121212; // 0x2 - complemetary mode, 0x1 - dead timer insertion enabled + + // Deadtime config + FTM0_DEADTIME = (int)count; // set counter - 1-63 + FTM0_DEADTIME |= ((prescaler>1) << 7) | ((prescaler>4) << 6); // set prescaler (0b01 - 1, 0b10 - 4, 0b11 - 16) + + // configure center aligned PWM + FTM0_SC = 0x00000028; // 0x2 - center-alignment, 0x8 - fixed clock freq + }else if(timer==3){ + // Configure FTM3 + // inverting and deadtime insertion for FTM1 + FTM3_COMBINE = 0x00121212; // 0x2 - complemetary mode, 0x1 - dead timer insertion enabled + + // Deadtime config + FTM3_DEADTIME = (int)count; // set counter - 1-63 + FTM3_DEADTIME |= ((prescaler>1) << 7) | ((prescaler>4) << 6); // set prescaler (0b01 - 1, 0b10 - 4, 0b11 - 16) + + // configure center aligned PWM + FTM3_SC = 0x00000028; // 0x2 - center-alignment, 0x8 - fixed clock freq + } + + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _UNUSED(phase_state); + // transform duty cycle from [0,1] to [0,255] + // phase A + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_a); + + // phase B + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_b); + + // phase C + analogWrite(((GenericDriverParams*)params)->pins[4], 255.0f*dc_c); + analogWrite(((GenericDriverParams*)params)->pins[5], 255.0f*dc_c); +} +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp new file mode 100644 index 0000000..322d5a3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp @@ -0,0 +1,306 @@ +#include "teensy_mcu.h" +#include "teensy4_mcu.h" +#include "../../../communication/SimpleFOCDebug.h" + +// if defined +// - Teensy 4.0 +// - Teensy 4.1 +#if defined(__arm__) && defined(CORE_TEENSY) && ( defined(__IMXRT1062__) || defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41) || defined(ARDUINO_TEENSY_MICROMOD) ) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Teensy 4.x") +#pragma message("") + + +// half_cycle of the PWM variable +int half_cycle = 0; + + +// function which finds the flexpwm instance for a pair of pins +// if they do not belong to the same timer it returns a nullpointer +IMXRT_FLEXPWM_t* get_flexpwm(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return nullptr; + } + info = pwm_pin_info + pin; + // FlexPWM pin + IMXRT_FLEXPWM_t *flexpwm1,*flexpwm2; + switch ((info->module >> 4) & 3) { + case 0: flexpwm1 = &IMXRT_FLEXPWM1; break; + case 1: flexpwm1 = &IMXRT_FLEXPWM2; break; + case 2: flexpwm1 = &IMXRT_FLEXPWM3; break; + default: flexpwm1 = &IMXRT_FLEXPWM4; + } + + info = pwm_pin_info + pin1; + switch ((info->module >> 4) & 3) { + case 0: flexpwm2 = &IMXRT_FLEXPWM1; break; + case 1: flexpwm2 = &IMXRT_FLEXPWM2; break; + case 2: flexpwm2 = &IMXRT_FLEXPWM3; break; + default: flexpwm2 = &IMXRT_FLEXPWM4; + } + if(flexpwm1 == flexpwm2){ + char s[60]; + sprintf (s, "TEENSY-DRV: Pins: %d, %d on Flextimer %d.", pin, pin1, ((info->module >> 4) & 3) + 1); + SIMPLEFOC_DEBUG(s); + return flexpwm1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not in same Flextimer!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return nullptr; + } +} + + +// function which finds the timer submodule for a pair of pins +// if they do not belong to the same submodule it returns a -1 +int get_submodule(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } + + info = pwm_pin_info + pin; + int sm1 = info->module&0x3; + info = pwm_pin_info + pin1; + int sm2 = info->module&0x3; + + if (sm1 == sm2) { + char s[60]; + sprintf (s, "TEENSY-DRV: Pins: %d, %d on submodule %d.", pin, pin1, sm1); + SIMPLEFOC_DEBUG(s); + return sm1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[50]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not in same submodule!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } +} + + +// function which finds the timer submodule for a pair of pins +// if they do not belong to the same submodule it returns a -1 +int get_inverted_channel(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } + + info = pwm_pin_info + pin; + int ch1 = info->channel; + info = pwm_pin_info + pin1; + int ch2 = info->channel; + + if (ch1 != 1) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pin: %d on channel %s - only A supported", pin1, ch1==2 ? "B" : "X"); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } else if (ch2 != 2) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Inverted pin: %d on channel %s - only B supported", pin1, ch2==1 ? "A" : "X"); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: Pin: %d on channel B inverted.", pin1); + SIMPLEFOC_DEBUG(s); +#endif +return ch2; + } +} + +// Helper to set up A/B pair on a FlexPWM submodule. +// can configure sync, prescale and B inversion. +// sets the desired frequency of the PWM +// sets the center-aligned pwm +void setup_pwm_pair (IMXRT_FLEXPWM_t * flexpwm, int submodule, const long frequency, float dead_zone ) +{ + int submodule_mask = 1 << submodule ; + flexpwm->MCTRL &= ~ FLEXPWM_MCTRL_RUN (submodule_mask) ; // stop it if its already running + flexpwm->MCTRL |= FLEXPWM_MCTRL_CLDOK (submodule_mask) ; // clear load OK + + + // calculate the counter and prescaler for the desired pwm frequency + uint32_t newdiv = (uint32_t)((float)F_BUS_ACTUAL / frequency + 0.5f); + uint32_t prescale = 0; + //printf(" div=%lu\n", newdiv); + while (newdiv > 65535 && prescale < 7) { + newdiv = newdiv >> 1; + prescale = prescale + 1; + } + if (newdiv > 65535) { + newdiv = 65535; + } else if (newdiv < 2) { + newdiv = 2; + } + + // the halfcycle of the PWM + half_cycle = int(newdiv/2.0f); + int dead_time = int(dead_zone*half_cycle); //default dead-time - 2% + int mid_pwm = int((half_cycle)/2.0f); + + // setup the timer + // https://github.com/PaulStoffregen/cores/blob/master/teensy4/imxrt.h + flexpwm->SM[submodule].CTRL2 = FLEXPWM_SMCTRL2_WAITEN | FLEXPWM_SMCTRL2_DBGEN | + FLEXPWM_SMCTRL2_FRCEN | FLEXPWM_SMCTRL2_INIT_SEL(0) | FLEXPWM_SMCTRL2_FORCE_SEL(6); + flexpwm->SM[submodule].CTRL = FLEXPWM_SMCTRL_FULL | FLEXPWM_SMCTRL_HALF | FLEXPWM_SMCTRL_PRSC(prescale) ; + // https://github.com/PaulStoffregen/cores/blob/70ba01accd728abe75ebfc8dcd8b3d3a8f3e3f25/teensy4/imxrt.h#L4948 + flexpwm->SM[submodule].OCTRL = 0;//channel_to_invert==2 ? 0 : FLEXPWM_SMOCTRL_POLA | FLEXPWM_SMOCTRL_POLB ; + flexpwm->SM[submodule].DTCNT0 = dead_time ; // should try this out (deadtime control) + flexpwm->SM[submodule].DTCNT1 = dead_time ; // should try this out (deadtime control) + // flexpwm->SM[submodule].TCTRL = FLEXPWM_SMTCTRL_OUT_TRIG_EN (0b000010) ; // sync trig out on VAL1 match. + flexpwm->SM[submodule].INIT = -half_cycle; // count from -HALFCYCLE to +HALFCYCLE + flexpwm->SM[submodule].VAL0 = 0 ; + flexpwm->SM[submodule].VAL1 = half_cycle ; + flexpwm->SM[submodule].VAL2 = -mid_pwm ; + flexpwm->SM[submodule].VAL3 = +mid_pwm ; + // flexpwm->SM[submodule].VAL4 = -mid_pwm ; + // flexpwm->SM[submodule].VAL5 = +mid_pwm ; + + flexpwm->MCTRL |= FLEXPWM_MCTRL_LDOK (submodule_mask) ; // loading reenabled + flexpwm->MCTRL |= FLEXPWM_MCTRL_RUN (submodule_mask) ; // start it running +} + + +// staring the PWM on A and B channels of the submodule +void startup_pwm_pair (IMXRT_FLEXPWM_t * flexpwm, int submodule) +{ + int submodule_mask = 1 << submodule ; + + flexpwm->OUTEN |= FLEXPWM_OUTEN_PWMA_EN (submodule_mask); // enable A output + flexpwm->OUTEN |= FLEXPWM_OUTEN_PWMB_EN (submodule_mask); // enable B output +} + + + +// PWM setting on the high and low pair of the PWM channels +void write_pwm_pair(IMXRT_FLEXPWM_t * flexpwm, int submodule, float duty){ + + int mid_pwm = int((half_cycle)/2.0f); + int count_pwm = int(mid_pwm*(duty*2-1)) + mid_pwm; + + flexpwm->SM[submodule].VAL2 = count_pwm; // A on + flexpwm->SM[submodule].VAL3 = -count_pwm ; // A off + // flexpwm->SM[submodule].VAL4 = - count_pwm ; // B off (assuming B inverted) + // flexpwm->SM[submodule].VAL5 = + count_pwm ; // B on + flexpwm->MCTRL |= FLEXPWM_MCTRL_LDOK (1<flextimers[0], ((Teensy4DriverParams*)params)->submodules[0], dc_a); + write_pwm_pair (((Teensy4DriverParams*)params)->flextimers[1], ((Teensy4DriverParams*)params)->submodules[1], dc_b); + write_pwm_pair (((Teensy4DriverParams*)params)->flextimers[2], ((Teensy4DriverParams*)params)->submodules[2], dc_c); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h new file mode 100644 index 0000000..5e38462 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h @@ -0,0 +1,110 @@ +#include "teensy_mcu.h" + +// if defined +// - Teensy 4.0 +// - Teensy 4.1 +#if defined(__arm__) && defined(CORE_TEENSY) && ( defined(__IMXRT1062__) || defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41) || defined(ARDUINO_TEENSY_MICROMOD) ) + +// teensy 4 driver configuration parameters +typedef struct Teensy4DriverParams { + IMXRT_FLEXPWM_t* flextimers[3] = {NULL}; + int submodules[3]; + long pwm_frequency; + float dead_zone; +} Teensy4DriverParams; + + +// pin definition from https://github.com/PaulStoffregen/cores/blob/master/teensy4/pwm.c +struct pwm_pin_info_struct { + uint8_t type; // 0=no pwm, 1=flexpwm, 2=quad + uint8_t module; // 0-3, 0-3 + uint8_t channel; // 0=X, 1=A, 2=B + uint8_t muxval; // +}; +#define M(a, b) ((((a) - 1) << 4) | (b)) +#if defined(__IMXRT1062__) +const struct pwm_pin_info_struct pwm_pin_info[] = { + {1, M(1, 1), 0, 4}, // FlexPWM1_1_X 0 // AD_B0_03 + {1, M(1, 0), 0, 4}, // FlexPWM1_0_X 1 // AD_B0_02 + {1, M(4, 2), 1, 1}, // FlexPWM4_2_A 2 // EMC_04 + {1, M(4, 2), 2, 1}, // FlexPWM4_2_B 3 // EMC_05 + {1, M(2, 0), 1, 1}, // FlexPWM2_0_A 4 // EMC_06 + {1, M(2, 1), 1, 1}, // FlexPWM2_1_A 5 // EMC_08 + {1, M(2, 2), 1, 2}, // FlexPWM2_2_A 6 // B0_10 + {1, M(1, 3), 2, 6}, // FlexPWM1_3_B 7 // B1_01 + {1, M(1, 3), 1, 6}, // FlexPWM1_3_A 8 // B1_00 + {1, M(2, 2), 2, 2}, // FlexPWM2_2_B 9 // B0_11 + {2, M(1, 0), 0, 1}, // QuadTimer1_0 10 // B0_00 + {2, M(1, 2), 0, 1}, // QuadTimer1_2 11 // B0_02 + {2, M(1, 1), 0, 1}, // QuadTimer1_1 12 // B0_01 + {2, M(2, 0), 0, 1}, // QuadTimer2_0 13 // B0_03 + {2, M(3, 2), 0, 1}, // QuadTimer3_2 14 // AD_B1_02 + {2, M(3, 3), 0, 1}, // QuadTimer3_3 15 // AD_B1_03 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {2, M(3, 1), 0, 1}, // QuadTimer3_1 18 // AD_B1_01 + {2, M(3, 0), 0, 1}, // QuadTimer3_0 19 // AD_B1_00 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(4, 0), 1, 1}, // FlexPWM4_0_A 22 // AD_B1_08 + {1, M(4, 1), 1, 1}, // FlexPWM4_1_A 23 // AD_B1_09 + {1, M(1, 2), 0, 4}, // FlexPWM1_2_X 24 // AD_B0_12 + {1, M(1, 3), 0, 4}, // FlexPWM1_3_X 25 // AD_B0_13 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(3, 1), 2, 1}, // FlexPWM3_1_B 28 // EMC_32 + {1, M(3, 1), 1, 1}, // FlexPWM3_1_A 29 // EMC_31 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(2, 0), 2, 1}, // FlexPWM2_0_B 33 // EMC_07 +#ifdef ARDUINO_TEENSY40 + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 34 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 35 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 36 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 37 // SD_B0_00 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 38 // SD_B0_05 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 39 // SD_B0_04 +#endif +#ifdef ARDUINO_TEENSY41 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(2, 3), 1, 6}, // FlexPWM2_3_A 36 // B1_00 + {1, M(2, 3), 2, 6}, // FlexPWM2_3_B 37 // B1_01 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 42 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 43 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 44 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 45 // SD_B0_00 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 46 // SD_B0_05 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 47 // SD_B0_04 + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_0_B + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_2_A + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_2_B + {1, M(3, 3), 2, 1}, // FlexPWM3_3_B 51 // EMC_22 + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_1_B + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_1_A + {1, M(3, 0), 1, 1}, // FlexPWM3_0_A 54 // EMC_29 +#endif +#ifdef ARDUINO_TEENSY_MICROMOD + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 34 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 35 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 36 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 37 // SD_B0_00 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 38 // SD_B0_04 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 39 // SD_B0_05 + {2, M(2, 1), 0, 1}, // QuadTimer2_1 40 // B0_04 + {2, M(2, 2), 0, 1}, // QuadTimer2_2 41 // B0_05 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_0 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_1 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_2 + {2, M(4, 0), 0, 1}, // QuadTimer4_0 45 // B0_09 +#endif +}; + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp new file mode 100644 index 0000000..dcfa3e1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp @@ -0,0 +1,116 @@ +#include "teensy_mcu.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin){ + analogWrite(pin, 0); + analogWriteFrequency(pin, freq); +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + _setHighFrequency(pwm_frequency, pinD); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h new file mode 100644 index 0000000..7956ea9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h @@ -0,0 +1,14 @@ +#include "../../hardware_api.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// debugging output +#define SIMPLEFOC_TEENSY_DEBUG + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin); + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.cpp new file mode 100644 index 0000000..fa4b8e7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.cpp @@ -0,0 +1,229 @@ +#include "Encoder.h" + + +/* + Encoder(int encA, int encB , int cpr, int index) + - encA, encB - encoder A and B pins + - cpr - counts per rotation number (cpm=ppm*4) + - index pin - (optional input) +*/ + +Encoder::Encoder(int _encA, int _encB , float _ppr, int _index){ + + // Encoder measurement structure init + // hardware pins + pinA = _encA; + pinB = _encB; + // counter setup + pulse_counter = 0; + pulse_timestamp = 0; + + cpr = _ppr; + A_active = 0; + B_active = 0; + I_active = 0; + // index pin + index_pin = _index; // its 0 if not used + + // velocity calculation variables + prev_Th = 0; + pulse_per_second = 0; + prev_pulse_counter = 0; + prev_timestamp_us = _micros(); + + // extern pullup as default + pullup = Pullup::USE_EXTERN; + // enable quadrature encoder by default + quadrature = Quadrature::ON; +} + +// Encoder interrupt callback functions +// A channel +void Encoder::handleA() { + bool A = digitalRead(pinA); + switch (quadrature){ + case Quadrature::ON: + // CPR = 4xPPR + if ( A != A_active ) { + pulse_counter += (A_active == B_active) ? 1 : -1; + pulse_timestamp = _micros(); + A_active = A; + } + break; + case Quadrature::OFF: + // CPR = PPR + if(A && !digitalRead(pinB)){ + pulse_counter++; + pulse_timestamp = _micros(); + } + break; + } +} +// B channel +void Encoder::handleB() { + bool B = digitalRead(pinB); + switch (quadrature){ + case Quadrature::ON: + // // CPR = 4xPPR + if ( B != B_active ) { + pulse_counter += (A_active != B_active) ? 1 : -1; + pulse_timestamp = _micros(); + B_active = B; + } + break; + case Quadrature::OFF: + // CPR = PPR + if(B && !digitalRead(pinA)){ + pulse_counter--; + pulse_timestamp = _micros(); + } + break; + } +} + +// Index channel +void Encoder::handleIndex() { + if(hasIndex()){ + bool I = digitalRead(index_pin); + if(I && !I_active){ + index_found = true; + // align encoder on each index + long tmp = pulse_counter; + // corrent the counter value + pulse_counter = round((double)pulse_counter/(double)cpr)*cpr; + // preserve relative speed + prev_pulse_counter += pulse_counter - tmp; + } + I_active = I; + } +} + + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void Encoder::update() { + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + angle_prev_ts = pulse_timestamp; + long copy_pulse_counter = pulse_counter; + interrupts(); + // TODO: numerical precision issue here if the pulse_counter overflows the angle will be lost + full_rotations = copy_pulse_counter / (int)cpr; + angle_prev = _2PI * ((copy_pulse_counter) % ((int)cpr)) / ((float)cpr); +} + +/* + Shaft angle calculation +*/ +float Encoder::getSensorAngle(){ + return _2PI * (pulse_counter) / ((float)cpr); +} + + + +/* + Shaft velocity calculation + function using mixed time and frequency measurement technique +*/ +float Encoder::getVelocity(){ + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + long copy_pulse_counter = pulse_counter; + long copy_pulse_timestamp = pulse_timestamp; + interrupts(); + // timestamp + long timestamp_us = _micros(); + // sampling time calculation + float Ts = (timestamp_us - prev_timestamp_us) * 1e-6f; + // quick fix for strange cases (micros overflow) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // time from last impulse + float Th = (timestamp_us - copy_pulse_timestamp) * 1e-6f; + long dN = copy_pulse_counter - prev_pulse_counter; + + // Pulse per second calculation (Eq.3.) + // dN - impulses received + // Ts - sampling time - time in between function calls + // Th - time from last impulse + // Th_1 - time form last impulse of the previous call + // only increment if some impulses received + float dt = Ts + prev_Th - Th; + pulse_per_second = (dN != 0 && dt > Ts/2) ? dN / dt : pulse_per_second; + + // if more than 0.05f passed in between impulses + if ( Th > 0.1f) pulse_per_second = 0; + + // velocity calculation + float velocity = pulse_per_second / ((float)cpr) * (_2PI); + + // save variables for next pass + prev_timestamp_us = timestamp_us; + // save velocity calculation variables + prev_Th = Th; + prev_pulse_counter = copy_pulse_counter; + return velocity; +} + +// getter for index pin +// return -1 if no index +int Encoder::needsSearch(){ + return hasIndex() && !index_found; +} + +// private function used to determine if encoder has index +int Encoder::hasIndex(){ + return index_pin != 0; +} + + +// encoder initialisation of the hardware pins +// and calculation variables +void Encoder::init(){ + + // Encoder - check if pullup needed for your encoder + if(pullup == Pullup::USE_INTERN){ + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + if(hasIndex()) pinMode(index_pin,INPUT_PULLUP); + }else{ + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + if(hasIndex()) pinMode(index_pin,INPUT); + } + + // counter setup + pulse_counter = 0; + pulse_timestamp = _micros(); + // velocity calculation variables + prev_Th = 0; + pulse_per_second = 0; + prev_pulse_counter = 0; + prev_timestamp_us = _micros(); + + // initial cpr = PPR + // change it if the mode is quadrature + if(quadrature == Quadrature::ON) cpr = 4*cpr; + + // we don't call Sensor::init() here because init is handled in Encoder class. +} + +// function enabling hardware interrupts of the for the callback provided +// if callback is not provided then the interrupt is not enabled +void Encoder::enableInterrupts(void (*doA)(), void(*doB)(), void(*doIndex)()){ + // attach interrupt if functions provided + switch(quadrature){ + case Quadrature::ON: + // A callback and B callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, CHANGE); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, CHANGE); + break; + case Quadrature::OFF: + // A callback and B callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, RISING); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, RISING); + break; + } + + // if index used initialize the index interrupt + if(hasIndex() && doIndex != nullptr) attachInterrupt(digitalPinToInterrupt(index_pin), doIndex, CHANGE); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.h new file mode 100644 index 0000000..af6a5ab --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/Encoder.h @@ -0,0 +1,91 @@ +#ifndef ENCODER_LIB_H +#define ENCODER_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/base_classes/Sensor.h" + + +/** + * Quadrature mode configuration structure + */ +enum Quadrature : uint8_t { + ON = 0x00, //!< Enable quadrature mode CPR = 4xPPR + OFF = 0x01 //!< Disable quadrature mode / CPR = PPR +}; + +class Encoder: public Sensor{ + public: + /** + Encoder class constructor + @param encA encoder B pin + @param encB encoder B pin + @param ppr impulses per rotation (cpr=ppr*4) + @param index index pin number (optional input) + */ + Encoder(int encA, int encB , float ppr, int index = 0); + + /** encoder initialise pins */ + void init() override; + /** + * function enabling hardware interrupts for the encoder channels with provided callback functions + * if callback is not provided then the interrupt is not enabled + * + * @param doA pointer to the A channel interrupt handler function + * @param doB pointer to the B channel interrupt handler function + * @param doIndex pointer to the Index channel interrupt handler function + * + */ + void enableInterrupts(void (*doA)() = nullptr, void(*doB)() = nullptr, void(*doIndex)() = nullptr); + + // Encoder interrupt callback functions + /** A channel callback function */ + void handleA(); + /** B channel callback function */ + void handleB(); + /** Index channel callback function */ + void handleIndex(); + + + // pins A and B + int pinA; //!< encoder hardware pin A + int pinB; //!< encoder hardware pin B + int index_pin; //!< index pin + + // Encoder configuration + Pullup pullup; //!< Configuration parameter internal or external pullups + Quadrature quadrature;//!< Configuration parameter enable or disable quadrature mode + float cpr;//!< encoder cpr number + + // Abstract functions of the Sensor class implementation + /** get current angle (rad) */ + float getSensorAngle() override; + /** get current angular velocity (rad/s) */ + float getVelocity() override; + virtual void update() override; + + /** + * returns 0 if it does need search for absolute zero + * 0 - encoder without index + * 1 - ecoder with index + */ + int needsSearch() override; + + private: + int hasIndex(); //!< function returning 1 if encoder has index pin and 0 if not. + + volatile long pulse_counter;//!< current pulse counter + volatile long pulse_timestamp;//!< last impulse timestamp in us + volatile int A_active; //!< current active states of A channel + volatile int B_active; //!< current active states of B channel + volatile int I_active; //!< current active states of Index channel + volatile bool index_found = false; //!< flag stating that the index has been found + + // velocity calculation variables + float prev_Th, pulse_per_second; + volatile long prev_pulse_counter, prev_timestamp_us; +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.cpp new file mode 100644 index 0000000..3d4025f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.cpp @@ -0,0 +1,26 @@ +#include "GenericSensor.h" + + +/* + GenericSensor( float (*readCallback)() ) + - readCallback - pointer to the function which reads the sensor angle. +*/ + +GenericSensor::GenericSensor(float (*readCallback)(), void (*initCallback)()){ + // if function provided add it to the + if(readCallback != nullptr) this->readCallback = readCallback; + if(initCallback != nullptr) this->initCallback = initCallback; +} + +void GenericSensor::init(){ + // if init callback specified run it + if(initCallback != nullptr) this->initCallback(); + this->Sensor::init(); // call base class init +} + +/* + Shaft angle calculation +*/ +float GenericSensor::getSensorAngle(){ + return this->readCallback(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.h new file mode 100644 index 0000000..ba0dfe5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/GenericSensor.h @@ -0,0 +1,31 @@ +#ifndef GENERIC_SENSOR_LIB_H +#define GENERIC_SENSOR_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/base_classes/Sensor.h" + + +class GenericSensor: public Sensor{ + public: + /** + GenericSensor class constructor + * @param readCallback pointer to the function reading the sensor angle + * @param initCallback pointer to the function initialising the sensor + */ + GenericSensor(float (*readCallback)() = nullptr, void (*initCallback)() = nullptr); + + float (*readCallback)() = nullptr; //!< function pointer to sensor reading + void (*initCallback)() = nullptr; //!< function pointer to sensor initialisation + + void init() override; + + // Abstract functions of the Sensor class implementation + /** get current angle (rad) */ + float getSensorAngle() override; + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.cpp new file mode 100644 index 0000000..38767d5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.cpp @@ -0,0 +1,173 @@ +#include "HallSensor.h" + + +/* + HallSensor(int hallA, int hallB , int cpr, int index) + - hallA, hallB, hallC - HallSensor A, B and C pins + - pp - pole pairs +*/ +HallSensor::HallSensor(int _hallA, int _hallB, int _hallC, int _pp){ + + // hardware pins + pinA = _hallA; + pinB = _hallB; + pinC = _hallC; + + // hall has 6 segments per electrical revolution + cpr = _pp * 6; + + // extern pullup as default + pullup = Pullup::USE_EXTERN; +} + +// HallSensor interrupt callback functions +// A channel +void HallSensor::handleA() { + A_active= digitalRead(pinA); + updateState(); +} +// B channel +void HallSensor::handleB() { + B_active = digitalRead(pinB); + updateState(); +} + +// C channel +void HallSensor::handleC() { + C_active = digitalRead(pinC); + updateState(); +} + +/** + * Updates the state and sector following an interrupt + */ +void HallSensor::updateState() { + long new_pulse_timestamp = _micros(); + + int8_t new_hall_state = C_active + (B_active << 1) + (A_active << 2); + + // glitch avoidance #1 - sometimes we get an interrupt but pins haven't changed + if (new_hall_state == hall_state) { + return; + } + hall_state = new_hall_state; + + int8_t new_electric_sector = ELECTRIC_SECTORS[hall_state]; + static Direction old_direction; + if (new_electric_sector - electric_sector > 3) { + //underflow + direction = Direction::CCW; + electric_rotations += direction; + } else if (new_electric_sector - electric_sector < (-3)) { + //overflow + direction = Direction::CW; + electric_rotations += direction; + } else { + direction = (new_electric_sector > electric_sector)? Direction::CW : Direction::CCW; + } + electric_sector = new_electric_sector; + + // glitch avoidance #2 changes in direction can cause velocity spikes. Possible improvements needed in this area + if (direction == old_direction) { + // not oscilating or just changed direction + pulse_diff = new_pulse_timestamp - pulse_timestamp; + } else { + pulse_diff = 0; + } + + pulse_timestamp = new_pulse_timestamp; + total_interrupts++; + old_direction = direction; + if (onSectorChange != nullptr) onSectorChange(electric_sector); +} + +/** + * Optionally set a function callback to be fired when sector changes + * void onSectorChange(int sector) { + * ... // for debug or call driver directly? + * } + * sensor.attachSectorCallback(onSectorChange); + */ +void HallSensor::attachSectorCallback(void (*_onSectorChange)(int sector)) { + onSectorChange = _onSectorChange; +} + + + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void HallSensor::update() { + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + angle_prev_ts = pulse_timestamp; + long last_electric_rotations = electric_rotations; + int8_t last_electric_sector = electric_sector; + interrupts(); + angle_prev = ((float)((last_electric_rotations * 6 + last_electric_sector) % cpr) / (float)cpr) * _2PI ; + full_rotations = (int32_t)((last_electric_rotations * 6 + last_electric_sector) / cpr); +} + + + +/* + Shaft angle calculation + TODO: numerical precision issue here if the electrical rotation overflows the angle will be lost +*/ +float HallSensor::getSensorAngle() { + return ((float)(electric_rotations * 6 + electric_sector) / (float)cpr) * _2PI ; +} + +/* + Shaft velocity calculation + function using mixed time and frequency measurement technique +*/ +float HallSensor::getVelocity(){ + noInterrupts(); + long last_pulse_timestamp = pulse_timestamp; + long last_pulse_diff = pulse_diff; + interrupts(); + if (last_pulse_diff == 0 || ((long)(_micros() - last_pulse_timestamp) > last_pulse_diff*2) ) { // last velocity isn't accurate if too old + return 0; + } else { + return direction * (_2PI / (float)cpr) / (last_pulse_diff / 1000000.0f); + } + +} + +// HallSensor initialisation of the hardware pins +// and calculation variables +void HallSensor::init(){ + // initialise the electrical rotations to 0 + electric_rotations = 0; + + // HallSensor - check if pullup needed for your HallSensor + if(pullup == Pullup::USE_INTERN){ + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + pinMode(pinC, INPUT_PULLUP); + }else{ + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + pinMode(pinC, INPUT); + } + + // init hall_state + A_active= digitalRead(pinA); + B_active = digitalRead(pinB); + C_active = digitalRead(pinC); + updateState(); + + pulse_timestamp = _micros(); + + // we don't call Sensor::init() here because init is handled in HallSensor class. +} + +// function enabling hardware interrupts for the callback provided +// if callback is not provided then the interrupt is not enabled +void HallSensor::enableInterrupts(void (*doA)(), void(*doB)(), void(*doC)()){ + // attach interrupt if functions provided + + // A, B and C callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, CHANGE); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, CHANGE); + if(doC != nullptr) attachInterrupt(digitalPinToInterrupt(pinC), doC, CHANGE); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.h new file mode 100644 index 0000000..ad50c7d --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/HallSensor.h @@ -0,0 +1,98 @@ +#ifndef HALL_SENSOR_LIB_H +#define HALL_SENSOR_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// seq 1 > 5 > 4 > 6 > 2 > 3 > 1 000 001 010 011 100 101 110 111 +const int8_t ELECTRIC_SECTORS[8] = { -1, 0, 4, 5, 2, 1, 3 , -1 }; + +class HallSensor: public Sensor{ + public: + /** + HallSensor class constructor + @param encA HallSensor A pin + @param encB HallSensor B pin + @param encC HallSensor C pin + @param pp pole pairs (e.g hoverboard motor has 15pp and small gimbals often have 7pp) + @param index index pin number (optional input) + */ + HallSensor(int encA, int encB, int encC, int pp); + + /** HallSensor initialise pins */ + void init(); + /** + * function enabling hardware interrupts for the HallSensor channels with provided callback functions + * if callback is not provided then the interrupt is not enabled + * + * @param doA pointer to the A channel interrupt handler function + * @param doB pointer to the B channel interrupt handler function + * @param doIndex pointer to the Index channel interrupt handler function + * + */ + void enableInterrupts(void (*doA)() = nullptr, void(*doB)() = nullptr, void(*doC)() = nullptr); + + // HallSensor interrupt callback functions + /** A channel callback function */ + void handleA(); + /** B channel callback function */ + void handleB(); + /** C channel callback function */ + void handleC(); + + + // pins A and B + int pinA; //!< HallSensor hardware pin A + int pinB; //!< HallSensor hardware pin B + int pinC; //!< HallSensor hardware pin C + + // HallSensor configuration + Pullup pullup; //!< Configuration parameter internal or external pullups + int cpr;//!< HallSensor cpr number + + // Abstract functions of the Sensor class implementation + /** Interrupt-safe update */ + void update() override; + /** get current angle (rad) */ + float getSensorAngle() override; + /** get current angular velocity (rad/s) */ + float getVelocity() override; + + // whether last step was CW (+1) or CCW (-1). + Direction direction; + + void attachSectorCallback(void (*onSectorChange)(int a) = nullptr); + + // the current 3bit state of the hall sensors + volatile int8_t hall_state; + // the current sector of the sensor. Each sector is 60deg electrical + volatile int8_t electric_sector; + // the number of electric rotations + volatile long electric_rotations; + // this is sometimes useful to identify interrupt issues (e.g. weak or no pullup resulting in 1000s of interrupts) + volatile long total_interrupts; + + // variable used to filter outliers - rad/s + float velocity_max = 1000.0f; + + private: + + Direction decodeDirection(int oldState, int newState); + void updateState(); + + volatile unsigned long pulse_timestamp;//!< last impulse timestamp in us + volatile int A_active; //!< current active states of A channel + volatile int B_active; //!< current active states of B channel + volatile int C_active; //!< current active states of C channel + + // function pointer for on sector change call back + void (*onSectorChange)(int sector) = nullptr; + + volatile long pulse_diff; + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.cpp new file mode 100644 index 0000000..d4adad6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.cpp @@ -0,0 +1,42 @@ +#include "MagneticSensorAnalog.h" + +/** MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * @param _pinAnalog the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * @param _max_raw_count the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note: For ESP32 (with 12bit ADC the value will be nearer 4096) + */ +MagneticSensorAnalog::MagneticSensorAnalog(uint8_t _pinAnalog, int _min_raw_count, int _max_raw_count){ + + pinAnalog = _pinAnalog; + + cpr = _max_raw_count - _min_raw_count; + min_raw_count = _min_raw_count; + max_raw_count = _max_raw_count; + + if(pullup == Pullup::USE_INTERN){ + pinMode(pinAnalog, INPUT_PULLUP); + }else{ + pinMode(pinAnalog, INPUT); + } + +} + + +void MagneticSensorAnalog::init(){ + raw_count = getRawCount(); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorAnalog::getSensorAngle(){ + // raw data from the sensor + raw_count = getRawCount(); + return ( (float) (raw_count) / (float)cpr) * _2PI; +} + +// function reading the raw counter of the magnetic sensor +int MagneticSensorAnalog::getRawCount(){ + return analogRead(pinAnalog); +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.h new file mode 100644 index 0000000..6f787b9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorAnalog.h @@ -0,0 +1,51 @@ +#ifndef MAGNETICSENSORANALOG_LIB_H +#define MAGNETICSENSORANALOG_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +/** + * This sensor has been tested with AS5600 running in 'analog mode'. This is where output pin of AS6000 is connected to an analog pin on your microcontroller. + * This approach is very simple but you may more accurate results with MagneticSensorI2C if that is also supported as its skips the DAC -> ADC conversion (ADC supports MagneticSensorI2C) + */ +class MagneticSensorAnalog: public Sensor{ + public: + /** + * MagneticSensorAnalog class constructor + * @param _pinAnalog the pin to read the PWM signal + */ + MagneticSensorAnalog(uint8_t _pinAnalog, int _min = 0, int _max = 0); + + + /** sensor initialise pins */ + void init(); + + int pinAnalog; //!< encoder hardware pin A + + // Encoder configuration + Pullup pullup; + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + /** raw count (typically in range of 0-1023), useful for debugging resolution issues */ + int raw_count; + + private: + int min_raw_count; + int max_raw_count; + int cpr; + int read(); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.cpp new file mode 100644 index 0000000..ac2b273 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.cpp @@ -0,0 +1,156 @@ +#include "MagneticSensorI2C.h" + +/** Typical configuration for the 12bit AMS AS5600 magnetic sensor over I2C interface */ +MagneticSensorI2CConfig_s AS5600_I2C = { + .chip_address = 0x36, + .bit_resolution = 12, + .angle_register = 0x0C, + .data_start_bit = 11 +}; + +/** Typical configuration for the 12bit AMS AS5048 magnetic sensor over I2C interface */ +MagneticSensorI2CConfig_s AS5048_I2C = { + .chip_address = 0x40, // highly configurable. if A1 and A2 are held low, this is probable value + .bit_resolution = 14, + .angle_register = 0xFE, + .data_start_bit = 15 +}; + + +// MagneticSensorI2C(uint8_t _chip_address, float _cpr, uint8_t _angle_register_msb) +// @param _chip_address I2C chip address +// @param _bit_resolution bit resolution of the sensor +// @param _angle_register_msb angle read register +// @param _bits_used_msb number of used bits in msb +MagneticSensorI2C::MagneticSensorI2C(uint8_t _chip_address, int _bit_resolution, uint8_t _angle_register_msb, int _bits_used_msb){ + // chip I2C address + chip_address = _chip_address; + // angle read register of the magnetic sensor + angle_register_msb = _angle_register_msb; + // register maximum value (counts per revolution) + cpr = _powtwo(_bit_resolution); + + // depending on the sensor architecture there are different combinations of + // LSB and MSB register used bits + // AS5600 uses 0..7 LSB and 8..11 MSB + // AS5048 uses 0..5 LSB and 6..13 MSB + // used bits in LSB + lsb_used = _bit_resolution - _bits_used_msb; + // extraction masks + lsb_mask = (uint8_t)( (2 << lsb_used) - 1 ); + msb_mask = (uint8_t)( (2 << _bits_used_msb) - 1 ); + wire = &Wire; +} + +MagneticSensorI2C::MagneticSensorI2C(MagneticSensorI2CConfig_s config){ + chip_address = config.chip_address; + + // angle read register of the magnetic sensor + angle_register_msb = config.angle_register; + // register maximum value (counts per revolution) + cpr = _powtwo(config.bit_resolution); + + int bits_used_msb = config.data_start_bit - 7; + lsb_used = config.bit_resolution - bits_used_msb; + // extraction masks + lsb_mask = (uint8_t)( (2 << lsb_used) - 1 ); + msb_mask = (uint8_t)( (2 << bits_used_msb) - 1 ); + wire = &Wire; +} + +void MagneticSensorI2C::init(TwoWire* _wire){ + + wire = _wire; + + // I2C communication begin + wire->begin(); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorI2C::getSensorAngle(){ + // (number of full rotations)*2PI + current sensor angle + return ( getRawCount() / (float)cpr) * _2PI ; +} + + + +// function reading the raw counter of the magnetic sensor +int MagneticSensorI2C::getRawCount(){ + return (int)MagneticSensorI2C::read(angle_register_msb); +} + +// I2C functions +/* +* Read a register from the sensor +* Takes the address of the register as a uint8_t +* Returns the value of the register +*/ +int MagneticSensorI2C::read(uint8_t angle_reg_msb) { + // read the angle register first MSB then LSB + byte readArray[2]; + uint16_t readValue = 0; + // notify the device that is aboout to be read + wire->beginTransmission(chip_address); + wire->write(angle_reg_msb); + currWireError = wire->endTransmission(false); + + // read the data msb and lsb + wire->requestFrom(chip_address, (uint8_t)2); + for (byte i=0; i < 2; i++) { + readArray[i] = wire->read(); + } + + // depending on the sensor architecture there are different combinations of + // LSB and MSB register used bits + // AS5600 uses 0..7 LSB and 8..11 MSB + // AS5048 uses 0..5 LSB and 6..13 MSB + readValue = ( readArray[1] & lsb_mask ); + readValue += ( ( readArray[0] & msb_mask ) << lsb_used ); + return readValue; +} + +/* +* Checks whether other devices have locked the bus. Can clear SDA locks. +* This should be called before sensor.init() on devices that suffer i2c slaves locking sda +* e.g some stm32 boards with AS5600 chips +* Takes the sda_pin and scl_pin +* Returns 0 for OK, 1 for other master and 2 for unfixable sda locked LOW +*/ +int MagneticSensorI2C::checkBus(byte sda_pin, byte scl_pin) { + + pinMode(scl_pin, INPUT_PULLUP); + pinMode(sda_pin, INPUT_PULLUP); + delay(250); + + if (digitalRead(scl_pin) == LOW) { + // Someone else has claimed master!"); + return 1; + } + + if(digitalRead(sda_pin) == LOW) { + // slave is communicating and awaiting clocks, we are blocked + pinMode(scl_pin, OUTPUT); + for (byte i = 0; i < 16; i++) { + // toggle clock for 2 bytes of data + digitalWrite(scl_pin, LOW); + delayMicroseconds(20); + digitalWrite(scl_pin, HIGH); + delayMicroseconds(20); + } + pinMode(sda_pin, INPUT); + delayMicroseconds(20); + if (digitalRead(sda_pin) == LOW) { + // SDA still blocked + return 2; + } + _delay(1000); + } + // SDA is clear (HIGH) + pinMode(sda_pin, INPUT); + pinMode(scl_pin, INPUT); + + return 0; +} diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.h new file mode 100644 index 0000000..f8b189f --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorI2C.h @@ -0,0 +1,84 @@ +#ifndef MAGNETICSENSORI2C_LIB_H +#define MAGNETICSENSORI2C_LIB_H + +#include "Arduino.h" +#include +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +struct MagneticSensorI2CConfig_s { + int chip_address; + int bit_resolution; + int angle_register; + int data_start_bit; +}; +// some predefined structures +extern MagneticSensorI2CConfig_s AS5600_I2C,AS5048_I2C; + +#if defined(TARGET_RP2040) +#define SDA I2C_SDA +#define SCL I2C_SCL +#endif + + +class MagneticSensorI2C: public Sensor{ + public: + /** + * MagneticSensorI2C class constructor + * @param chip_address I2C chip address + * @param bits number of bits of the sensor resolution + * @param angle_register_msb angle read register msb + * @param _bits_used_msb number of used bits in msb + */ + MagneticSensorI2C(uint8_t _chip_address, int _bit_resolution, uint8_t _angle_register_msb, int _msb_bits_used); + + /** + * MagneticSensorI2C class constructor + * @param config I2C config + */ + MagneticSensorI2C(MagneticSensorI2CConfig_s config); + + static MagneticSensorI2C AS5600(); + + /** sensor initialise pins */ + void init(TwoWire* _wire = &Wire); + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + + /** experimental function to check and fix SDA locked LOW issues */ + int checkBus(byte sda_pin , byte scl_pin ); + + /** current error code from Wire endTransmission() call **/ + uint8_t currWireError = 0; + + private: + float cpr; //!< Maximum range of the magnetic sensor + uint16_t lsb_used; //!< Number of bits used in LSB register + uint8_t lsb_mask; + uint8_t msb_mask; + + // I2C variables + uint8_t angle_register_msb; //!< I2C angle register to read + uint8_t chip_address; //!< I2C chip select pins + + // I2C functions + /** Read one I2C register value */ + int read(uint8_t angle_register_msb); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + /* the two wire instance for this sensor */ + TwoWire* wire; + + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.cpp new file mode 100644 index 0000000..cf21164 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.cpp @@ -0,0 +1,117 @@ +#include "MagneticSensorPWM.h" +#include "Arduino.h" + +/** MagneticSensorPWM(uint8_t _pinPWM, int _min, int _max) + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading + * @param _max_raw_count the largest expected reading + */ +MagneticSensorPWM::MagneticSensorPWM(uint8_t _pinPWM, int _min_raw_count, int _max_raw_count){ + + pinPWM = _pinPWM; + + cpr = _max_raw_count - _min_raw_count + 1; + min_raw_count = _min_raw_count; + max_raw_count = _max_raw_count; + + // define if the sensor uses interrupts + is_interrupt_based = false; + + // define as not set + last_call_us = _micros(); +} + + +/** MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks) + * + * Constructor that computes the min and max raw counts based on the PWM frequency and the number of PWM clocks in one period + * + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param freqHz the frequency of the PWM signal, in Hz, e.g. 115, 230, 460 or 920 for the AS5600, depending on the PWM frequency setting + * @param _total_pwm_clocks the total number of PWM clocks in one period, e.g. 4351 for the AS5600 + * @param _min_pwm_clocks the 0 value returned by the sensor, in PWM clocks, e.g. 128 for the AS5600 + * @param _max_pwm_clocks the largest value returned by the sensor, in PWM clocks, e.g. 4223 for the AS5600 + */ +MagneticSensorPWM::MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks){ + + pinPWM = _pinPWM; + + min_raw_count = lroundf(1000000.0f/freqHz/_total_pwm_clocks*_min_pwm_clocks); + max_raw_count = lroundf(1000000.0f/freqHz/_total_pwm_clocks*_max_pwm_clocks); + cpr = max_raw_count - min_raw_count + 1; + + // define if the sensor uses interrupts + is_interrupt_based = false; + + min_elapsed_time = 1.0f/freqHz; // set the minimum time between two readings + + // define as not set + last_call_us = _micros(); +} + + + +void MagneticSensorPWM::init(){ + + // initial hardware + pinMode(pinPWM, INPUT); + raw_count = getRawCount(); + pulse_timestamp = _micros(); + + this->Sensor::init(); // call base class init +} + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void MagneticSensorPWM::update() { + if (is_interrupt_based) + noInterrupts(); + Sensor::update(); + angle_prev_ts = pulse_timestamp; // Timestamp of actual sample, before the time-consuming PWM communication + if (is_interrupt_based) + interrupts(); +} + +// get current angle (rad) +float MagneticSensorPWM::getSensorAngle(){ + // raw data from sensor + raw_count = getRawCount(); + if (raw_count > max_raw_count) raw_count = max_raw_count; + if (raw_count < min_raw_count) raw_count = min_raw_count; + return( (float) (raw_count - min_raw_count) / (float)cpr) * _2PI; +} + + +// read the raw counter of the magnetic sensor +int MagneticSensorPWM::getRawCount(){ + if (!is_interrupt_based){ // if it's not interrupt based read the value in a blocking way + pulse_timestamp = _micros(); // ideally this should be done right at the rising edge of the pulse + pulse_length_us = pulseIn(pinPWM, HIGH, 1200); // 1200us timeout, should this be configurable? + } + return pulse_length_us; +} + + +void MagneticSensorPWM::handlePWM() { + // unsigned long now_us = ticks(); + unsigned long now_us = _micros(); + + // if falling edge, calculate the pulse length + if (!digitalRead(pinPWM)) { + pulse_length_us = now_us - last_call_us; + pulse_timestamp = last_call_us; // angle was sampled at the rising edge of the pulse, so use that timestamp + } + + // save the currrent timestamp for the next call + last_call_us = now_us; + is_interrupt_based = true; // set the flag to true +} + +// function enabling hardware interrupts of the for the callback provided +// if callback is not provided then the interrupt is not enabled +void MagneticSensorPWM::enableInterrupt(void (*doPWM)()){ + // declare it's interrupt based + is_interrupt_based = true; + + // enable interrupts on pwm input pin + attachInterrupt(digitalPinToInterrupt(pinPWM), doPWM, CHANGE); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.h new file mode 100644 index 0000000..48492c8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorPWM.h @@ -0,0 +1,73 @@ +#ifndef MAGNETICSENSORPWM_LIB_H +#define MAGNETICSENSORPWM_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// This sensor has been tested with AS5048a running in PWM mode. + +class MagneticSensorPWM: public Sensor{ + public: + /** MagneticSensorPWM(uint8_t _pinPWM, int _min, int _max) + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading + * @param _max_raw_count the largest expected reading + */ + MagneticSensorPWM(uint8_t _pinPWM,int _min = 0, int _max = 0); + /** MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks) + * + * Constructor that computes the min and max raw counts based on the PWM frequency and the number of PWM clocks in one period + * + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param freqHz the frequency of the PWM signal, in Hz, e.g. 115, 230, 460 or 920 for the AS5600, depending on the PWM frequency setting + * @param _total_pwm_clocks the total number of PWM clocks in one period, e.g. 4351 for the AS5600 + * @param _min_pwm_clocks the 0 value returned by the sensor, in PWM clocks, e.g. 128 for the AS5600 + * @param _max_pwm_clocks the largest value returned by the sensor, in PWM clocks, e.g. 4223 for the AS5600 + */ + MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks); + + // initialize the sensor hardware + void init(); + + int pinPWM; + + // Interrupt-safe update + void update() override; + + // get current angle (rad) + float getSensorAngle() override; + + // pwm handler + void handlePWM(); + void enableInterrupt(void (*doPWM)()); + unsigned long pulse_length_us; + + private: + // raw count (typically in range of 0-1023) + int raw_count; + int min_raw_count; + int max_raw_count; + int cpr; + + // flag saying if the readings are interrupt based or not + bool is_interrupt_based; + + int read(); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + // time tracking variables + unsigned long last_call_us; + // unsigned long pulse_length_us; + unsigned long pulse_timestamp; + + +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.cpp b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.cpp new file mode 100644 index 0000000..52febc3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.cpp @@ -0,0 +1,161 @@ + +#include "MagneticSensorSPI.h" + +/** Typical configuration for the 14bit AMS AS5147 magnetic sensor over SPI interface */ +MagneticSensorSPIConfig_s AS5147_SPI = { + .spi_mode = SPI_MODE1, + .clock_speed = 1000000, + .bit_resolution = 14, + .angle_register = 0x3FFF, + .data_start_bit = 13, + .command_rw_bit = 14, + .command_parity_bit = 15 +}; +// AS5048 and AS5047 are the same as AS5147 +MagneticSensorSPIConfig_s AS5048_SPI = AS5147_SPI; +MagneticSensorSPIConfig_s AS5047_SPI = AS5147_SPI; + +/** Typical configuration for the 14bit MonolithicPower MA730 magnetic sensor over SPI interface */ +MagneticSensorSPIConfig_s MA730_SPI = { + .spi_mode = SPI_MODE0, + .clock_speed = 1000000, + .bit_resolution = 14, + .angle_register = 0x0000, + .data_start_bit = 15, + .command_rw_bit = 0, // not required + .command_parity_bit = 0 // parity not implemented +}; + + +// MagneticSensorSPI(int cs, float _bit_resolution, int _angle_register) +// cs - SPI chip select pin +// _bit_resolution sensor resolution bit number +// _angle_register - (optional) angle read register - default 0x3FFF +MagneticSensorSPI::MagneticSensorSPI(int cs, int _bit_resolution, int _angle_register){ + + chip_select_pin = cs; + // angle read register of the magnetic sensor + angle_register = _angle_register ? _angle_register : DEF_ANGLE_REGISTER; + // register maximum value (counts per revolution) + cpr = _powtwo(_bit_resolution); + spi_mode = SPI_MODE1; + clock_speed = 1000000; + bit_resolution = _bit_resolution; + + command_parity_bit = 15; // for backwards compatibilty + command_rw_bit = 14; // for backwards compatibilty + data_start_bit = 13; // for backwards compatibilty +} + +MagneticSensorSPI::MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs){ + chip_select_pin = cs; + // angle read register of the magnetic sensor + angle_register = config.angle_register ? config.angle_register : DEF_ANGLE_REGISTER; + // register maximum value (counts per revolution) + cpr = _powtwo(config.bit_resolution); + spi_mode = config.spi_mode; + clock_speed = config.clock_speed; + bit_resolution = config.bit_resolution; + + command_parity_bit = config.command_parity_bit; // for backwards compatibilty + command_rw_bit = config.command_rw_bit; // for backwards compatibilty + data_start_bit = config.data_start_bit; // for backwards compatibilty +} + +void MagneticSensorSPI::init(SPIClass* _spi){ + spi = _spi; + // 1MHz clock (AMS should be able to accept up to 10MHz) + settings = SPISettings(clock_speed, MSBFIRST, spi_mode); + //setup pins + pinMode(chip_select_pin, OUTPUT); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + // do any architectures need to set the clock divider for SPI? Why was this in the code? + //spi->setClockDivider(SPI_CLOCK_DIV8); + digitalWrite(chip_select_pin, HIGH); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorSPI::getSensorAngle(){ + return (getRawCount() / (float)cpr) * _2PI; +} + +// function reading the raw counter of the magnetic sensor +int MagneticSensorSPI::getRawCount(){ + return (int)MagneticSensorSPI::read(angle_register); +} + +// SPI functions +/** + * Utility function used to calculate even parity of word + */ +byte MagneticSensorSPI::spiCalcEvenParity(word value){ + byte cnt = 0; + byte i; + + for (i = 0; i < 16; i++) + { + if (value & 0x1) cnt++; + value >>= 1; + } + return cnt & 0x1; +} + + /* + * Read a register from the sensor + * Takes the address of the register as a 16 bit word + * Returns the value of the register + */ +word MagneticSensorSPI::read(word angle_register){ + + word command = angle_register; + + if (command_rw_bit > 0) { + command = angle_register | (1 << command_rw_bit); + } + if (command_parity_bit > 0) { + //Add a parity bit on the the MSB + command |= ((word)spiCalcEvenParity(command) << command_parity_bit); + } + + //SPI - begin transaction + spi->beginTransaction(settings); + + //Send the command + digitalWrite(chip_select_pin, LOW); + spi->transfer16(command); + digitalWrite(chip_select_pin,HIGH); + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) // if ESP32 board + delayMicroseconds(50); // why do we need to delay 50us on ESP32? In my experience no extra delays are needed, on any of the architectures I've tested... +#else + delayMicroseconds(1); // delay 1us, the minimum time possible in plain arduino. 350ns is the required time for AMS sensors, 80ns for MA730, MA702 +#endif + + //Now read the response + digitalWrite(chip_select_pin, LOW); + word register_value = spi->transfer16(0x00); + digitalWrite(chip_select_pin, HIGH); + + //SPI - end transaction + spi->endTransaction(); + + register_value = register_value >> (1 + data_start_bit - bit_resolution); //this should shift data to the rightmost bits of the word + + const static word data_mask = 0xFFFF >> (16 - bit_resolution); + + return register_value & data_mask; // Return the data, stripping the non data (e.g parity) bits +} + +/** + * Closes the SPI connection + * SPI has an internal SPI-device counter, for each init()-call the close() function must be called exactly 1 time + */ +void MagneticSensorSPI::close(){ + spi->end(); +} + + diff --git a/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.h b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.h new file mode 100644 index 0000000..03ebde4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/Simple FOC/src/sensors/MagneticSensorSPI.h @@ -0,0 +1,84 @@ +#ifndef MAGNETICSENSORSPI_LIB_H +#define MAGNETICSENSORSPI_LIB_H + + +#include "Arduino.h" +#include +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +#define DEF_ANGLE_REGISTER 0x3FFF + +struct MagneticSensorSPIConfig_s { + int spi_mode; + long clock_speed; + int bit_resolution; + int angle_register; + int data_start_bit; + int command_rw_bit; + int command_parity_bit; +}; +// typical configuration structures +extern MagneticSensorSPIConfig_s AS5147_SPI,AS5048_SPI,AS5047_SPI, MA730_SPI; + +class MagneticSensorSPI: public Sensor{ + public: + /** + * MagneticSensorSPI class constructor + * @param cs SPI chip select pin + * @param bit_resolution sensor resolution bit number + * @param angle_register (optional) angle read register - default 0x3FFF + */ + MagneticSensorSPI(int cs, int bit_resolution, int angle_register = 0); + /** + * MagneticSensorSPI class constructor + * @param config SPI config + * @param cs SPI chip select pin + */ + MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs); + + /** sensor initialise pins */ + void init(SPIClass* _spi = &SPI); + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + + // returns the spi mode (phase/polarity of read/writes) i.e one of SPI_MODE0 | SPI_MODE1 | SPI_MODE2 | SPI_MODE3 + int spi_mode; + + /* returns the speed of the SPI clock signal */ + long clock_speed; + + + private: + float cpr; //!< Maximum range of the magnetic sensor + // spi variables + int angle_register; //!< SPI angle register to read + int chip_select_pin; //!< SPI chip select pin + SPISettings settings; //!< SPI settings variable + // spi functions + /** Stop SPI communication */ + void close(); + /** Read one SPI register value */ + word read(word angle_register); + /** Calculate parity value */ + byte spiCalcEvenParity(word value); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + int bit_resolution; //!< the number of bites of angle data + int command_parity_bit; //!< the bit where parity flag is stored in command + int command_rw_bit; //!< the bit where read/write flag is stored in command + int data_start_bit; //!< the the position of first bit + + SPIClass* spi; +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.github/workflows/ccpp.yml b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.github/workflows/ccpp.yml new file mode 100644 index 0000000..bd74689 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.github/workflows/ccpp.yml @@ -0,0 +1,76 @@ +name: Library Compile +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update + project-type: library + build: + name: Test compile + runs-on: ubuntu-latest + strategy: + matrix: + arduino-boards-fqbn: + - arduino:avr:uno # arudino uno + - arduino:sam:arduino_due_x # arduino due + - arduino:samd:nano_33_iot # samd21 + - adafruit:samd:adafruit_metro_m4 # samd51 + - esp32:esp32:esp32 # esp32 + - esp32:esp32:esp32s2 # esp32s2 + - STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # stm32 bluepill + - STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # stm32 nucleo + - arduino:mbed_rp2040:pico # rpi pico + include: + - arduino-boards-fqbn: arduino:avr:uno + sketches-exclude: calibrated mt6816_spi smoothing + required-libraries: Simple FOC + - arduino-boards-fqbn: arduino:sam:arduino_due_x + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: arduino:samd:nano_33_iot + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: arduino:mbed_rp2040:pico + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: adafruit:samd:adafruit_metro_m4 + platform-url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + # - arduino-boards-fqbn: esp32:esp32:esp32doit-devkit-v1 + # platform-url: https://dl.espressif.com/dl/package_esp32_index.json + # required-libraries: Simple FOC + # sketch-names: '**.ino' + - arduino-boards-fqbn: esp32:esp32:esp32 # esp32 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: esp32:esp32:esp32s2 # esp32s2 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated mt6816_spi smoothing + - arduino-boards-fqbn: STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + required-libraries: Simple FOC + sketches-exclude: smoothing + # Do not cancel all jobs / architectures if one job fails + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@master + - name: Compile all examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} + required-libraries: ${{ matrix.required-libraries }} + platform-url: ${{ matrix.platform-url }} + sketch-names: ${{ matrix.sketch-names }} + sketches-exclude: ${{ matrix.sketches-exclude }} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.gitignore b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.gitignore new file mode 100644 index 0000000..ce7f6d0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.gitignore @@ -0,0 +1,2 @@ +.project +.DS_Store diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.piopm b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.piopm new file mode 100644 index 0000000..284f86d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "SimpleFOCDrivers", "version": "1.0.5", "spec": {"owner": "simplefoc", "id": 13496, "name": "SimpleFOCDrivers", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/LICENSE b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/LICENSE new file mode 100644 index 0000000..53dcc14 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Richard Unger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/README.md new file mode 100644 index 0000000..e5f571d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/README.md @@ -0,0 +1,112 @@ +# SimpleFOC Driver and Support Library + +![Library Compile](https://github.com/simplefoc/Arduino-FOC-drivers/workflows/Library%20Compile/badge.svg) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +![Release](https://www.ardu-badge.com/badge/SimpleFOCDrivers.svg?) + +This library contains an assortment of drivers and supporting code for SimpleFOC. + +The intent is to keep the core of SimpleFOC clean, and thus easy to maintain, understand and port to different platforms. In addition to this core, there are various drivers and supporting code which has grown around SimpleFOC, and which we would like to make available to the community. + +## New Release + +v1.0.5 - Released July 2023, for Simple FOC 2.3.1 + +What's changed since 1.0.4? +- Added smoothing sensor by [@dekutree64](https://github.com/dekutree64) +- Added TMD6200 SPI driver by [@YaseenTwati](https://github.com/YaseenTwati) +- Added HybridStepperMotor by [@VIPQualityPost](https://github.com/VIPQualityPost) +- New Settings abstraction to load and save SimpleFOC settings and calibration +- New Settings driver: SAMDNVMSettingsStorage +- SimpleFOCRegisters abstraction, mapping SimpleFOC parameters to virtual "Registers" +- Updated I2CCommander to use the new registers abstraction +- Bugfixes [included](https://github.com/simplefoc/Arduino-FOC-drivers/issues?q=is%3Aissue+milestone%3A1.0.5+) + +## What is included + +What is here? See the sections below. Each driver or function should come with its own more detailed README. + +### Motor/Gate driver ICs + + - [TMC6200 driver](src/drivers/tmc6200/) - SPI driver for Trinamics TMC6200 motor driver IC. + - [DRV8316 driver](src/drivers/drv8316/) - SPI driver for TI's DRV8316 motor driver IC. + +### Encoders + + - [AS5048A SPI driver](src/encoders/as5048a/) - SPI driver for the AMS AS5048A absolute position magnetic rotary encoder IC. + - [AS5047 SPI driver](src/encoders/as5047/) - SPI driver for the AMS AS5047P and AS5047D absolute position magnetic rotary encoder ICs. + - [AS5047U SPI driver](src/encoders/as5047u/) - SPI driver for the AMS AS5047U absolute position magnetic rotary encoder ICs. + - [MA730 SPI driver](src/encoders/ma730/) - SPI driver for the MPS MagAlpha MA730 absolute position magnetic rotary encoder IC. + - [MA730 SSI driver](src/encoders/ma730/) - SSI driver for the MPS MagAlpha MA730 absolute position magnetic rotary encoder IC. + - [AS5145 SSI driver](src/encoders/as5145/) - SSI driver for the AMS AS5145 and AS5045 absolute position magnetic rotary encoder ICs. + - [TLE5012B SPI driver](src/encoders/tle5012b/) - SPI (half duplex) driver for TLE5012B absolute position magnetic rotary encoder IC. + - [STM32 Hardware Encoder](src/encoders/stm32hwencoder/) - Hardware timer based encoder driver for ABI type quadrature encoders. + - [SC60228 SPI driver](src/encoders/sc60228/) - SPI driver for SemiMent SC60288 magnetic encoder IC. + - [MA330 SPI driver](src/encoders/ma330/) - SPI driver for the MPS MagAlpha MA330 absolute position magnetic rotary encoder IC. + - [MT6816 SPI driver](src/encoders/mt6816/) - SPI driver for the MagnTek MT6816 absolute position magnetic rotary encoder IC. + - [MT6701 SSI driver](src/encoders/mt6701/) - SSI driver for the MagnTek MT6701 absolute position magnetic rotary encoder IC. + - [MT6835 SPI driver](src/encoders/mt6835/) - SPI driver for the MagnTek MT6835 21 bit magnetic rotary encoder IC. + - [STM32 PWM sensor driver](src/encoders/stm32pwmsensor/) - STM32 native timer-based driver for PWM angle sensors. + - [SmoothingSensor](src/encoders/smoothing/) - A SimpleFOC Sensor wrapper implementation which adds angle extrapolation. + +### Communications + + - [I2CCommander I2C driver](src/comms/i2c/) - I2C communications protocol and drivers for both controller and target devices, based on register abstraction + - [STM32 SpeedDir Input](src/comms/stm32speeddir/) - Control target velocity with PWM speed and direction inputs + - [SerialBinaryCommander](src/comms/serial/) - Serial communications with binary protocol, based on register abstraction + - [Telemetry](src/comms/telemetry/) - Telemetry abstraction, based on registers + - [SerialASCIITelemetry](src/comms/serial/) - Serial communications with ascii protocol, based on register abstraction + +### Settings + +Load and store SimpleFOC motor settings, based on register abstraction. + + - [SAMD NVM storage driver](src/settings/samd/) - Store settings to the NVM flash memory in your SAMD MCU + - [CAT24 I2C EEPROM storage driver](src/settings/i2c/) - Store settings to CAT24 I2C EEPROMs + +### Utilities + + - [STM32 PWM Input driver](src/utilities/stm32pwm/) - PWM Input driver for STM32 MCUs. Accurately measure PWM inputs with zero MCU overhead. + + +## How to use + +#### Arduino Library Manager +The simplest way to get hold of the library is directly by using Arduino IDE and its integrated Library Manager. +- Open Arduino IDE and start Arduino Library Manager by clicking: `Tools > Manage Libraries...`. +- Search for `Simple FOC drivers` library and install the latest version. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC drivers`. + +#### Using Github website +- Go to the [github repository](https://github.com/simplefoc/Arduino-FOC-drivers) +- Click first on `Clone or Download > Download ZIP`. +- Unzip it and place it in `Arduino Libraries` folder. Windows: `Documents > Arduino > libraries`. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC drivers`. + +#### Using parts + +You can copy parts of the library, for example to minimize your code size, or make it easier to add adaptations of your own. +If you do so, please be sure to adhere to and include the [LICENSE](https://github.com/simplefoc/Arduino-FOC-drivers/LICENSE). + + +## Further Documentation + +Find out more information about the Arduino SimpleFOC project on the [docs website](https://docs.simplefoc.com/) + +## Release History + +What's changed since 1.0.3? +- New Comms/Input: STM32SpeedDirCommander +- New Utility: STM32PWMInput +- Fixed MT6835 driver bugs +- Improved AS5047 driver, fixed bugs +- Improved AS5047U driver, fixed bugs + +What's changed since 1.0.2? +- New Sensor: MT6835 +- Fixed bugs + +What's changed since 1.0.1? +- Calibrated sensor by @MarethyuPrefect +- New Sensors: MT6701, MA330, MT6816 +- Fixed bugs diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino new file mode 100644 index 0000000..be89c74 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino @@ -0,0 +1,123 @@ + + + +#include "Arduino.h" +#include +#include +#include +#include "drivers/drv8316/drv8316.h" + + + + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver3PWM driver = DRV8316Driver3PWM(2,3,4,7,false); // use the right pins for your setup! +#define ENABLE_A 0 +#define ENABLE_B 1 +#define ENABLE_C 6 + + +void printDRV8316Status() { + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +} + + + + + +void setup() { + + Serial.begin(115200); + while (!Serial); + delay(1); + Serial.println("Initializing..."); + + pinMode(ENABLE_A, OUTPUT); + digitalWrite(ENABLE_A, 1); // enable + pinMode(ENABLE_B, OUTPUT); + digitalWrite(ENABLE_B, 1); // enable + pinMode(ENABLE_C, OUTPUT); + digitalWrite(ENABLE_C, 1); // enable + + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity_openloop; + motor.voltage_limit = 3; + motor.velocity_limit = 20; + motor.init(); + Serial.println("Init complete..."); + + delay(100); + printDRV8316Status(); +} + + +// velocity set point variable +float target_velocity = 7.0; + + +void loop() { + //delay(100); + //driver.setPwm(7.4/4, 7.4/2, 7.4/4*3); + motor.move(target_velocity); +} + + + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino new file mode 100644 index 0000000..2eb0dd3 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino @@ -0,0 +1,111 @@ + + + +#include "Arduino.h" +#include +#include +#include +#include "drivers/drv8316/drv8316.h" + + + + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver6PWM driver = DRV8316Driver6PWM(0,1,2,3,4,6,7,false); // use the right pins for your setup! + + + +void printDRV8316Status() { + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +} + + + + + +void setup() { + Serial.begin(115200); + while (!Serial); + delay(1); + Serial.println("Initializing..."); + + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity_openloop; + motor.voltage_limit = 3; + motor.velocity_limit = 20; + motor.init(); + Serial.println("Init complete..."); + + delay(100); + printDRV8316Status(); +} + + +// velocity set point variable +float target_velocity = 7.0; + + +void loop() { + motor.move(target_velocity); +} + + + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino new file mode 100644 index 0000000..6cc6110 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino @@ -0,0 +1,86 @@ +/** + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include +#include +#include "encoders/calibrated/CalibratedSensor.h" + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5048_SPI, PB6); +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// instantiate the calibrated sensor object +CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); + +// voltage set point variable +float target_voltage = 2; + +// instantiate the commander +Commander command = Commander(Serial); + +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + SPI.setMISO(PB14); + SPI.setMOSI(PB15); + SPI.setSCLK(PB13); + + sensor.init(); + // Link motor to sensor + motor.linkSensor(&sensor); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + + // set voltage to run calibration + sensor_calibrated.voltage_calibration = 6; + // Running calibration + sensor_calibrated.calibrate(motor); + + //Serial.println("Calibrating Sensor Done."); + // Linking sensor to motor object + motor.linkSensor(&sensor_calibrated); + + // calibrated init FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + motor.loopFOC(); + motor.move(target_voltage); + command.run(); + motor.monitor(); + +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino new file mode 100644 index 0000000..c1ac1cd --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino @@ -0,0 +1,106 @@ +/** + * Comprehensive BLDC motor control example using magnetic sensor MT6816 + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include +#include +#include + +// magnetic sensor instance - MT6816 SPI mode +MagneticSensorMT6816 sensor = MagneticSensorMT6816(5); + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(32, 25, 26, 33); + +// Inline Current Sense instance +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, 35, 34); + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the control type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + current_sense.linkDriver(&driver); + current_sense.init(); + current_sense.gain_b *= -1; + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino new file mode 100644 index 0000000..1ff658d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino @@ -0,0 +1,141 @@ +/** + * + * Hall sensor velocity motion control example, modified to demonstrate usage of SmoothingSensor + * Steps: + * 1) Configure the motor and sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * 4) Try with and without smoothing to see the difference (send E1 and E0 commands from serial terminal) + * + * + * + * NOTE : + * > Specifically for Arduino UNO example code for running velocity motion control using a hall sensor + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doC directly to the sensor.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + */ +#include +// software interrupt library +#include +#include +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); +// wrapper instance +SmoothingSensor smooth(sensor, motor); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(sensor.pinC, doC); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void enableSmoothing(char* cmd) { + float enable; + command.scalar(&enable, cmd); + motor.linkSensor(enable == 0 ? (Sensor*)&sensor : (Sensor*)&smooth); +} + +void setup() { + + // initialize sensor sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenerIndex); + // set SmoothingSensor phase correction for hall sensors + smooth.phase_correction = -_PI_6; + // link the SmoothingSensor to the motor + motor.linkSensor(&smooth); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + // add smoothing enable/disable command E (send E0 to use hall sensor alone, or E1 to use smoothing) + command.add('E', enableSmoothing, "enable smoothing"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/keywords.txt b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/keywords.txt new file mode 100644 index 0000000..516cc85 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/keywords.txt @@ -0,0 +1,11 @@ +SimpleFOC KEYWORD1 +DRV8316 KEYWORD1 +AS5048A KEYWORD1 +AS5047 KEYWORD1 +AS5145 KEYWORD1 +MA730 KEYWORD1 +MT6835 KEYWORD1 +SC60228 KEYWORD1 +TLE5012B KEYWORD1 +I2CCommander KEYWORD1 +STM32HWEncoder KEYWORD1 diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/library.properties b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/library.properties new file mode 100644 index 0000000..d3565e4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/library.properties @@ -0,0 +1,11 @@ +name=SimpleFOCDrivers +version=1.0.5 +author=Simplefoc +maintainer=Simplefoc +sentence=A library of supporting drivers for SimpleFOC. Motor drivers chips, encoder chips, current sensing and supporting code. +paragraph=SimpleFOC runs BLDC and Stepper motors using the FOC algorithm. This library supports the core SimpleFOC code by adding support for specific hardware like motor driver ICs, encoders, current sensing and other supporting code. +category=Device Control +url=https://docs.simplefoc.com +architectures=* +includes=SimpleFOCDrivers.h +depends=Simple FOC \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/SimpleFOCDrivers.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/SimpleFOCDrivers.h new file mode 100644 index 0000000..689ea30 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/SimpleFOCDrivers.h @@ -0,0 +1,11 @@ + +#ifndef __SIMPLEFOC_DRIVERS_H__ +#define __SIMPLEFOC_DRIVERS_H__ + +// empty header file. In Arduino IDE, include this header to enable the +// IDE to "find" the library. +// Then include the headers for the individual drivers/sensors/utilities +// that you want to use. + +#endif + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/README.md new file mode 100644 index 0000000..4469af6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/README.md @@ -0,0 +1,5 @@ + +# SimpleFOC communications support code + +This folder contains classes to support you communicating between MCUs running SimpleFOC, and other systems. + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp new file mode 100644 index 0000000..44f7c54 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp @@ -0,0 +1,131 @@ + +#include "./RegisterReceiver.h" +#include "BLDCMotor.h" + + +void RegisterReceiver::setRegister(SimpleFOCRegister reg, FOCMotor* motor){ + // write a register value for the given motor + switch(reg) { + case REG_ENABLE: + readByte((uint8_t*)&(motor->enabled)); + break; + case REG_MODULATION_MODE: + readByte((uint8_t*)&(motor->foc_modulation)); + break; + case REG_TORQUE_MODE: + readByte((uint8_t*)&(motor->torque_controller)); + break; + case REG_CONTROL_MODE: + readByte((uint8_t*)&(motor->controller)); + break; + case REG_TARGET: + readFloat(&(motor->target)); + break; + case REG_VEL_PID_P: + readFloat(&(motor->PID_velocity.P)); + break; + case REG_VEL_PID_I: + readFloat(&(motor->PID_velocity.I)); + break; + case REG_VEL_PID_D: + readFloat(&(motor->PID_velocity.D)); + break; + case REG_VEL_LPF_T: + readFloat(&(motor->LPF_velocity.Tf)); + break; + case REG_ANG_PID_P: + readFloat(&(motor->P_angle.P)); + break; + case REG_VEL_LIMIT: + readFloat(&(motor->velocity_limit)); + break; + case REG_VEL_MAX_RAMP: + readFloat(&(motor->PID_velocity.output_ramp)); + break; + + case REG_CURQ_PID_P: + readFloat(&(motor->PID_current_q.P)); + break; + case REG_CURQ_PID_I: + readFloat(&(motor->PID_current_q.I)); + break; + case REG_CURQ_PID_D: + readFloat(&(motor->PID_current_q.D)); + break; + case REG_CURQ_LPF_T: + readFloat(&(motor->LPF_current_q.Tf)); + break; + case REG_CURD_PID_P: + readFloat(&(motor->PID_current_d.P)); + break; + case REG_CURD_PID_I: + readFloat(&(motor->PID_current_d.I)); + break; + case REG_CURD_PID_D: + readFloat(&(motor->PID_current_d.D)); + break; + case REG_CURD_LPF_T: + readFloat(&(motor->LPF_current_d.Tf)); + break; + + case REG_VOLTAGE_LIMIT: + readFloat(&(motor->voltage_limit)); + break; + case REG_CURRENT_LIMIT: + readFloat(&(motor->current_limit)); + break; + case REG_MOTION_DOWNSAMPLE: + readByte((uint8_t*)&(motor->motion_downsample)); + break; + case REG_DRIVER_VOLTAGE_LIMIT: + readFloat(&(((BLDCMotor*)motor)->driver->voltage_limit)); + break; + case REG_PWM_FREQUENCY: + readInt((uint32_t*)&(((BLDCMotor*)motor)->driver->pwm_frequency)); + break; + + case REG_ZERO_ELECTRIC_ANGLE: + readFloat(&(motor->zero_electric_angle)); + break; + case REG_SENSOR_DIRECTION: + readByte((uint8_t*)&(motor->sensor_direction)); + break; + case REG_ZERO_OFFSET: + readFloat(&(motor->sensor_offset)); + break; + case REG_PHASE_RESISTANCE: + readFloat(&(motor->phase_resistance)); + break; + case REG_KV: + readFloat(&(motor->KV_rating)); + break; + case REG_INDUCTANCE: + readFloat(&(motor->phase_inductance)); + break; + case REG_POLE_PAIRS: + readByte((uint8_t*)&(motor->pole_pairs)); + break; + // unknown register or read-only register (no write) or can't handle in superclass + case REG_STATUS: + case REG_ANGLE: + case REG_POSITION: + case REG_VELOCITY: + case REG_SENSOR_ANGLE: + case REG_VOLTAGE_Q: + case REG_VOLTAGE_D: + case REG_CURRENT_Q: + case REG_CURRENT_D: + case REG_CURRENT_A: + case REG_CURRENT_B: + case REG_CURRENT_C: + case REG_CURRENT_ABC: + case REG_SYS_TIME: + case REG_NUM_MOTORS: + case REG_MOTOR_ADDRESS: + case REG_ENABLE_ALL: + case REG_REPORT: + default: + break; + } +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.h new file mode 100644 index 0000000..fc2af1c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterReceiver.h @@ -0,0 +1,15 @@ + +#pragma once + +#include "./SimpleFOCRegisters.h" +#include "common/base_classes/FOCMotor.h" + + + +class RegisterReceiver { +protected: + virtual void setRegister(SimpleFOCRegister reg, FOCMotor* motor); + virtual uint8_t readByte(uint8_t* valueToSet) = 0; + virtual uint8_t readFloat(float* valueToSet) = 0; + virtual uint8_t readInt(uint32_t* valueToSet) = 0; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.cpp new file mode 100644 index 0000000..2a163ce --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.cpp @@ -0,0 +1,193 @@ + +#include "./RegisterSender.h" +#include "common/foc_utils.h" +#include "BLDCMotor.h" + +bool RegisterSender::sendRegister(SimpleFOCRegister reg, FOCMotor* motor){ + // write a register value for the given motor + switch(reg) { + case REG_STATUS: + writeByte(motor->motor_status); + break; + case REG_ENABLE: + writeByte(motor->enabled); + break; + case REG_MODULATION_MODE: + writeByte(motor->foc_modulation); + break; + case REG_TORQUE_MODE: + writeByte(motor->torque_controller); + break; + case REG_CONTROL_MODE: + writeByte(motor->controller); + break; + case REG_TARGET: + writeFloat(motor->target); + break; + case REG_ANGLE: + writeFloat(motor->shaft_angle); + break; + case REG_POSITION: + if (motor->sensor) { + writeInt(motor->sensor->getFullRotations()); + writeFloat(motor->sensor->getMechanicalAngle()); + } + else { + writeInt(motor->shaft_angle/_2PI); + writeFloat(fmod(motor->shaft_angle, _2PI)); + } + break; + case REG_VELOCITY: + writeFloat(motor->shaft_velocity); + break; + case REG_SENSOR_ANGLE: + if (motor->sensor) + writeFloat(motor->sensor->getAngle()); // stored angle + else + writeFloat(motor->shaft_angle); + break; + + case REG_VOLTAGE_Q: + writeFloat(motor->voltage.q); + break; + case REG_VOLTAGE_D: + writeFloat(motor->voltage.d); + break; + case REG_CURRENT_Q: + writeFloat(motor->current.q); + break; + case REG_CURRENT_D: + writeFloat(motor->current.d); + break; + case REG_CURRENT_A: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().a); + else + writeFloat(0.0f); + break; + case REG_CURRENT_B: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().b); + else + writeFloat(0.0f); + break; + case REG_CURRENT_C: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().c); + else + writeFloat(0.0f); + break; + case REG_CURRENT_ABC: + if (motor->current_sense) { + PhaseCurrent_s currents = motor->current_sense->getPhaseCurrents(); + writeFloat(currents.a); + writeFloat(currents.b); + writeFloat(currents.c); + } + else { + writeFloat(0.0f); + writeFloat(0.0f); + writeFloat(0.0f); + } + break; + case REG_VEL_PID_P: + writeFloat(motor->PID_velocity.P); + break; + case REG_VEL_PID_I: + writeFloat(motor->PID_velocity.I); + break; + case REG_VEL_PID_D: + writeFloat(motor->PID_velocity.D); + break; + case REG_VEL_LPF_T: + writeFloat(motor->LPF_velocity.Tf); + break; + case REG_ANG_PID_P: + writeFloat(motor->P_angle.P); + break; + case REG_VEL_LIMIT: + writeFloat(motor->velocity_limit); + break; + case REG_VEL_MAX_RAMP: + writeFloat(motor->PID_velocity.output_ramp); + break; + + case REG_CURQ_PID_P: + writeFloat(motor->PID_current_q.P); + break; + case REG_CURQ_PID_I: + writeFloat(motor->PID_current_q.I); + break; + case REG_CURQ_PID_D: + writeFloat(motor->PID_current_q.D); + break; + case REG_CURQ_LPF_T: + writeFloat(motor->LPF_current_q.Tf); + break; + case REG_CURD_PID_P: + writeFloat(motor->PID_current_d.P); + break; + case REG_CURD_PID_I: + writeFloat(motor->PID_current_d.I); + break; + case REG_CURD_PID_D: + writeFloat(motor->PID_current_d.D); + break; + case REG_CURD_LPF_T: + writeFloat(motor->LPF_current_d.Tf); + break; + + case REG_VOLTAGE_LIMIT: + writeFloat(motor->voltage_limit); + break; + case REG_CURRENT_LIMIT: + writeFloat(motor->current_limit); + break; + case REG_MOTION_DOWNSAMPLE: + writeByte((uint8_t)motor->motion_downsample); + break; + case REG_DRIVER_VOLTAGE_LIMIT: + writeFloat(((BLDCMotor*)motor)->driver->voltage_limit); + break; + case REG_PWM_FREQUENCY: + writeInt(((BLDCMotor*)motor)->driver->pwm_frequency); + break; + + case REG_ZERO_ELECTRIC_ANGLE: + writeFloat(motor->zero_electric_angle); + break; + case REG_SENSOR_DIRECTION: + writeByte((int8_t)motor->sensor_direction); + break; + case REG_ZERO_OFFSET: + writeFloat(motor->sensor_offset); + break; + case REG_PHASE_RESISTANCE: + writeFloat(motor->phase_resistance); + break; + case REG_KV: + writeFloat(motor->KV_rating); + break; + case REG_INDUCTANCE: + writeFloat(motor->phase_inductance); + break; + case REG_POLE_PAIRS: + writeByte((uint8_t)motor->pole_pairs); + break; + + case REG_SYS_TIME: + // TODO how big is millis()? Same on all platforms? + writeInt((int)millis()); + break; + // unknown register or write only register (no read) or can't handle in superclass + case REG_NUM_MOTORS: + case REG_REPORT: + case REG_MOTOR_ADDRESS: + case REG_ENABLE_ALL: + default: + writeByte(0); // TODO what to send in this case? + return false; + } + return true; +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.h new file mode 100644 index 0000000..b2486a1 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/RegisterSender.h @@ -0,0 +1,18 @@ + +#pragma once + +#include "./SimpleFOCRegisters.h" +#include "common/base_classes/FOCMotor.h" + +/** + * Register sending functionality is shared by Commander and Telemetry implementations. + * Since the code to access all the registers is quite large, it makes sense to abstract it out, + * and also make sure registers are addressed in the same way for all implementations. + */ +class RegisterSender { +protected: + virtual bool sendRegister(SimpleFOCRegister reg, FOCMotor* motor); + virtual uint8_t writeByte(uint8_t value) = 0; + virtual uint8_t writeFloat(float value) = 0; + virtual uint8_t writeInt(uint32_t value) = 0; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h new file mode 100644 index 0000000..6fea825 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h @@ -0,0 +1,72 @@ + +#pragma once + +#include + + +// this constant is changed each time the registers definition are changed *in an incompatible way*. This means that just adding new registers +// does not change the version, but removing or changing the meaning of existing registers does, or changing the number of an existing register. +#define SIMPLEFOC_REGISTERS_VERSION 0x01 + + + +typedef enum : uint8_t { + REG_STATUS = 0x00, // RO - 1 byte (motor status) + REG_MOTOR_ADDRESS = 0x01, // R/W - 1 byte + REG_REPORT = 0x02, // R/W - Write: variable, Read: variable, up to 32 bytes + REG_ENABLE_ALL = 0x03, // WO - 1 byte + REG_ENABLE = 0x04, // R/W - 1 byte + REG_CONTROL_MODE = 0x05, // R/W - 1 byte + REG_TORQUE_MODE = 0x06, // R/W - 1 byte + REG_MODULATION_MODE = 0x07, // R/W - 1 byte + + REG_TARGET = 0x08, // R/W - float + REG_ANGLE = 0x09, // RO - float + REG_POSITION = 0x10, // RO - int32_t full rotations + float position (0-2PI, in radians) (4 bytes + 4 bytes) + REG_VELOCITY = 0x11, // RO - float + REG_SENSOR_ANGLE = 0x12, // RO - float + + REG_VOLTAGE_Q = 0x20, // RO - float + REG_VOLTAGE_D = 0x21, // RO - float + REG_CURRENT_Q = 0x22, // RO - float + REG_CURRENT_D = 0x23, // RO - float + REG_CURRENT_A = 0x24, // RO - float + REG_CURRENT_B = 0x25, // RO - float + REG_CURRENT_C = 0x26, // RO - float + REG_CURRENT_ABC = 0x27, // RO - 3xfloat = 12 bytes + REG_CURRENT_DC = 0x28, // RO - float + + REG_VEL_PID_P = 0x30, // R/W - float + REG_VEL_PID_I = 0x31, // R/W - float + REG_VEL_PID_D = 0x32, // R/W - float + REG_VEL_LPF_T = 0x33, // R/W - float + REG_ANG_PID_P = 0x34, // R/W - float + REG_VEL_LIMIT = 0x35, // R/W - float + REG_VEL_MAX_RAMP = 0x36, // R/W - float + + REG_CURQ_PID_P = 0x40, // R/W - float + REG_CURQ_PID_I = 0x41, // R/W - float + REG_CURQ_PID_D = 0x42, // R/W - float + REG_CURQ_LPF_T = 0x43, // R/W - float + REG_CURD_PID_P = 0x44, // R/W - float + REG_CURD_PID_I = 0x45, // R/W - float + REG_CURD_PID_D = 0x46, // R/W - float + REG_CURD_LPF_T = 0x47, // R/W - float + + REG_VOLTAGE_LIMIT = 0x50, // R/W - float + REG_CURRENT_LIMIT = 0x51, // R/W - float + REG_MOTION_DOWNSAMPLE = 0x52, // R/W - uint32_t + REG_DRIVER_VOLTAGE_LIMIT = 0x53,// R/W - float + REG_PWM_FREQUENCY = 0x54, // R/W - uint32_t + + REG_ZERO_ELECTRIC_ANGLE = 0x60, // RO - float + REG_SENSOR_DIRECTION = 0x61, // RO - 1 byte + REG_ZERO_OFFSET = 0x62, // R/W - float + REG_POLE_PAIRS = 0x63, // RO - uint32_t + REG_PHASE_RESISTANCE = 0x64, // R/W - float + REG_KV = 0x65, // R/W - float + REG_INDUCTANCE = 0x66, // R/W - float + + REG_NUM_MOTORS = 0x70, // RO - 1 byte + REG_SYS_TIME = 0x71, // RO - uint32_t +} SimpleFOCRegister; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp new file mode 100644 index 0000000..737b2ee --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp @@ -0,0 +1,424 @@ + +#include "I2CCommander.h" + +I2CCommander::I2CCommander(TwoWire* wire) : _wire(wire) { + +}; + +I2CCommander::~I2CCommander(){}; + +void I2CCommander::init(uint8_t address) { + _address = address; +}; + + + +void I2CCommander::addMotor(FOCMotor* motor){ + if (numMotorsavailable()>=numBytes){ + byte* bytes = (byte*)valueToSet; + for (int i=0;iread(); + return true; + } + commanderror = true; + return false; +}; + + + +void I2CCommander::writeFloat(float value){ + _wire->write((byte*)&value, 4); +}; + + + + +void I2CCommander::onReceive(int numBytes){ + lastcommanderror = commanderror; + lastcommandregister = curRegister; + commanderror = false; + if (numBytes>=1) { // set the current register + curRegister = static_cast(_wire->read()); + } + if (numBytes>1) { // read from i2c and update value represented by register as well... + if (!receiveRegister(curMotor, curRegister, numBytes)) + commanderror = true; + } + if (numBytes<1) + commanderror = true; +}; + + + +void I2CCommander::onRequest(){ + commanderror = false; + if (!sendRegister(curMotor, curRegister)) + commanderror = true; +}; + + + + +/* +Reads values from I2C bus and updates the motor's values. + +Currently this isn't really thread-safe, but works ok in practice on 32-bit MCUs. + +Do not use on 8-bit architectures where the 32 bit writes may not be atomic! + +Plan to make this safe: the writes should be buffered, and not actually executed +until in the main loop by calling commander->run(); +the run() method disables interrupts while the updates happen. +*/ +bool I2CCommander::receiveRegister(uint8_t motorNum, uint8_t registerNum, int numBytes) { + int val; + switch (registerNum) { + case REG_MOTOR_ADDRESS: + val = _wire->read(); // reading one more byte is definately ok, since numBytes>1 + if (val>=0 && val=3 && (numBytes&0x01)==1) { // numBytes must be odd, since we have register and n pairs of motor/register numbers + val = (numBytes-1)/2; + if (val>I2CCOMMANDER_MAX_REPORT_REGISTERS) + val = I2CCOMMANDER_MAX_REPORT_REGISTERS; + for (int i=0;iread(); + reportRegisters[i] = _wire->read(); + } + } + else + commanderror = true; + break; + case REG_ENABLE_ALL: + val = _wire->read(); + for (int i=0; i0)?motors[i]->enable():motors[i]->disable(); + break; + case REG_ENABLE: + val = _wire->read(); + (val>0)?motors[motorNum]->enable():motors[motorNum]->disable(); + break; + case REG_CONTROL_MODE: + val = _wire->read(); + if (val>=0 && val<=4) // TODO these enums don't have assigned constants + motors[motorNum]->controller = static_cast(val); + else + commanderror = true; + break; + case REG_TORQUE_MODE: + val = _wire->read(); + if (val>=0 && val<=2) + motors[motorNum]->torque_controller = static_cast(val); + else + commanderror = true; + break; + case REG_MODULATION_MODE: + val = _wire->read(); + if (val>=0 && val<=3) + motors[motorNum]->foc_modulation = static_cast(val); + else + commanderror = true; + break; + case REG_TARGET: + readBytes(&(motors[motorNum]->target), 4); + break; + case REG_VEL_PID_P: + readBytes(&(motors[motorNum]->PID_velocity.P), 4); + break; + case REG_VEL_PID_I: + readBytes(&(motors[motorNum]->PID_velocity.I), 4); + break; + case REG_VEL_PID_D: + readBytes(&(motors[motorNum]->PID_velocity.D), 4); + break; + case REG_VEL_LPF_T: + readBytes(&(motors[motorNum]->LPF_velocity.Tf), 4); + break; + case REG_ANG_PID_P: + readBytes(&(motors[motorNum]->P_angle.P), 4); + break; + case REG_VEL_LIMIT: + readBytes(&(motors[motorNum]->velocity_limit), 4); + break; + case REG_VEL_MAX_RAMP: + readBytes(&(motors[motorNum]->PID_velocity.output_ramp), 4); + break; + case REG_CURQ_PID_P: + readBytes(&(motors[motorNum]->PID_current_q.P), 4); + break; + case REG_CURQ_PID_I: + readBytes(&(motors[motorNum]->PID_current_q.I), 4); + break; + case REG_CURQ_PID_D: + readBytes(&(motors[motorNum]->PID_current_q.D), 4); + break; + case REG_CURQ_LPF_T: + readBytes(&(motors[motorNum]->LPF_current_q.Tf), 4); + break; + case REG_CURD_PID_P: + readBytes(&(motors[motorNum]->PID_current_d.P), 4); + break; + case REG_CURD_PID_I: + readBytes(&(motors[motorNum]->PID_current_d.I), 4); + break; + case REG_CURD_PID_D: + readBytes(&(motors[motorNum]->PID_current_d.D), 4); + break; + case REG_CURD_LPF_T: + readBytes(&(motors[motorNum]->LPF_current_d.Tf), 4); + break; + case REG_VOLTAGE_LIMIT: + readBytes(&(motors[motorNum]->voltage_limit), 4); + break; + case REG_CURRENT_LIMIT: + readBytes(&(motors[motorNum]->current_limit), 4); + break; + case REG_MOTION_DOWNSAMPLE: + readBytes(&(motors[motorNum]->motion_downsample), 4); + break; + case REG_ZERO_OFFSET: + readBytes(&(motors[motorNum]->sensor_offset), 4); + break; + // RO registers + case REG_STATUS: + case REG_ANGLE: + case REG_POSITION: + case REG_VELOCITY: + case REG_SENSOR_ANGLE: + case REG_VOLTAGE_Q: + case REG_VOLTAGE_D: + case REG_CURRENT_Q: + case REG_CURRENT_D: + case REG_CURRENT_A: + case REG_CURRENT_B: + case REG_CURRENT_C: + case REG_CURRENT_ABC: + case REG_ZERO_ELECTRIC_ANGLE: + case REG_SENSOR_DIRECTION: + case REG_POLE_PAIRS: + case REG_PHASE_RESISTANCE: + case REG_NUM_MOTORS: + case REG_SYS_TIME: + default: // unknown register + return false; + } + return true; +} + + + + + +/* + Reads values from motor/sensor and writes them to I2C bus. Intended to be run + from the Wire.onRequest interrupt. + + Assumes atomic 32 bit reads. On 8-bit arduino this assumption does not hold and this + code is not safe on those platforms. You might read "half-written" floats. + + A solution might be to maintain a complete set of shadow registers in the commander + class, and update them in the run() method (which runs with interrupts off). Not sure + of the performance impact of all those 32 bit read/writes though. In any case, since + I use only 32 bit MCUs I'll leave it as an excercise to the one who needs it. ;-) + + On 32 bit platforms the implication is that reads will occur atomically, so data will + be intact, but they can occur at any time during motor updates, so different values might + not be in a fully consistent state (i.e. phase A current might be from the current iteration + but phase B current from the previous iteration). +*/ +bool I2CCommander::sendRegister(uint8_t motorNum, uint8_t registerNum) { + // read the current register + switch(registerNum) { + case REG_STATUS: + _wire->write(curMotor); + _wire->write((uint8_t)lastcommandregister); + _wire->write((uint8_t)lastcommanderror+1); + for (int i=0;(iwrite(motors[i]->motor_status); + } + break; + case REG_MOTOR_ADDRESS: + _wire->write(curMotor); + break; + case REG_REPORT: + for (int i=0;iwrite(motors[motorNum]->enabled); + break; + case REG_MODULATION_MODE: + _wire->write(motors[motorNum]->foc_modulation); + break; + case REG_TORQUE_MODE: + _wire->write(motors[motorNum]->torque_controller); + break; + case REG_CONTROL_MODE: + _wire->write(motors[motorNum]->controller); + break; + + case REG_TARGET: + writeFloat(motors[motorNum]->target); + break; + case REG_ANGLE: + writeFloat(motors[motorNum]->shaft_angle); + break; + case REG_VELOCITY: + writeFloat(motors[motorNum]->shaft_velocity); + break; + case REG_SENSOR_ANGLE: + if (motors[motorNum]->sensor) + writeFloat(motors[motorNum]->sensor->getAngle()); // stored angle + else + writeFloat(motors[motorNum]->shaft_angle); + break; + + case REG_VOLTAGE_Q: + writeFloat(motors[motorNum]->voltage.q); + break; + case REG_VOLTAGE_D: + writeFloat(motors[motorNum]->voltage.d); + break; + case REG_CURRENT_Q: + writeFloat(motors[motorNum]->current.q); + break; + case REG_CURRENT_D: + writeFloat(motors[motorNum]->current.d); + break; + case REG_CURRENT_A: + if (motors[motorNum]->current_sense) // TODO check if current sense can be called from inside this callback function + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().a); + else + writeFloat(0.0f); + break; + case REG_CURRENT_B: + if (motors[motorNum]->current_sense) + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().b); + else + writeFloat(0.0f); + break; + case REG_CURRENT_C: + if (motors[motorNum]->current_sense) + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().c); + else + writeFloat(0.0f); + break; + case REG_CURRENT_ABC: + if (motors[motorNum]->current_sense) { + PhaseCurrent_s currents = motors[motorNum]->current_sense->getPhaseCurrents(); + writeFloat(currents.a); + writeFloat(currents.b); + writeFloat(currents.c); + } + else { + writeFloat(0.0f); + writeFloat(0.0f); + writeFloat(0.0f); + } + break; + case REG_VEL_PID_P: + writeFloat(motors[motorNum]->PID_velocity.P); + break; + case REG_VEL_PID_I: + writeFloat(motors[motorNum]->PID_velocity.I); + break; + case REG_VEL_PID_D: + writeFloat(motors[motorNum]->PID_velocity.D); + break; + case REG_VEL_LPF_T: + writeFloat(motors[motorNum]->LPF_velocity.Tf); + break; + case REG_ANG_PID_P: + writeFloat(motors[motorNum]->P_angle.P); + break; + case REG_VEL_LIMIT: + writeFloat(motors[motorNum]->velocity_limit); + break; + case REG_VEL_MAX_RAMP: + writeFloat(motors[motorNum]->PID_velocity.output_ramp); + break; + + case REG_CURQ_PID_P: + writeFloat(motors[motorNum]->PID_current_q.P); + break; + case REG_CURQ_PID_I: + writeFloat(motors[motorNum]->PID_current_q.I); + break; + case REG_CURQ_PID_D: + writeFloat(motors[motorNum]->PID_current_q.D); + break; + case REG_CURQ_LPF_T: + writeFloat(motors[motorNum]->LPF_current_q.Tf); + break; + case REG_CURD_PID_P: + writeFloat(motors[motorNum]->PID_current_d.P); + break; + case REG_CURD_PID_I: + writeFloat(motors[motorNum]->PID_current_d.I); + break; + case REG_CURD_PID_D: + writeFloat(motors[motorNum]->PID_current_d.D); + break; + case REG_CURD_LPF_T: + writeFloat(motors[motorNum]->LPF_current_d.Tf); + break; + + case REG_VOLTAGE_LIMIT: + writeFloat(motors[motorNum]->voltage_limit); + break; + case REG_CURRENT_LIMIT: + writeFloat(motors[motorNum]->current_limit); + break; + case REG_MOTION_DOWNSAMPLE: + _wire->write((int)motors[motorNum]->motion_downsample); // TODO int will have different sizes on different platforms + // but using uint32 doesn't compile clean on all, e.g. RP2040 + break; + + case REG_ZERO_ELECTRIC_ANGLE: + writeFloat(motors[motorNum]->zero_electric_angle); + break; + case REG_SENSOR_DIRECTION: + _wire->write((int8_t)motors[motorNum]->sensor_direction); + break; + case REG_ZERO_OFFSET: + writeFloat(motors[motorNum]->sensor_offset); + break; + case REG_PHASE_RESISTANCE: + writeFloat(motors[motorNum]->phase_resistance); + break; + case REG_POLE_PAIRS: + _wire->write((int)motors[motorNum]->pole_pairs); + break; + + case REG_SYS_TIME: + // TODO how big is millis()? Same on all platforms? + _wire->write((int)millis()); + break; + case REG_NUM_MOTORS: + _wire->write(numMotors); + break; + + // unknown register or write only register (no read) + case REG_ENABLE_ALL: + default: + _wire->write(0); // TODO what to send in this case? + return false; + } + return true; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h new file mode 100644 index 0000000..65cefde --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h @@ -0,0 +1,49 @@ + +#ifndef SIMPLEFOC_I2CCOMMANDER_H +#define SIMPLEFOC_I2CCOMMANDER_H + +#include "Arduino.h" +#include "Wire.h" +#include "common/base_classes/FOCMotor.h" +#include "../SimpleFOCRegisters.h" + +#ifndef I2CCOMMANDER_MAX_MOTORS_COMMANDABLE +#define I2CCOMMANDER_MAX_MOTORS_COMMANDABLE 4 +#endif + +#define I2CCOMMANDER_MIN_VELOCITY_FOR_MOTOR_MOVING 0.1f // in rad/s +#define I2CCOMMANDER_MAX_REPORT_REGISTERS 8 + + +class I2CCommander { + public: + I2CCommander(TwoWire* wire = &Wire); + ~I2CCommander(); + + void addMotor(FOCMotor* motor); // first add motors + virtual void init(uint8_t address); // then init + + void onReceive(int numBytes); + void onRequest(); + + protected: + void writeFloat(float value); + bool readBytes(void* valueToSet, uint8_t numBytes); + virtual bool sendRegister(uint8_t motorNum, uint8_t registerNum); + virtual bool receiveRegister(uint8_t motorNum, uint8_t registerNum, int numBytes); + + uint8_t _address; + TwoWire* _wire; + uint8_t numMotors = 0; + uint8_t curMotor = 0; + SimpleFOCRegister curRegister = REG_STATUS; + bool commanderror = false; + bool lastcommanderror = false; + uint8_t lastcommandregister = REG_STATUS; + FOCMotor* motors[I2CCOMMANDER_MAX_MOTORS_COMMANDABLE]; + uint8_t numReportRegisters = 0; + uint8_t reportMotors[I2CCOMMANDER_MAX_REPORT_REGISTERS]; + uint8_t reportRegisters[I2CCOMMANDER_MAX_REPORT_REGISTERS]; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp new file mode 100644 index 0000000..666e009 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp @@ -0,0 +1,59 @@ + +#include "I2CCommanderMaster.h" + + +I2CCommanderMaster::I2CCommanderMaster(int maxMotors) : maxMotors(maxMotors), motors(new I2CRemoteMotor[maxMotors]) { +}; + + + +I2CCommanderMaster::~I2CCommanderMaster(){ +}; + + + +void I2CCommanderMaster::init(){ +}; + + +// TODO handle multiple motors per target +void I2CCommanderMaster::addI2CMotors(uint8_t i2cAddress, uint8_t motorCount, TwoWire *wire){ + for (int i=0;ibeginTransmission(motors[motor].address); + motors[motor].wire->write((uint8_t)registerNum); + motors[motor].wire->write((uint8_t*)data, size); + motors[motor].wire->endTransmission(); + return size; +}; + + +int I2CCommanderMaster::readRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size){ + motors[motor].wire->beginTransmission(motors[motor].address); + int numWrite = motors[motor].wire->write((uint8_t)registerNum); // TODO check return value + motors[motor].wire->endTransmission(); + if (numWrite==1) + return readLastUsedRegister(motor, data, size); + return 0; +}; + + +int I2CCommanderMaster::readLastUsedRegister(int motor, void* data, uint8_t size){ + int numRead = motors[motor].wire->requestFrom(motors[motor].address, size); + if (numRead==size) + motors[motor].wire->readBytes((uint8_t*)data, size); + else { + return 0; + } + return numRead; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h new file mode 100644 index 0000000..9b52ae7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h @@ -0,0 +1,42 @@ +#ifndef SIMPLEFOC_I2CCOMMANDER_H +#define SIMPLEFOC_I2CCOMMANDER_H + +#include +#include + +#include "../SimpleFOCRegisters.h" + +#define I2COMMANDER_DEFAULT_MAX_REMOTE_MOTORS 4 + + +typedef struct { + TwoWire* wire; + uint8_t address; +} I2CRemoteMotor; + + +class I2CCommanderMaster { + + public: + I2CCommanderMaster(int maxMotors = I2COMMANDER_DEFAULT_MAX_REMOTE_MOTORS); + ~I2CCommanderMaster(); + void init(); + void addI2CMotors(uint8_t i2cAddress, uint8_t motorCount, TwoWire *wire = &Wire); + + int writeRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size); + int readRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size); + int readLastUsedRegister(int motor, void* data, uint8_t size); + + // Motor intialization interface for convenience - think about how this will best work + // i.e. which parameters should be set by i2c and which should be hard-coded, and where config info is saved + // TODO bool initializeMotor(int motor); + + private: + int maxMotors; + int numMotors = 0; + I2CRemoteMotor* motors; + +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/README.md new file mode 100644 index 0000000..eac9e34 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/i2c/README.md @@ -0,0 +1,98 @@ + +# I2CCommander + +Implementation of SimpleFOC Commander for I2C communication bus. + +This code takes the point of view that the motor driver (the "muscle") is the I2C target device, and another MCU/CPU (the "brain") is the I2C controller device, which is coordinating one or more motor drivers on the same or different I2C buses. Each motor driver can offer one or more motors for control via the I2CCommander. So fairly flexible setups are possible, with multiple motors per driver, multiple drivers per I2C bus and multiple I2C buses on the brain MCU. + +## Warning + +This is new code, and has not been extensively tested. Your milage may vary. That said, basic use cases have been tested, and we would certainly appreciate feedback and help with testing it out. + +In particular, there are concurrency issues with reading/writing the SimpleFOC motor values from I2C while the motor is running. These should be solved soon in an upcoming version. + +**Do not run on 8-bit MCUs!** The code currently assumes atomic 32 bit reads, so running on Arduino UNO or Nano is unfortunately a no-go. + +## Using + +As would be expected for I2C, each target device needs a unique I2C address on its bus, and setting up and discovering these addresses is out-of-scope for I2CCommander. Setting up and configuring the TwoWire objects (which pins, speed, etc...) is also out of scope and finished, initialized TwoWire objects must be passed to I2CCommander. If you don't specify a different reference, the standard *Wire* object is assumed. + +Communication with the motor drivers happens via a register paradigm. The driver board offers many registers, some of which can be read, some can be written, and some are read/write. The controller MCU sends I2C messages to the target device to read or write a register as desired. The size of the data to be read/written depends on the register, and must be known by the controller. See Registers, below, for more details on the individual registers. + +Since each target motor driver can handle multiple motors, one of the registers contains the currently selected motor. Most of the other registers then operate on the currently selected motor. There are some exceptions, like REG_ENABLE_ALL - which operates on all the motors, or REG_STATUS, which returns stati for all the motors. + +### Target device (motor driver) + +The target device (motor driver) initializes and uses an instance of I2CCommander. Only one instance is needed for all attached motors: + +```c++ +#include "Arduino.h" +#include +#include +#include "SimpleFOCDrivers.h" +#include "comms/i2c/I2CCommander.h" + +// commander instance +uint8_t i2c_addr = 0x60; // can be anything you choose +I2CCommander commander; +// interrupt callbacks +void onReceive(int numBytes) { commander.onReceive(numBytes); } +void onRequest() { commander.onRequest(); } + + +// ... other variables, like sensor, etc... +BLDCMotor motor = BLDCMotor(POLE_PAIRS); + + +void setup() { + + // ...other setup code + + Wire.setClock(400000); // use same speed on controller device + Wire.begin(i2c_addr, true); // initialize i2c in target mode + commander.addMotor(&motor); // add a motor + //commander.addMotor(&motor2); // you could add more than one motor + commander.init(i2c_addr); // initialize commander + Wire.onReceive(onReceive); // connect the interrupt handlers + Wire.onRequest(onRequest); + +} +``` + +### Controller device ("brain" MCU) + +On the controller device, you use an instance of I2CCommanderMaster, which you initialize by adding one or more target devices to it: + +```c++ +#include "Arduino.h" +#include +#include +#include "SimpleFOCDrivers.h" +#include "comms/i2c/I2CCommanderMaster.h" + +#define TARGET_I2C_ADDRESS 0x60 +I2CCommanderMaster commander; + +void setup() { + + // ...other setup code + + Wire.setClock(400000); // use same speed on target device! + Wire.begin(); // initialize i2c in controller mode + commander.addI2CMotors(TARGET_I2C_ADDRESS, 1); // add target device, it has 1 motor + //commander.addI2CMotors(TARGET_I2C_ADDRESS2, 1); // you could add another target device on the same bus + //commander.addI2CMotors(TARGET_I2C_ADDRESS, 1, &wire2); // or on a different i2c bus + commander.init(); // init commander + Wire.onReceive(onReceive); // connect the interrupt handlers + Wire.onRequest(onRequest); + +} + +``` + + +## Extending + +The API is somewhat opinionated, and unlike the standard serial commander, currently does not support hooking in your own variables for reading/writing to them via I2C. This is because I2C is a bit less flexible than Serial. + +If you want to extend I2CCommander please subclass it and override the functions `sendRegister` and `receiveRegister` to add new registers. Use register numbers above 0x80 to prevent collisions with the standard registers. \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/README.md new file mode 100644 index 0000000..2e1b733 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/README.md @@ -0,0 +1,49 @@ + +# Serial communications classes + +Serial communications classes for register-based control and telemetry from SimpleFOC. + +## SerialASCIITelemetry + +:warning: unfinished, untested + +Telemetry class that sends telemetry as ASCII on a serial port. Similar to the classic "monitoring" functionality of SimpleFOC, but allows you to configure telemetry based on most of the defined registers. + +Usage: + +```c++ + +SerialASCIITelemetry telemetry = SerialASCIITelemetry(); // number of float digits to display + +void setup() { + ... + telemetry.addMotor(&motor); + telemetry.setTelemetryRegisters(2, [REG_VELOCITY, REG_VOLTAGE_Q]); + telemetry.init(); + ... +} + +void loop() { + motor.move(); + motor.loopFOC(); + telemetry.run(); +} +``` + +Some options are supported: + +```c++ + telemetry.floatPrecision = 4; // send floats with 4 decimal places + telemetry.min_elapsed_time = 0; // microseconds between sending telemetry + telemetry.downsample = 100; // send every this many loop iterations +``` + + + +## SerialBinaryCommander + +:warning: unfinished, untested! + +Control SimpleFOC via a binary protocol over the serial port. The standard SimpleFOC registers are used. + +TODO document the protocol \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp new file mode 100644 index 0000000..084b940 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp @@ -0,0 +1,65 @@ + +#include "./SerialASCIITelemetry.h" + +SerialASCIITelemetry::SerialASCIITelemetry(int floatPrecision) : Telemetry() { + this->floatPrecision = floatPrecision; +}; + +SerialASCIITelemetry::~SerialASCIITelemetry(){ + +}; + +void SerialASCIITelemetry::init(Print* print){ + this->_print = print; + this->Telemetry::init(); +}; + + + +void SerialASCIITelemetry::sendHeader() { + if (numRegisters > 0) { + writeChar('H'); + writeChar(' '); + for (uint8_t i = 0; i < numRegisters; i++) { + writeByte(registers_motor[i]); + writeChar(':'); + writeByte(registers[i]); + if (i < numRegisters-1) + writeChar(' '); + }; + writeChar('\n'); + }; +}; + + + +void SerialASCIITelemetry::sendTelemetry(){ + if (numRegisters > 0) { + for (uint8_t i = 0; i < numRegisters; i++) { + sendRegister(static_cast(registers[i]), motors[registers_motor[i]]); + if (i < numRegisters-1) + writeChar(' '); + }; + writeChar('\n'); + } +}; + + + +uint8_t SerialASCIITelemetry::writeChar(char value){ + return _print->print(value); +}; + +uint8_t SerialASCIITelemetry::writeByte(uint8_t value){ + return _print->print(value); +}; + + +uint8_t SerialASCIITelemetry::writeFloat(float value){ + return _print->print(value, floatPrecision); +}; + + +uint8_t SerialASCIITelemetry::writeInt(uint32_t value){ + return _print->print(value); +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h new file mode 100644 index 0000000..e218f3b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h @@ -0,0 +1,27 @@ + +#pragma once + +#include "Arduino.h" +#include "../telemetry/Telemetry.h" + +class SerialASCIITelemetry : public Telemetry { +public: + SerialASCIITelemetry(int floatPrecision = 2); + virtual ~SerialASCIITelemetry(); + + void init(Print* print = &Serial); + +protected: + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + uint8_t writeChar(char value); + + void sendTelemetry() override; + void sendHeader() override; + + Print* _print; + int floatPrecision = 2; +}; + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp new file mode 100644 index 0000000..ffa2406 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp @@ -0,0 +1,153 @@ + +#include "SerialBinaryCommander.h" + + + +SerialBinaryCommander::SerialBinaryCommander(bool echo) : Telemetry(), echo(echo) { + +}; + + + +SerialBinaryCommander::~SerialBinaryCommander(){ + +}; + + + +void SerialBinaryCommander::init(Stream &serial) { + _serial = &serial; + this->Telemetry::init(); +}; + + + +void SerialBinaryCommander::run() { + if (_serial->available()>2) { // TODO make this work with partial packets... + uint8_t command = _serial->read(); + uint8_t registerNum = _serial->read(); + uint8_t motorNum = _serial->read(); + if (command==SERIALBINARY_COMMAND_READ){ + startFrame(registerNum, SERIALBINARY_FRAMETYPE_REGISTER); // TODO call is incorrect + sendRegister(static_cast(registerNum), motors[motorNum]); + endFrame(); + } + else if (command==SERIALBINARY_COMMAND_WRITE) { + setRegister(static_cast(registerNum), motors[motorNum]); + if (echo) { + startFrame(registerNum, SERIALBINARY_FRAMETYPE_REGISTER); // TODO call is incorrect + sendRegister(static_cast(registerNum), motors[motorNum]); + endFrame(); + } + } + } + // and handle the telemetry + this->Telemetry::run(); +}; + + + +uint8_t SerialBinaryCommander::readBytes(void* valueToSet, uint8_t numBytes){ + if (_serial->available()read(); + } + return numBytes; +}; + + + + +uint8_t SerialBinaryCommander::writeBytes(void* valueToSend, uint8_t numBytes){ + uint8_t* value = (uint8_t*)valueToSend; + for (uint8_t i=0; iwrite(value[i]); + } + return numBytes; +}; + + + + + + +void SerialBinaryCommander::startFrame(uint8_t frameSize, uint8_t type){ + _serial->write(0xA5); + _serial->write((uint8_t)((type<<6)|frameSize)); +}; + + + + + +void SerialBinaryCommander::endFrame(){ + _serial->write(0x5A); +}; + + + + +uint8_t SerialBinaryCommander::writeByte(uint8_t value){ + return writeBytes(&value, 1); +}; + + + +uint8_t SerialBinaryCommander::writeFloat(float value){ + return writeBytes(&value, 4); +}; + + + +uint8_t SerialBinaryCommander::writeInt(uint32_t value){ + return writeBytes(&value, 4); +}; + + + + +uint8_t SerialBinaryCommander::readByte(uint8_t* valueToSet){ + return readBytes(valueToSet, 1); +}; + + + +uint8_t SerialBinaryCommander::readFloat(float* valueToSet){ + return readBytes(valueToSet, 4); +}; + + + +uint8_t SerialBinaryCommander::readInt(uint32_t* valueToSet){ + return readBytes(valueToSet, 4); +}; + + + + + + +void SerialBinaryCommander::sendHeader() { + if (numRegisters > 0) { + startFrame(numRegisters*2, TELEMETRY_FRAMETYPE_HEADER); + for (uint8_t i = 0; i < numRegisters; i++) { + writeByte(registers[i]); + writeByte(registers_motor[i]); + }; + endFrame(); + }; +}; + + + +void SerialBinaryCommander::sendTelemetry(){ + if (numRegisters > 0) { + startFrame(frameSize, TELEMETRY_FRAMETYPE_DATA); + for (uint8_t i = 0; i < numRegisters; i++) { + sendRegister(static_cast(registers[i]), motors[registers_motor[i]]); + }; + endFrame(); + } +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h new file mode 100644 index 0000000..f80ccc4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h @@ -0,0 +1,57 @@ + +#pragma once + +#include +#include "../RegisterReceiver.h" +#include "../telemetry/Telemetry.h" + + +#ifndef COMMS_MAX_REPORT_REGISTERS +#define COMMS_MAX_REPORT_REGISTERS 7 +#endif + + +#define SERIALBINARY_FRAMETYPE_REGISTER 0x03 +#define SERIALBINARY_COMMAND_READ 0x00 +#define SERIALBINARY_COMMAND_WRITE 0x80 + + +class SerialBinaryCommander : public Telemetry, public RegisterReceiver { +public: + SerialBinaryCommander(bool echo = false); + virtual ~SerialBinaryCommander(); + + void init(Stream &serial = Serial); + void run(); + + + bool echo; +protected: + virtual void startFrame(uint8_t frameSize, uint8_t type = TELEMETRY_FRAMETYPE_DATA); + virtual void endFrame(); + uint8_t readBytes(void* valueToSet, uint8_t numBytes); + uint8_t writeBytes(void* valueToSend, uint8_t numBytes); + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + void sendTelemetry() override; + void sendHeader() override; + + uint8_t curMotor = 0; + SimpleFOCRegister curRegister = REG_STATUS; + bool commanderror = false; + bool lastcommanderror = false; + uint8_t lastcommandregister = REG_STATUS; + + uint8_t numReportRegisters = 0; + uint8_t reportMotors[COMMS_MAX_REPORT_REGISTERS]; + SimpleFOCRegister reportRegisters[COMMS_MAX_REPORT_REGISTERS]; + + Stream* _serial; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/README.md new file mode 100644 index 0000000..1e36a5a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/README.md @@ -0,0 +1,58 @@ + +# STM32SpeedDirInput + +Control SimpleFOC using PWM speed and direction inputs. + +Based on STM32 timer PWM-Input capabilities, which means this can only be used on STM32 MCUs. It can cover a wide range of PWM frequencies, and runs without MCU overhead in the timer hardware. + + + +## Setup + +The PWM speed input should be connected to either channel 1 or channel 2 of a general purpose or advanced control timer on your STM32 MCU. Suitable timers are: +- Advanced control timers: TIM1, TIM8 +- General purpose timers (32 bit): TIM2, TIM5 +- General purpose timers (16 bit): TIM3, TIM4, TIM9, TIM12 +If in doubt, check in STM32CubeIDE and see if the PWM-Input mode can be enabled (under "Combined Channels") for the timer. + +The optional direction input can be connected to any GPIO pin. By default a high level direction input is associated with a positive velocity value, while a low level direction input results in a negative velocity value. To reverse this, set the option `dir_positive_high = false` + +The direction input is optional - if not provided, you can control the direction from software using the `direction` field. + +The velocity values returned are in the range `min_speed` to `max_speed`, while the input PWM duty cycle should lie within the range `min_pwm` to `max_pwm`. Actual input values smaller than `min_pwm` will be treated as `min_pwm`, values larger than `max_pwm` will be treated as `max_pwm`. The behaviour for 100% or 0% duty cycles is undefined, and they should be avoided. + +## Usage + +Use it like this: + +```c++ +#include "comms/stm32speeddir/STM32SpeedDirInput.h" + +// some example pins - the speed pin has to be on channel 1 or 2 of a timer +#define PIN_SPEED PC6 +#define PIN_DIRECTION PB8 + +STM32SpeedDirInput speed_dir = STM32SpeedDirInput(PIN_SPEED, PIN_DIRECTION); + +float target = 0.0f; + +void setup(){ + ... + + speed_dir.min_speed = 10.0f; // 10rad/s min speed + speed_dir.max_speed = 100.0f; // 100rad/s max speed + speed_dir.min_pwm = 5.0f; // 5% min duty cycle + speed_dir.max_pwm = 95.0f; // 95% max duty cycle + speed_dir.init(); + + ... +} + + +void loop(){ + target = speed_dir.getTargetVelocity(); + motor.move(target); + motor.loopFOC(); +} + +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp new file mode 100644 index 0000000..4bc8bee --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp @@ -0,0 +1,29 @@ + +#include "./STM32SpeedDirInput.h" + +#if defined(_STM32_DEF_) + +STM32SpeedDirInput::STM32SpeedDirInput(int pin_speed, int pin_dir) : STM32PWMInput(pin_speed) { + _pin_speed = pin_speed; + _pin_dir = pin_dir; +}; + +STM32SpeedDirInput::~STM32SpeedDirInput(){}; + +int STM32SpeedDirInput::init(){ + pinMode(_pin_dir, INPUT); + return STM32PWMInput::initialize(); +}; + + +float STM32SpeedDirInput::getTargetVelocity(){ + if (_pin_dir != NOT_SET) + direction = digitalRead(_pin_dir); + float speed = getDutyCyclePercent(); + speed = constrain(speed, min_pwm, max_pwm); + speed = (speed - min_pwm)/(max_pwm - min_pwm) * (max_speed - min_speed) + min_speed; + return (direction == dir_positive_high) ? speed : -speed; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h new file mode 100644 index 0000000..3a3647b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h @@ -0,0 +1,31 @@ + +#pragma once + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + +#include "common/foc_utils.h" +#include "utilities/stm32pwm/STM32PWMInput.h" + +class STM32SpeedDirInput : public STM32PWMInput { + public: + STM32SpeedDirInput(int pin_speed, int pin_dir = NOT_SET); + ~STM32SpeedDirInput(); + + int init(); + float getTargetVelocity(); + + float min_speed = 0; // min speed in rad/s + float max_speed = 100; // max speed in rad/s + float min_pwm = 5.0; // min duty cycle in % + float max_pwm = 95.0; // max duty cycle in % + bool dir_positive_high = true; // true if the direction pin is high when the motor is spinning in the positive direction + bool direction = true; // direction of rotation, default positive + protected: + int _pin_speed; + int _pin_dir; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/README.md new file mode 100644 index 0000000..384098f --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/README.md @@ -0,0 +1,42 @@ + +# SimpleFOC Telemetry + +:warning: unfinished, untested + +A flexible abstraction for telemetry (monitoring) of SimpleFOC systems. + +The telemetry implementation is based on the SimpleFOC registers, and allows you to send telemetry for any (readable) register. Telemetry supports multiple motors. + +The concept allows you to choose registers which are then sent by the telemetry automatically, on a regular schedule. + +The method of sending depends on the type of telemetry you add to your program. There are telemetry drivers for: + +- Serial ASCII telemetry +- Serial Binary telemetry +- and more drivers will be added in the future + +Multiple motors can be added to the same telemetry, to monitor several motors at the same time. The registers reported by telemetry can be changed at run-time. Multiple instances of telemetry can be used to monitor different sets of values at different time intervals, or to send to multiple channels at the same time. + +## Usage + +Using telemetry is simple: + +```c++ + +SerialASCIITelemetry telemetry = SerialASCIITelemetry(); +... + +void setup() { + ... + telemetry.addMotor(&motor); + telemetry.setTelemetryRegisters(3, { REG_TARGET, REG_ANGLE, REG_VELOCITY }); + telemetry.init(); + ... +} + +void loop() { + motor.move(); + motor.loopFOC(); + telemetry.run(); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp new file mode 100644 index 0000000..6a6c446 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp @@ -0,0 +1,67 @@ + + +#include "./Telemetry.h" + + + +Telemetry::Telemetry() { + this->numRegisters = 0; +}; + + + +Telemetry::~Telemetry(){ + +}; + + + + + +void Telemetry::setTelemetryRegisters(uint8_t numRegisters, uint8_t* registers, uint8_t* motors){ + if (numRegisters<=TELEMETRY_MAX_REGISTERS) { + this->numRegisters = numRegisters; + for (uint8_t i=0; iregisters[i] = registers[i]; + if (motors!=NULL) + this->registers_motor[i] = motors[i]; + else + this->registers_motor[i] = 0; + } + } +}; + + + +void Telemetry::init() { + headerSent = false; +}; + + + +void Telemetry::run() { + if (numRegisters<1) + return; + if (!headerSent) { + sendHeader(); + headerSent = true; + } + if (downsampleCnt++ < downsample) return; + downsampleCnt = 0; + if (min_elapsed_time > 0) { + long now = _micros(); + if (now - last_run_time < min_elapsed_time) return; + last_run_time = now; + } + sendTelemetry(); +} + + + +void Telemetry::addMotor(FOCMotor* motor) { + if (numMotors < TELEMETRY_MAX_MOTORS) { + motors[numMotors] = motor; + numMotors++; + } +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h new file mode 100644 index 0000000..256036a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h @@ -0,0 +1,53 @@ + +#pragma once + +#include "../SimpleFOCRegisters.h" +#include "../RegisterSender.h" + +#ifndef TELEMETRY_MAX_REGISTERS +#define TELEMETRY_MAX_REGISTERS 8 +#endif + +#ifndef TELEMETRY_MAX_MOTORS +#define TELEMETRY_MAX_MOTORS 4 +#endif + + +#define DEF_TELEMETRY_DOWNSAMPLE 100 + + +typedef enum : uint8_t { + TELEMETRY_FRAMETYPE_DATA = 0x01, + TELEMETRY_FRAMETYPE_HEADER = 0x02 +} TelemetryFrameType; + + + + +class Telemetry : public RegisterSender { +public: + Telemetry(); + virtual ~Telemetry(); + virtual void init(); + void addMotor(FOCMotor* motor); + void setTelemetryRegisters(uint8_t numRegisters, uint8_t* registers, uint8_t* motors = NULL); + void run(); + + uint16_t downsample = DEF_TELEMETRY_DOWNSAMPLE; + uint32_t min_elapsed_time = 0; +protected: + virtual void sendTelemetry() = 0; + virtual void sendHeader() = 0; + + FOCMotor* motors[TELEMETRY_MAX_MOTORS]; + uint8_t numMotors = 0; + + uint8_t numRegisters; + uint8_t registers[TELEMETRY_MAX_REGISTERS]; + uint8_t registers_motor[TELEMETRY_MAX_REGISTERS]; + uint8_t frameSize; + bool headerSent; + long last_run_time = 0; + uint16_t downsampleCnt = 0; +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/README.md new file mode 100644 index 0000000..8d15d0c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/README.md @@ -0,0 +1,175 @@ + +# DRV8316 SimpleFOC driver + +The DRV8316 is a integrated FET, integrated current sensing 3-phase BLDC driver IC from Texas Instruments, including all protections and many cool configuration optons. See https://www.ti.com/product/DRV8316 for more information. + +This driver includes a DRV8316 SPI driver, and specific subclasses for SimpleFOCs BLDCDriver3PWM and BLDCDriver6PWM generic drivers. The code is designed to be "Ardunio compatible" and should work with any of the hardware architectures supported by SimpleFOC. + +## Hardware setup + +To use the DRV8316 you will have to connect the following: + +- GND +- SPI MOSI +- SPI MISO +- SPI CLK +- SPI nCS +- INH_U - connect to motor PWM pin +- INH_V - connect to motor PWM pin +- INH_W - connect to motor PWM pin +- INL_U - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- INL_V - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- INL_W - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation + +Optionally, but probably useful: + +- nFault - digital in, active low +- DRVOFF - digital out + +For current sensting: + +- VREF - either connect to DAC output of the MCU or a fixed voltage reference. +- ISENA - connect to analog in +- ISENB - connect to analog in +- ISENC - connect to analog in + +For current limiting: + +- ILIM (same as VREF) - connect it to a DAC output of the MCU if you want to control the current limit from the MCU + +## Usage + +Usage is quite easy, especially if you already know SimpleFOC. See also the [examples](https://github.com/simplefoc/Arduino-FOC-drivers/examples/drivers/drv8316/) + +```c++ +#include "Arduino.h" +#include +#include +#include +#include "SimpleFOCDrivers.h" +#include "drivers/drv8316/drv8316.h" + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver6PWM driver = DRV8316Driver6PWM(A3,0,A4,1,2,6,7,false); // MKR1010 6-PWM + +//... normal simpleFOC init code... +``` + +Or, for 3-PWM: + +```c++ +#include "Arduino.h" +#include +#include +#include +#include "SimpleFOCDrivers.h" +#include "drivers/drv8316/drv8316.h" + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver3PWM driver = DRV8316Driver3PWM(A3,A4,2,7,false); // MKR1010 3-PWM +// these are examples, for 3-PWM you could use any output pins as the enable pins. +#define ENABLE_A 0 +#define ENABLE_B 1 +#define ENABLE_C 6 + +void setup() { + + + pinMode(ENABLE_A, OUTPUT); + digitalWrite(ENABLE_A, 1); // enable + pinMode(ENABLE_B, OUTPUT); + digitalWrite(ENABLE_B, 1); // enable + pinMode(ENABLE_C, OUTPUT); + digitalWrite(ENABLE_C, 1); // enable + + +//... normal simpleFOC init code... + + +} + +``` + +You can use the driver's features. In general you can do this at any time, but certain features only make sense at setup-time (e.g. setting the PWM mode, which is handled automatically by the DRV8316Driver3PWM class, or setting the current limit, which you generally want to get done before applying power to the motor). + +Driver usage, for example reading and printing the complete status: + +```c++ + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +``` + + +Setting options can be conveniently done via the provided setter methods. All documented registers and options are available via the driver, and the option values can be accessed via enums. + +Leave at least 400ns delay between reading and/or writing options to ensure you don't talk to the DRV8316 too quickly: + +```c++ +driver.setPWM100Frequency(DRV8316_PWM100DUTY::FREQ_40KHz); +delayMicroseconds(1); // ensure 400ns delay +driver.setBuckVoltage(DRV8316_BuckVoltage::VB_5V7); +``` + + +### Current sensing + +TODO... + +### Current limiting + +TODO... + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp new file mode 100644 index 0000000..9586faa --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp @@ -0,0 +1,586 @@ + + +#include "./drv8316.h" + + +void DRV8316Driver3PWM::init(SPIClass* _spi) { + DRV8316Driver::init(_spi); + setRegistersLocked(false); + delayMicroseconds(1); + DRV8316Driver::setPWMMode(DRV8316_PWMMode::PWM3_Mode); + BLDCDriver3PWM::init(); +}; + + +void DRV8316Driver6PWM::init(SPIClass* _spi) { + DRV8316Driver::init(_spi); + setRegistersLocked(false); + delayMicroseconds(1); + DRV8316Driver::setPWMMode(DRV8316_PWMMode::PWM6_Mode); // default mode is 6-PWM + BLDCDriver6PWM::init(); +}; + + + + + + +/* + * SPI setup: + * + * capture on falling, propagate on rising = + * MSB first + * + * 16 bit words + * outgoing: R/W:1 addr:6 parity:1 data:8 + * incoming: status:8 data:8 + * + * on reads, incoming data is content of register being read + * on writes, incomnig data is content of register being written + * + * + */ + +void handleInterrupt() { + +} + +void DRV8316Driver::init(SPIClass* _spi) { + // TODO make SPI speed configurable + spi = _spi; + settings = SPISettings(1000000, MSBFIRST, SPI_MODE1); + + //setup pins + pinMode(cs, OUTPUT); + digitalWrite(cs, HIGH); // switch off + + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + + if (_isset(nFault)) { + pinMode(nFault, INPUT); + // TODO add interrupt handler on the nFault pin if configured + // add configuration for how to handle faults... idea: interrupt handler calls a callback, depending on the type of fault + // consider what would be a useful configuration in practice? What do we want to do on a fault, e.g. over-temperature for example? + + //attachInterrupt(digitalPinToInterrupt(nFault), handleInterrupt, PinStatus::FALLING); + } +}; + + + + +bool DRV8316Driver::getParity(uint16_t data) { + //PARITY = XNOR(CMD, A5..A0, D7..D0) + uint8_t par = 0; + for (int i=0;i<16;i++) { + if (((data)>>i) & 0x0001) + par+=1; + } + return (par&0x01)==0x01; // even number of bits means true +} + + + + +uint16_t DRV8316Driver::readSPI(uint8_t addr) { + digitalWrite(cs, 0); + spi->beginTransaction(settings); + uint16_t data = (((addr<<1) | 0x80)<<8)|0x0000; + if (getParity(data)) + data |= 0x0100; + uint16_t result = spi->transfer16(data); + spi->endTransaction(); + digitalWrite(cs, 1); +// Serial.print("SPI Read Result: "); +// Serial.print(data, HEX); +// Serial.print(" -> "); +// Serial.println(result, HEX); + return result; +} + + +uint16_t DRV8316Driver::writeSPI(uint8_t addr, uint8_t value) { + digitalWrite(cs, 0); + spi->beginTransaction(settings); + uint16_t data = ((addr<<1)<<8)|value; + if (getParity(data)) + data |= 0x0100; + uint16_t result = spi->transfer16(data); + spi->endTransaction(); + digitalWrite(cs, 1); +// Serial.print("SPI Write Result: "); +// Serial.print(data, HEX); +// Serial.print(" -> "); +// Serial.println(result, HEX); + return result; +} + + + +DRV8316Status DRV8316Driver::getStatus() { + IC_Status data; + Status__1 data1; + Status__2 data2; + uint16_t result = readSPI(IC_Status_ADDR); + data.reg = (result & 0x00FF); + delayMicroseconds(1); // delay at least 400ns between operations + result = readSPI(Status__1_ADDR); + data1.reg = (result & 0x00FF); + delayMicroseconds(1); // delay at least 400ns between operations + result = readSPI(Status__2_ADDR); + data2.reg = (result & 0x00FF); + return DRV8316Status(data, data1, data2); +} + + + + + + + + +void DRV8316Driver::clearFault() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.CLR_FLT |= 1; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + + + + + + +bool DRV8316Driver::isRegistersLocked(){ + uint16_t result = readSPI(Control__1_ADDR); + Control__1 data; + data.reg = (result & 0x00FF); + return data.REG_LOCK==REG_LOCK_LOCK; +} +void DRV8316Driver::setRegistersLocked(bool lock){ + uint16_t result = readSPI(Control__1_ADDR); + Control__1 data; + data.reg = (result & 0x00FF); + data.REG_LOCK = lock?REG_LOCK_LOCK:REG_LOCK_UNLOCK; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__1_ADDR, data.reg); +} + + + +DRV8316_PWMMode DRV8316Driver::getPWMMode() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_PWMMode)data.PWM_MODE; +}; +void DRV8316Driver::setPWMMode(DRV8316_PWMMode pwmMode){ + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.PWM_MODE = pwmMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +DRV8316_Slew DRV8316Driver::getSlew() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_Slew)data.SLEW; +}; +void DRV8316Driver::setSlew(DRV8316_Slew slewRate) { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.SLEW = slewRate; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +DRV8316_SDOMode DRV8316Driver::getSDOMode() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_SDOMode)data.SDO_MODE; +}; +void DRV8316Driver::setSDOMode(DRV8316_SDOMode sdoMode) { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.SDO_MODE = sdoMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isOvertemperatureReporting(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.OTW_REP==OTW_REP_ENABLE; +}; +void DRV8316Driver::setOvertemperatureReporting(bool reportFault){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OTW_REP = reportFault?OTW_REP_ENABLE:OTW_REP_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isSPIFaultReporting(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.SPI_FLT_REP==SPI_FLT_REP_ENABLE; +} +void DRV8316Driver::setSPIFaultReporting(bool reportFault){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.SPI_FLT_REP = reportFault?SPI_FLT_REP_ENABLE:SPI_FLT_REP_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +} + + + +bool DRV8316Driver::isOvervoltageProtection(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.OVP_EN==OVP_EN_ENABLE; +}; +void DRV8316Driver::setOvervoltageProtection(bool enabled){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OVP_EN = enabled?OVP_EN_ENABLE:OVP_EN_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_OVP DRV8316Driver::getOvervoltageLevel(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return (DRV8316_OVP)data.OVP_SEL; +}; +void DRV8316Driver::setOvervoltageLevel(DRV8316_OVP voltage){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OVP_SEL = voltage; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_PWM100DUTY DRV8316Driver::getPWM100Frequency(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return (DRV8316_PWM100DUTY)data.PWM_100_DUTY_SEL; +}; +void DRV8316Driver::setPWM100Frequency(DRV8316_PWM100DUTY freq){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.PWM_100_DUTY_SEL = freq; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_OCPMode DRV8316Driver::getOCPMode(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPMode)data.OCP_MODE; + +}; +void DRV8316Driver::setOCPMode(DRV8316_OCPMode ocpMode){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_MODE = ocpMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPLevel DRV8316Driver::getOCPLevel(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPLevel)data.OCP_LVL; +}; +void DRV8316Driver::setOCPLevel(DRV8316_OCPLevel amps){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_LVL = amps; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPRetry DRV8316Driver::getOCPRetryTime(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPRetry)data.OCP_RETRY; +}; +void DRV8316Driver::setOCPRetryTime(DRV8316_OCPRetry ms){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_RETRY = ms; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPDeglitch DRV8316Driver::getOCPDeglitchTime(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPDeglitch)data.OCP_DEG; +}; +void DRV8316Driver::setOCPDeglitchTime(DRV8316_OCPDeglitch ms){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_DEG = ms; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isOCPClearInPWMCycleChange(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return data.OCP_CBC==OCP_CBC_ENABLE; +}; +void DRV8316Driver::setOCPClearInPWMCycleChange(bool enable){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_CBC = enable?OCP_CBC_ENABLE:OCP_CBC_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isDriverOffEnabled(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return data.DRV_OFF==DRV_OFF_ENABLE; +}; +void DRV8316Driver::setDriverOffEnabled(bool enabled){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.DRV_OFF = enabled?DRV_OFF_ENABLE:DRV_OFF_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_CSAGain DRV8316Driver::getCurrentSenseGain(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return (DRV8316_CSAGain)data.CSA_GAIN; +}; +void DRV8316Driver::setCurrentSenseGain(DRV8316_CSAGain gain){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.CSA_GAIN = gain; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isActiveSynchronousRectificationEnabled(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return data.EN_ASR==EN_ASR_ENABLE; + +}; +void DRV8316Driver::setActiveSynchronousRectificationEnabled(bool enabled){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.EN_ASR = enabled?EN_ASR_ENABLE:EN_ASR_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isActiveAsynchronousRectificationEnabled(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return data.EN_AAR==EN_AAR_ENABLE; +}; +void DRV8316Driver::setActiveAsynchronousRectificationEnabled(bool enabled){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.EN_AAR = enabled?EN_AAR_ENABLE:EN_AAR_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +DRV8316_Recirculation DRV8316Driver::getRecirculationMode(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return (DRV8316_Recirculation)data.ILIM_RECIR; +}; +void DRV8316Driver::setRecirculationMode(DRV8316_Recirculation recirculationMode){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.ILIM_RECIR = recirculationMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isBuckEnabled(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return data.BUCK_DIS==BUCK_DIS_BUCK_ENABLE; +}; +void DRV8316Driver::setBuckEnabled(bool enabled){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_DIS = enabled?BUCK_DIS_BUCK_ENABLE:BUCK_DIS_BUCK_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_BuckVoltage DRV8316Driver::getBuckVoltage(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return (DRV8316_BuckVoltage)data.BUCK_SEL; +}; +void DRV8316Driver::setBuckVoltage(DRV8316_BuckVoltage volts){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_SEL = volts; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_BuckCurrentLimit DRV8316Driver::getBuckCurrentLimit(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return (DRV8316_BuckCurrentLimit)data.BUCK_CL; +}; +void DRV8316Driver::setBuckCurrentLimit(DRV8316_BuckCurrentLimit mamps){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_CL = mamps; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isBuckPowerSequencingEnabled(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return data.BUCK_PS_DIS==BUCK_PS_DIS_ENABLE; + +}; +void DRV8316Driver::setBuckPowerSequencingEnabled(bool enabled){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_PS_DIS = enabled?BUCK_PS_DIS_ENABLE:BUCK_PS_DIS_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_DelayTarget DRV8316Driver::getDelayTarget(){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + return (DRV8316_DelayTarget)data.DLY_TARGET; +}; +void DRV8316Driver::setDelayTarget(DRV8316_DelayTarget us){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + data.DLY_TARGET = us; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__10_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isDelayCompensationEnabled(){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + return data.DLYCMP_EN==DLYCMP_EN_ENABLE; +}; +void DRV8316Driver::setDelayCompensationEnabled(bool enabled){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + data.DLYCMP_EN = enabled?DLYCMP_EN_ENABLE:DLYCMP_EN_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__10_ADDR, data.reg); +}; + + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h new file mode 100644 index 0000000..dfec32e --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h @@ -0,0 +1,316 @@ + + +#ifndef SIMPLEFOC_DRV8316 +#define SIMPLEFOC_DRV8316 + + +#include "Arduino.h" +#include +#include +#include + +#include "./drv8316_registers.h" + + +enum DRV8316_PWMMode { + PWM6_Mode = 0b00, + PWM6_CurrentLimit_Mode = 0b01, + PWM3_Mode = 0b10, + PWM3_CurrentLimit_Mode = 0b11 +}; + + +enum DRV8316_SDOMode { + SDOMode_OpenDrain = 0b0, + SDOMode_PushPull = 0b1 +}; + + +enum DRV8316_Slew { + Slew_25Vus = 0b00, + Slew_50Vus = 0b01, + Slew_150Vus = 0b10, + Slew_200Vus = 0b11 +}; + + +enum DRV8316_OVP { + OVP_SEL_32V = 0b0, + OVP_SEL_20V = 0b1 +}; + + +enum DRV8316_PWM100DUTY { + FREQ_20KHz = 0b0, + FREQ_40KHz = 0b1 +}; + + +enum DRV8316_OCPMode { + Latched_Fault = 0b00, + AutoRetry_Fault = 0b01, + ReportOnly = 0b10, + NoAction = 0b11 +}; + + +enum DRV8316_OCPLevel { + Curr_16A = 0b0, + Curr_24A = 0b1 +}; + + +enum DRV8316_OCPRetry { + Retry5ms = 0b0, + Retry500ms = 0b1 +}; + + +enum DRV8316_OCPDeglitch { + Deglitch_0us2 = 0b00, + Deglitch_0us6 = 0b01, + Deglitch_1us1 = 0b10, + Deglitch_1us6 = 0b11 +}; + + +enum DRV8316_CSAGain { + Gain_0V15 = 0b00, + Gain_0V1875 = 0b01, + Gain_0V25 = 0b10, + Gain_0V375 = 0b11 +}; + + +enum DRV8316_Recirculation { + BrakeMode = 0b00, // FETs + CoastMode = 0b01 // Diodes +}; + + +enum DRV8316_BuckVoltage { + VB_3V3 = 0b00, + VB_5V = 0b01, + VB_4V = 0b10, + VB_5V7 = 0b11 +}; + + +enum DRV8316_BuckCurrentLimit { + Limit_600mA = 0b00, + Limit_150mA = 0b01 +}; + + +enum DRV8316_DelayTarget { + Delay_0us = 0x0, + Delay_0us4 = 0x1, + Delay_0us6 = 0x2, + Delay_0us8 = 0x3, + Delay_1us = 0x4, + Delay_1us2 = 0x5, + Delay_1us4 = 0x6, + Delay_1us6 = 0x7, + Delay_1us8 = 0x8, + Delay_2us = 0x9, + Delay_2us2 = 0xA, + Delay_2us4 = 0xB, + Delay_2us6 = 0xC, + Delay_2us8 = 0xD, + Delay_3us = 0xE, + Delay_3us2 = 0xF +}; + + + +class DRV8316ICStatus { +public: + DRV8316ICStatus(IC_Status status) : status(status) {}; + ~DRV8316ICStatus() {}; + + bool isFault() { return status.FAULT==0b1; }; + bool isOverTemperature() { return status.OT==0b1; }; + bool isOverCurrent() { return status.OCP==0b1; }; + bool isOverVoltage() { return status.OVP==0b1; }; + bool isSPIError() { return status.SPI_FLT==0b1; }; + bool isBuckError() { return status.BK_FLT==0b1; }; + bool isPowerOnReset() { return status.NPOR==0b1; }; + + IC_Status status; +}; + + +class DRV8316Status1 { +public: + DRV8316Status1(Status__1 status1) : status1(status1) {}; + ~DRV8316Status1() {}; + + + bool isOverCurrent_Ah() { return status1.OCP_HA==0b1; }; + bool isOverCurrent_Al() { return status1.OCP_LA==0b1; }; + bool isOverCurrent_Bh() { return status1.OCP_HB==0b1; }; + bool isOverCurrent_Bl() { return status1.OCP_LB==0b1; }; + bool isOverCurrent_Ch() { return status1.OCP_HC==0b1; }; + bool isOverCurrent_Cl() { return status1.OCP_LC==0b1; }; + bool isOverTemperatureShutdown() { return status1.OTS==0b1; }; + bool isOverTemperatureWarning() { return status1.OTW==0b1; }; + + Status__1 status1; +}; + + +class DRV8316Status2 { +public: + DRV8316Status2(Status__2 status2) : status2(status2) {}; + ~DRV8316Status2() {}; + + + bool isOneTimeProgrammingError() { return status2.OTP_ERR==0b1; }; + bool isBuckOverCurrent() { return status2.BUCK_OCP==0b1; }; + bool isBuckUnderVoltage() { return status2.BUCK_UV==0b1; }; + bool isChargePumpUnderVoltage() { return status2.VCP_UV==0b1; }; + bool isSPIParityError() { return status2.SPI_PARITY==0b1; }; + bool isSPIClockFramingError() { return status2.SPI_SCLK_FLT==0b1; }; + bool isSPIAddressError() { return status2.SPI_ADDR_FLT==0b1; }; + + Status__2 status2; +}; + + + +class DRV8316Status : public DRV8316ICStatus, public DRV8316Status1, public DRV8316Status2 { + public: + DRV8316Status(IC_Status status, Status__1 status1, Status__2 status2) : DRV8316ICStatus(status), DRV8316Status1(status1), DRV8316Status2(status2) {}; + ~DRV8316Status() {}; +}; + + + + +class DRV8316Driver { + + public: + DRV8316Driver(int cs, bool currentLimit = false, int nFault = NOT_SET) : currentLimit(currentLimit), cs(cs), nFault(nFault), spi(&SPI), settings(1000000, MSBFIRST, SPI_MODE1) {}; + virtual ~DRV8316Driver() {}; + + virtual void init(SPIClass* _spi = &SPI); + + void clearFault(); // TODO check for fault condition methods + + DRV8316Status getStatus(); + + bool isRegistersLocked(); + void setRegistersLocked(bool lock); + + DRV8316_PWMMode getPWMMode(); + void setPWMMode(DRV8316_PWMMode pwmMode); + + DRV8316_Slew getSlew(); + void setSlew(DRV8316_Slew slewRate); + + DRV8316_SDOMode getSDOMode(); + void setSDOMode(DRV8316_SDOMode sdoMode); + + bool isOvertemperatureReporting(); + void setOvertemperatureReporting(bool reportFault); + + bool isSPIFaultReporting(); + void setSPIFaultReporting(bool reportFault); + + bool isOvervoltageProtection(); + void setOvervoltageProtection(bool enabled); + + DRV8316_OVP getOvervoltageLevel(); + void setOvervoltageLevel(DRV8316_OVP voltage); + + DRV8316_PWM100DUTY getPWM100Frequency(); + void setPWM100Frequency(DRV8316_PWM100DUTY freq); + + DRV8316_OCPMode getOCPMode(); + void setOCPMode(DRV8316_OCPMode ocpMode); + + DRV8316_OCPLevel getOCPLevel(); + void setOCPLevel(DRV8316_OCPLevel amps); + + DRV8316_OCPRetry getOCPRetryTime(); + void setOCPRetryTime(DRV8316_OCPRetry ms); + + DRV8316_OCPDeglitch getOCPDeglitchTime(); + void setOCPDeglitchTime(DRV8316_OCPDeglitch ms); + + bool isOCPClearInPWMCycleChange(); + void setOCPClearInPWMCycleChange(bool enable); + + bool isDriverOffEnabled(); + void setDriverOffEnabled(bool enabled); + + DRV8316_CSAGain getCurrentSenseGain(); + void setCurrentSenseGain(DRV8316_CSAGain gain); + + bool isActiveSynchronousRectificationEnabled(); + void setActiveSynchronousRectificationEnabled(bool enabled); + + bool isActiveAsynchronousRectificationEnabled(); + void setActiveAsynchronousRectificationEnabled(bool enabled); + + DRV8316_Recirculation getRecirculationMode(); + void setRecirculationMode(DRV8316_Recirculation recirculationMode); + + bool isBuckEnabled(); + void setBuckEnabled(bool enabled); + + DRV8316_BuckVoltage getBuckVoltage(); + void setBuckVoltage(DRV8316_BuckVoltage volts); + + DRV8316_BuckCurrentLimit getBuckCurrentLimit(); + void setBuckCurrentLimit(DRV8316_BuckCurrentLimit mamps); + + bool isBuckPowerSequencingEnabled(); + void setBuckPowerSequencingEnabled(bool enabled); + + DRV8316_DelayTarget getDelayTarget(); + void setDelayTarget(DRV8316_DelayTarget us); + + bool isDelayCompensationEnabled(); + void setDelayCompensationEnabled(bool enabled); + + private: + uint16_t readSPI(uint8_t addr); + uint16_t writeSPI(uint8_t addr, uint8_t data); + bool getParity(uint16_t data); + + bool currentLimit; + int cs; + int nFault; + SPIClass* spi; + SPISettings settings; +}; + + + +class DRV8316Driver3PWM : public DRV8316Driver, public BLDCDriver3PWM { + + public: + DRV8316Driver3PWM(int phA,int phB,int phC, int cs, bool currentLimit = false, int en = NOT_SET, int nFault = NOT_SET) : + DRV8316Driver(cs, currentLimit, nFault), BLDCDriver3PWM(phA, phB, phC, en) { enable_active_high=false; }; + virtual ~DRV8316Driver3PWM() {}; + + virtual void init(SPIClass* _spi = &SPI) override; + +}; + + + +class DRV8316Driver6PWM : public DRV8316Driver, public BLDCDriver6PWM { + + public: + DRV8316Driver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int cs, bool currentLimit = false, int en = NOT_SET, int nFault = NOT_SET) : + DRV8316Driver(cs, currentLimit, nFault), BLDCDriver6PWM(phA_h, phA_l, phB_h, phB_l, phC_h, phC_l, en) { enable_active_high=false; }; + virtual ~DRV8316Driver6PWM() {}; + + virtual void init(SPIClass* _spi = &SPI) override; + +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h new file mode 100644 index 0000000..04bca1e --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h @@ -0,0 +1,184 @@ + + +#ifndef SIMPLEFOC_DRV8316_REGISTERS +#define SIMPLEFOC_DRV8316_REGISTERS + + +#define IC_Status_ADDR 0x0 +#define Status__1_ADDR 0x1 +#define Status__2_ADDR 0x2 +#define Control__1_ADDR 0x3 +#define Control__2_ADDR 0x4 +#define Control__3_ADDR 0x5 +#define Control__4_ADDR 0x6 +#define Control__5_ADDR 0x7 +#define Control__6_ADDR 0x8 +#define Control__10_ADDR 0xC + +#define REG_LOCK_LOCK 0b110 +#define REG_LOCK_UNLOCK 0b011 +#define CLR_FAULT_CLR 0b1 +#define OTW_REP_ENABLE 0b1 +#define OTW_REP_DISABLE 0b0 +#define SPI_FLT_REP_ENABLE 0b0 +#define SPI_FLT_REP_DISABLE 0b1 +#define OVP_EN_ENABLE 0b1 +#define OVP_EN_DISABLE 0b0 +#define OCP_CBC_ENABLE 0b1 +#define OCP_CBC_DISABLE 0b0 +#define DRV_OFF_ENABLE 0b1 +#define DRV_OFF_DISABLE 0b0 +#define EN_ASR_ENABLE 0b1 +#define EN_ASR_DISABLE 0b0 +#define EN_AAR_ENABLE 0b1 +#define EN_AAR_DISABLE 0b0 +#define BUCK_DIS_BUCK_DISABLE 0b1 +#define BUCK_DIS_BUCK_ENABLE 0b0 +#define BUCK_PS_DIS_DISABLE 0b1 +#define BUCK_PS_DIS_ENABLE 0b0 +#define DLYCMP_EN_ENABLE 0b1 +#define DLYCMP_EN_DISABLE 0b0 + + + + +typedef union { + struct { + uint8_t REG_LOCK:3; + uint8_t :5; + }; + uint8_t reg; +} Control__1; + + +typedef union { + struct { + uint8_t CLR_FLT:1; + uint8_t PWM_MODE:2; + uint8_t SLEW:2; + uint8_t SDO_MODE:1; + uint8_t :2; + }; + uint8_t reg; +} Control__2; + + +typedef union { + struct { + uint8_t OTW_REP:1; + uint8_t SPI_FLT_REP:1; + uint8_t OVP_EN:1; + uint8_t OVP_SEL:1; + uint8_t PWM_100_DUTY_SEL:1; + uint8_t :3; + }; + uint8_t reg; +} Control__3; + + +typedef union { + struct { + uint8_t OCP_MODE:2; + uint8_t OCP_LVL:1; + uint8_t OCP_RETRY:1; + uint8_t OCP_DEG:2; + uint8_t OCP_CBC:1; + uint8_t DRV_OFF:1; + }; + uint8_t reg; +} Control__4; + + + + +typedef union { + struct { + uint8_t CSA_GAIN:2; + uint8_t EN_ASR:1; + uint8_t EN_AAR:1; + uint8_t :2; + uint8_t ILIM_RECIR:1; + uint8_t :1; + }; + uint8_t reg; +} Control__5; + + + + +typedef union { + struct { + uint8_t BUCK_DIS:1; + uint8_t BUCK_SEL:2; + uint8_t BUCK_CL:1; + uint8_t BUCK_PS_DIS:1; + uint8_t :2; + }; + uint8_t reg; +} Control__6; + + + + +typedef union { + struct { + uint8_t DLY_TARGET:4; + uint8_t DLYCMP_EN:1; + uint8_t :3; + }; + uint8_t reg; +} Control__10; + + + + +typedef union { + struct { + uint8_t FAULT:1; + uint8_t OT:1; + uint8_t OVP:1; + uint8_t NPOR:1; + uint8_t OCP:1; + uint8_t SPI_FLT:1; + uint8_t BK_FLT:1; + uint8_t :1; + }; + uint8_t reg; +} IC_Status; + + + + +typedef union { + struct { + uint8_t OCP_LA:1; + uint8_t OCP_HA:1; + uint8_t OCP_LB:1; + uint8_t OCP_HB:1; + uint8_t OCP_LC:1; + uint8_t OCP_HC:1; + uint8_t OTS:1; + uint8_t OTW:1; + }; + uint8_t reg; +} Status__1; + + + + +typedef union { + struct { + uint8_t SPI_ADDR_FLT:1; + uint8_t SPI_SCLK_FLT:1; + uint8_t SPI_PARITY:1; + uint8_t VCP_UV:1; + uint8_t BUCK_UV:1; + uint8_t BUCK_OCP:1; + uint8_t OTP_ERR:1; + uint8_t :1; + }; + uint8_t reg; +} Status__2; + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/README.md new file mode 100644 index 0000000..b34ba7a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/README.md @@ -0,0 +1,147 @@ + +# TMC6200 SimpleFOC Driver + +by [@YaseenTwati](https://github.com/YaseenTwati) + +The TMC6200 is a Universal high voltage BLDC/PMSM/Servo MOSFET 3-halfbridge gate-driver with in line motor current +sensing. External MOSFETs for up to 100A motor current. + +See https://www.trinamic.com/fileadmin/assets/Products/ICs_Documents/TMC6200_datasheet_Rev1.05.pdf for more information. + +## Hardware setup + +To use the TMC6200 you will have to connect the following: + +- GND +- VCC_IO - 3.3V or 5V +- SPE - pull up to VCC to enable SPI mode +- SPI MOSI ( SDI ) +- SPI MISO ( SDO ) +- SPI CLK +- SPI nCS +- DRV_EN - connect to digital out (or pull up to VCC) +- UH - connect to motor PWM pin +- VH - connect to motor PWM pin +- WH - connect to motor PWM pin +- UL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- VL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- WL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation + +Optionally, but useful: + +- FAULT - digital in, active high + +For current sensing: + +- CURU - connect to analog in +- CURV - connect to analog in +- CURW - connect to analog in + +## Usage + +Usage is quite easy, especially if you already know SimpleFOC. See also the [examples](https://github.com/simplefoc/Arduino-FOC-drivers/examples/drivers/drv8316/) + +```c++ +#include "Arduino.h" +#include +#include "SimpleFOCDrivers.h" +#include "drivers/tmc6200/TMC6200.hpp" + +BLDCMotor motor = BLDCMotor(15); +TMC6200Driver6PWM driver = DRV8316Driver6PWM(UH, UL, VH, VL, WH, WL, nCS, DRV_EN); + +//... normal simpleFOC init code... +``` + +Or, for 3-PWM: + +```c++ +#include "Arduino.h" +#include +#include "SimpleFOCDrivers.h" +#include "drivers/tmc6200/TMC6200.hpp" + +BLDCMotor motor = BLDCMotor(15); +TMC6200Driver3PWM driver = TMC6200Driver3PWM(UH, MOTOR_VH, MOTOR_WH, nCS, DRV_EN); + +void setup() { + + pinMode(UL, OUTPUT); + pinMode(VL, OUTPUT); + pinMode(WL, OUTPUT); + + digitalWrite(WL, HIGH); + digitalWrite(UL, HIGH); + digitalWrite(VL, HIGH); + + //... normal simpleFOC init code... +} + +``` + +### Validating the SPI Connection +You can validate the SPI connection by checking the value of VERSION field in IOIN register. The value should be 0x10. + +```c++ + if(driver.getInputs().VERSION != TMC6200_VERSION){ + // something is wrong with the spi connection + } +``` + +### Current Sensing +The gain of the internal current amplifiers can be set to 5, 10 or 20 through `setCurrentSenseGain()` + +```c++ + driver.setCurrentSenseGain(TMC6200_AmplificationGain::_5); + //driver.setCurrentSenseGain(TMC6200_AmplificationGain::_10); + //driver.setCurrentSenseGain(TMC6200_AmplificationGain::_20); +``` +The sense amplifiers can also be turned off ( they are on by default ), through `setCurrentSenseAmplifierState()` + +```c++ + driver.setCurrentSenseAmplifierState(false); +``` +### Driver Strength +The strength of the mosfet drivers can be controlled through `setDriverStrength()` + +```c++ + driver.setDriverStrength(TMC6200_DRVStrength::Weak); + //driver.setDriverStrength(TMC6200_DRVStrength::WeakTC); // (medium above OTPW level) + //driver.setDriverStrength(TMC6200_DRVStrength::Medium); + //driver.setDriverStrength(TMC6200_DRVStrength::Strong); +``` + +### Handling Faults +The fault line will go high if a fault occurs such as a short, an interrupt can be used to handle it. +Note that some faults will disable the driver and will require the DRV_EN to be cycled to clear the fault. + +```c++ + // somewhere in setup + attachInterrupt(digitalPinToInterrupt(FAULT), handleFault, RISING); +``` + +```c++ + void handleFault() + { + // you can read the status register to see what happened + TMC6200GStatus status = driver.getStatus(); + Serial.print("hasUShorted: "); Serial.println(status.hasUShorted()); + Serial.print("hasVShorted: "); Serial.println(status.hasVShorted()); + Serial.print("hasWShorted: "); Serial.println(status.hasWShorted()); + Serial.print("isUShortedToGround: "); Serial.println(status.isUShortedToGround()); + Serial.print("isUShortedToSupply: "); Serial.println(status.isUShortedToSupply()); + Serial.print("isVShortedToGround: "); Serial.println(status.isVShortedToGround()); + Serial.print("isVShortedToSupply: "); Serial.println(status.isVShortedToSupply()); + Serial.print("isWShortedToGround: "); Serial.println(status.isWShortedToGround()); + Serial.print("isWShortedToSupply: "); Serial.println(status.isWShortedToSupply()); + Serial.print("isOverTemperaturePreWarning: "); Serial.println(status.isOverTemperaturePreWarning()); + Serial.print("isChargePumpUnderVoltage: "); Serial.println(status.isChargePumpUnderVoltage()); + + // the driver must be cycled to clear the fault + digitalWrite(DRV_EN, LOW); + delayMicrosockets(someSmallDelay); + digitalWrite(DRV_EN, HIGH); + } +``` + +The driver provides other features such as controlling the tolerences of short detection and the BBM cycle time and so on, setting the options can be conveniently done via the provided setter methods. All documented registers and options are available via the driver, and the option values can be accessed via enums. diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp new file mode 100644 index 0000000..659ab5d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp @@ -0,0 +1,221 @@ +#include "TMC6200.hpp" + +void TMC6200Driver::init(SPIClass *_spi) { + spi = _spi; + settings = SPISettings(500000, MSBFIRST, SPI_MODE3); + + pinMode(csPin, OUTPUT); + digitalWrite(csPin, HIGH); +} + +// TMC6200_GCONF ------ + +void TMC6200Driver::setDriverState(bool state) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.DISABLE = state ? 0 : 1; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setPWMMode(TMC6200_PWMMode pwmMode) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.SINGLE_LINE = pwmMode; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setFaultDirect(TMC6200_FaultDirect faultDirect) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.FAULT_DIRECT = faultDirect; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setCurrentSenseAmplifierState(bool state) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.AMPLIFIER_OFF = state ? 0 : 1; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setCurrentSenseGain(TMC6200_AmplificationGain amplificationGain) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.AMPLIFICATION = amplificationGain; + writeRegister(TMC6200_GCONF_REG, gConf.reg); + + gConf.reg = readRegister(TMC6200_GCONF_REG); +} + +// TMC6200_DRV_CONF ------ + +void TMC6200Driver::setOverTemperatureThreshold(TMC6200_OTSelect otLevel) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.OT_SELECT = otLevel; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +void TMC6200Driver::setDriverStrength(TMC6200_DRVStrength strength) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.DRV_STRENGTH = strength; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +void TMC6200Driver::setBBMCycles(uint8_t clockCycles) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.BBMCLKS = clockCycles; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +// TMC6200_SHORT_CONF ------ + +void TMC6200Driver::setShortDelay(TMC6200_ShortDelay shortDelay) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.SHORT_DELAY = shortDelay; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::shortFilter(TMC6200_ShortFilter shortFilter) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.SHORT_FILTER = shortFilter; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToSupplySensitivityLevel(uint8_t level) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.S2VS_LEVEL = level; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToGroundSensitivityLevel(uint8_t level) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.S2G_LEVEL = level; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortRetries(uint8_t retries) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.RETRY = retries; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setParallelProtect(TMC6200_ParallelProtect parallelProtect) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.PROTECT_PARALLEL = parallelProtect; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToGroundDetectionState(bool state) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.DISABLE_S2G = state ? 0 : 1; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToSupplyDetectionState(bool state) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.DISABLE_S2VS = state ? 0 : 1; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +// TMC6200_IOIN ------ + +TMC6200_IOIN TMC6200Driver::getInputs() { + TMC6200_IOIN ioin; + ioin.reg = readRegister(TMC6200_IOIN_REG); + + return ioin; +} + +// TMC6200_GSTAT ------ + +TMC6200GStatus TMC6200Driver::getStatus() { + TMC6200_GSTAT gstat; + gstat.reg = readRegister(TMC6200_GSTAT_REG); + + return {gstat}; +} + +void TMC6200Driver::setStatus(TMC6200_GSTAT status) { + writeRegister(TMC6200_GSTAT_REG, status.reg); +} + +uint32_t TMC6200Driver::readRegister(uint8_t addr) { + digitalWrite(csPin, LOW); + spi->beginTransaction(settings); + + // Address + spi->transfer(TMC_ADDRESS(addr)); + + // 32bit Data + uint32_t value = 0; + value |= (spi->transfer(0x00) << 24); + value |= (spi->transfer(0x00) << 16); + value |= (spi->transfer(0x00) << 8); + value |= (spi->transfer(0x00) << 0); + + spi->end(); + digitalWrite(csPin, HIGH); + + return value; +} + +void TMC6200Driver::writeRegister(uint8_t addr, uint32_t data) { + digitalWrite(csPin, LOW); + spi->beginTransaction(settings); + + // Address + spi->transfer(addr | TMC_WRITE_BIT); + + // 32bit Data + spi->transfer(0xFF & (data >> 24)); + spi->transfer(0xFF & (data >> 16)); + spi->transfer(0xFF & (data >> 8)); + spi->transfer(0xFF & (data >> 0)); + + spi->end(); + digitalWrite(csPin, HIGH); +} + +// -------------------- + +void TMC6200Driver3PWM::init(SPIClass *_spi) { + TMC6200Driver::init(_spi); + delayMicroseconds(1); + TMC6200Driver::setPWMMode(TMC6200_PWMMode::SingleLine); + BLDCDriver3PWM::init(); +}; + + +void TMC6200Driver6PWM::init(SPIClass *_spi) { + TMC6200Driver::init(_spi); + delayMicroseconds(1); + TMC6200Driver::setPWMMode(TMC6200_PWMMode::Individual); + BLDCDriver6PWM::init(); +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp new file mode 100644 index 0000000..7a5215c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include "TMC6200_Registers.hpp" + +#define TMC6200_VERSION 0x10 + +enum TMC6200_PWMMode { + Individual = 0, // 6PWM + SingleLine = 1, // 3PWM +}; + +enum TMC6200_AmplificationGain { + _5 = 0, + _10 = 1, + _Also_10 = 2, // maybe just remove this + _20 = 3, +}; + +enum TMC6200_ShortDelay { + _750nS = 0, + _1500nS = 1, +}; + +enum TMC6200_ShortFilter { + _100nS = 0, + _1uS = 1, + _2uS = 2, + _3uS = 3, +}; + +enum TMC6200_ParallelProtect { + Detected = 0, + All = 1, +}; + +enum TMC6200_FaultDirect { + AtLeastOneBridgeDown = 0, + EachProtectiveAction = 1, +}; + +enum TMC6200_OTSelect { + _150 = 0b00, + _143 = 0b01, + _136 = 0b10, + _120 = 0b11, +}; + +enum TMC6200_DRVStrength { + Weak = 0b00, + WeakTC = 0b01, // (medium above OTPW level) + Medium = 0b10, + Strong = 0b11, +}; + +class TMC6200GStatus { +public: + TMC6200GStatus(TMC6200_GSTAT status) : status(status) {}; + + bool isReset() const { return status.RESET == 0b1; }; + + bool isOverTemperaturePreWarning() const { return status.DRV_OTPW == 0b1; }; + + bool isOverTemperature() const { return status.DRV_OT == 0b1; }; + + bool isChargePumpUnderVoltage() const { return status.UV_CP == 0b1; }; + + bool hasUShorted() const { return status.SHORT_DET_U == 0b1; }; + + bool hasVShorted() const { return status.SHORT_DET_V == 0b1; }; + + bool hasWShorted() const { return status.SHORT_DET_W == 0b1; }; + + bool isUShortedToGround() const { return status.S2GU == 0b1; }; + + bool isUShortedToSupply() const { return status.S2VSU == 0b1; }; + + bool isVShortedToGround() const { return status.S2GV == 0b1; }; + + bool isVShortedToSupply() const { return status.S2VSV == 0b1; }; + + bool isWShortedToGround() const { return status.S2GW == 0b1; }; + + bool isWShortedToSupply() const { return status.S2VSW == 0b1; }; + + TMC6200_GSTAT status; +}; + +class TMC6200Driver { + +public: + TMC6200Driver(int csPin) : csPin(csPin), spi(&SPI), settings(1000000, MSBFIRST, SPI_MODE3) {}; + + virtual ~TMC6200Driver() {}; + + virtual void init(SPIClass *_spi); + + void setDriverState(bool state); + + void setPWMMode(TMC6200_PWMMode pwmMode); + + void setFaultDirect(TMC6200_FaultDirect faultDirect); + + void setCurrentSenseGain(TMC6200_AmplificationGain amplificationGain); + + void setOverTemperatureThreshold(TMC6200_OTSelect otLevel); + + void setDriverStrength(TMC6200_DRVStrength strength); + + void setCurrentSenseAmplifierState(bool state); + + void setShortDelay(TMC6200_ShortDelay shortDelay); + + void shortFilter(TMC6200_ShortFilter shortFilter); + + void setShortToSupplySensitivityLevel(uint8_t level); + + void setShortToGroundSensitivityLevel(uint8_t level); + + void setShortRetries(uint8_t retries); + + void setParallelProtect(TMC6200_ParallelProtect parallelProtect); + + void setShortToGroundDetectionState(bool state); + + void setShortToSupplyDetectionState(bool state); + + void setBBMCycles(uint8_t clockCycles); + + void setStatus(TMC6200_GSTAT status); + + TMC6200GStatus getStatus(); + + TMC6200_IOIN getInputs(); + +private: + uint32_t readRegister(uint8_t addr); + + void writeRegister(uint8_t addr, uint32_t data); + + int csPin; + SPIClass *spi; + SPISettings settings; +}; + + +class TMC6200Driver3PWM : public TMC6200Driver, public BLDCDriver3PWM { + +public: + TMC6200Driver3PWM(int phA, int phB, int phC, int cs, int en = NOT_SET) : + TMC6200Driver(cs), BLDCDriver3PWM(phA, phB, phC, en) {}; + + ~TMC6200Driver3PWM() override = default; + + virtual void init(SPIClass *_spi = &SPI) override; +}; + + +class TMC6200Driver6PWM : public TMC6200Driver, public BLDCDriver6PWM { + +public: + TMC6200Driver6PWM(int phA_h, int phA_l, int phB_h, int phB_l, int phC_h, int phC_l, int cs, int en = NOT_SET) : + TMC6200Driver(cs), BLDCDriver6PWM(phA_h, phA_l, phB_h, phB_l, phC_h, phC_l, en) {}; + + ~TMC6200Driver6PWM() override = default; + + virtual void init(SPIClass *_spi = &SPI) override; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp new file mode 100644 index 0000000..aff9fed --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp @@ -0,0 +1,111 @@ +#pragma once + +// ------ + +#define TMC_WRITE_BIT 0x80 +#define TMC_ADDRESS_MASK 0x7F +#define TMC_ADDRESS(x) ((x) & (TMC_ADDRESS_MASK)) + +// ------ + +#define TMC6200_GCONF_REG 0x00 // RW +#define TMC6200_GSTAT_REG 0x01 // RW + WC +#define TMC6200_IOIN_REG 0x04 // R + +#define TMC6200_OTP_PROG_REG 0x06 // not used +#define TMC6200_OTP_READ_REG 0x07 // not used +#define TMC6200_FACTORY_CONF_REG 0x08 // not used + +#define TMC6200_SHORT_CONF_REG 0x09 // RW +#define TMC6200_DRV_CONF_REG 0x0A // RW + +typedef union { + struct { + uint32_t DISABLE: 1; + uint32_t SINGLE_LINE: 1; + uint32_t FAULT_DIRECT: 1; + uint32_t : 1; + uint32_t AMPLIFICATION: 2; + uint32_t AMPLIFIER_OFF: 1; + uint32_t TMC6200_TEST_MODE: 1; + uint32_t : 24; + }; + uint32_t reg; +} TMC6200_GCONF; + +typedef union { + struct { + uint32_t RESET: 1; + uint32_t DRV_OTPW: 1; + uint32_t DRV_OT: 1; + uint32_t UV_CP: 1; + + uint32_t SHORT_DET_U: 1; + uint32_t S2GU: 1; + uint32_t S2VSU: 1; + uint32_t : 1; + + uint32_t SHORT_DET_V: 1; + uint32_t S2GV: 1; + uint32_t S2VSV: 1; + uint32_t : 1; + + uint32_t SHORT_DET_W: 1; + uint32_t S2GW: 1; + uint32_t S2VSW: 1; + uint32_t : 1; + }; + uint32_t reg; +} TMC6200_GSTAT; + +typedef union { + struct { + unsigned UL: 1; + uint32_t UH: 1; + uint32_t VL: 1; + uint32_t VH: 1; + uint32_t WL: 1; + uint32_t WH: 1; + uint32_t DRV_EN: 1; + uint32_t _reserved1: 1; + uint32_t OTPW: 1; + uint32_t OT136C: 1; + uint32_t OT143C: 1; + uint32_t OT150C: 1; + uint32_t _reserved2: 12; + uint32_t VERSION: 8; + }; + + uint32_t reg; +} TMC6200_IOIN; + +typedef union { + struct { + uint32_t S2VS_LEVEL: 4; + uint32_t : 4; + uint32_t S2G_LEVEL: 4; + uint32_t : 4; + uint32_t SHORT_FILTER: 2; + uint32_t : 2; + uint32_t SHORT_DELAY: 1; + uint32_t : 4; + uint32_t RETRY: 2; + uint32_t : 2; + uint32_t PROTECT_PARALLEL: 1; + uint32_t DISABLE_S2G: 1; + uint32_t DISABLE_S2VS: 1; + uint32_t : 2; + }; + uint32_t reg; +} TMC6200_SHORT_CONF; + +typedef union { + struct { + uint32_t BBMCLKS: 5; + uint32_t : 11; + uint32_t OT_SELECT: 2; + uint32_t DRV_STRENGTH: 2; + uint32_t : 12; + }; + uint32_t reg; +} TMC6200_DRV_CONF; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp new file mode 100644 index 0000000..85a4e11 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp @@ -0,0 +1,54 @@ + +#include "./A1334.h" + + + +A1334::A1334(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + + + +A1334::~A1334() { +}; + + + + +void A1334::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +}; + + + + +A1334Angle A1334::readRawAngle() { + uint16_t command = A1334_REG_ANG; + uint16_t cmdResult = spi_transfer16(command); // TODO fast mode + cmdResult = spi_transfer16(command); + A1334Angle result = { .reg = cmdResult }; + // TODO check parity + // errorflag = result.ef; + return result; +}; + + + + +uint16_t A1334::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + return result; +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.h new file mode 100644 index 0000000..798dcc4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/A1334.h @@ -0,0 +1,51 @@ + + +#ifndef __A1334_H__ +#define __A1334_H__ + +#include +#include + + +#define A1334_CPR 4096 +#define A1334_BITORDER MSBFIRST + +static SPISettings A1334SPISettings(8000000, A1334_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + + +typedef union { + struct { + uint16_t angle:12; + uint16_t p:1; + uint16_t nf:1; + uint16_t ef:1; + uint16_t ridc:1; + }; + uint16_t reg; +} A1334Angle; + + +#define A1334_REG_ANG 0x2000 + + + + +class A1334 { +public: + A1334(SPISettings settings = A1334SPISettings, int nCS = -1); + virtual ~A1334(); + + virtual void init(SPIClass* _spi = &SPI); + A1334Angle readRawAngle(); // 10 or 12 bit angle value +protected: + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + int nCS = -1; +}; + + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp new file mode 100644 index 0000000..c793947 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp @@ -0,0 +1,27 @@ + +#include "./MagneticSensorA1334.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorA1334::MagneticSensorA1334(int nCS, SPISettings settings) : A1334(settings, nCS) { + +} + + +MagneticSensorA1334::~MagneticSensorA1334(){ + +} + + +void MagneticSensorA1334::init(SPIClass* _spi) { + this->A1334::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorA1334::getSensorAngle() { + A1334Angle angle_data = readRawAngle(); + float result = ( angle_data.angle / (float)A1334_CPR) * _2PI; + // return the shaft angle + return result; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h new file mode 100644 index 0000000..59ae961 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h @@ -0,0 +1,24 @@ + +#ifndef __MAGNETIC_SENSOR_A1334_H__ +#define __MAGNETIC_SENSOR_A1334_H__ + +#include "common/base_classes/Sensor.h" +#include "./A1334.h" + + + +class MagneticSensorA1334 : public Sensor, public A1334 { +public: + MagneticSensorA1334(int nCS = -1, SPISettings settings = A1334SPISettings); + virtual ~MagneticSensorA1334(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp new file mode 100644 index 0000000..8be3bd5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp @@ -0,0 +1,102 @@ + +#include "AEAT8800Q24.h" + + +AEAT8800Q24::AEAT8800Q24(int nCS, int pinNSL, SPISettings spiSettings, SPISettings ssiSettings) : nCS(nCS), pinNSL(pinNSL), spiSettings(spiSettings), ssiSettings(ssiSettings) { +}; +AEAT8800Q24::~AEAT8800Q24(){ +}; + +void AEAT8800Q24::init(SPIClass* _spi){ + spi = _spi; + spi->beginTransaction(ssiSettings); +}; + + +float AEAT8800Q24::getCurrentAngle() { + return ((float)readRawAngle())/(float)AEAT8800Q24_CPR * 2.0f * (float)PI; +}; // angle in radians, return current value + + +// reads angle via ssi +uint16_t AEAT8800Q24::readRawAngle() { + // digitalWrite(pinNSL, LOW); //TODO maybe we don't need it... + // // 300ns delay + // delayMicroseconds(1); + uint16_t value = spi->transfer16(0x0000); + uint8_t status = spi->transfer(0x00); + //digitalWrite(pinNSL, HIGH); + // // 200ns delay before next read... + // delayMicroseconds(1); + lastStatus.reg = status; + return value; +}; + +AEAT8800Q24_Status_t AEAT8800Q24::getLastStatus() { + return lastStatus; +} + +uint16_t AEAT8800Q24::getZero(){ + uint8_t value = readRegister(AEAT8800Q24_REG_ZERO_MSB); + return (value<<8)|readRegister(AEAT8800Q24_REG_ZERO_LSB); +}; +void AEAT8800Q24::setZero(uint16_t value){ + writeRegister(AEAT8800Q24_REG_ZERO_MSB, (value>>8)&0xFF); + writeRegister(AEAT8800Q24_REG_ZERO_LSB, value&0xFF); +}; + +AEAT8800Q24_CONF0_t AEAT8800Q24::getConf0(){ + AEAT8800Q24_CONF0_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF0); + return result; +}; +void AEAT8800Q24::setConf0(AEAT8800Q24_CONF0_t value){ + writeRegister(AEAT8800Q24_REG_CONF0, value.reg); +}; + +AEAT8800Q24_CONF1_t AEAT8800Q24::getConf1(){ + AEAT8800Q24_CONF1_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF1); + return result; +}; +void AEAT8800Q24::setConf1(AEAT8800Q24_CONF1_t value){ + writeRegister(AEAT8800Q24_REG_CONF1, value.reg); +}; + +AEAT8800Q24_CONF2_t AEAT8800Q24::getConf2(){ + AEAT8800Q24_CONF2_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF2); + return result; +}; +void AEAT8800Q24::setConf2(AEAT8800Q24_CONF2_t value){ + writeRegister(AEAT8800Q24_REG_CONF2, value.reg); +}; + + + + +uint16_t AEAT8800Q24::transfer16SPI(uint16_t outValue) { + // delay 1us between switching the CS line to SPI + delayMicroseconds(1); + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->endTransaction(); + spi->beginTransaction(spiSettings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + // delay 1us between switching the CS line to SSI + delayMicroseconds(1); + spi->beginTransaction(ssiSettings); + return value; +}; +uint8_t AEAT8800Q24::readRegister(uint8_t reg) { + uint16_t cmd = 0x8000 | ((reg&0x001F)<<8); + uint16_t value = transfer16SPI(cmd); + return value&0x00FF; +}; +void AEAT8800Q24::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x4000 | ((reg&0x1F)<<8) | value; + /*uint16_t result =*/ transfer16SPI(cmd); +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h new file mode 100644 index 0000000..4acfe6d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h @@ -0,0 +1,119 @@ +#ifndef __AEAT8800Q24_H__ +#define __AEAT8800Q24_H__ + + +#include "Arduino.h" +#include "SPI.h" + +#define AEAT8800Q24_CPR 65536 + +#define AEAT8800Q24_REG_CUST0 0x00 +#define AEAT8800Q24_REG_CUST1 0x01 +#define AEAT8800Q24_REG_ZERO_LSB 0x02 +#define AEAT8800Q24_REG_ZERO_MSB 0x03 +#define AEAT8800Q24_REG_CONF0 0x04 +#define AEAT8800Q24_REG_CONF1 0x05 +#define AEAT8800Q24_REG_CONF2 0x06 +#define AEAT8800Q24_REG_CONF_OTP 0x13 +#define AEAT8800Q24_REG_CAL_OTP 0x1B +#define AEAT8800Q24_REG_CUST_OTP 0x11 +#define AEAT8800Q24_REG_CAL 0x17 + +#define AEAT8800Q24_CONF_OTP_ON 0xA3 +#define AEAT8800Q24_CAL_OTP_ON 0xA5 +#define AEAT8800Q24_CUST_OTP_ON 0xA1 +#define AEAT8800Q24_CAL_ON 0x02 +#define AEAT8800Q24_CAL_OFF 0x00 + + +typedef union { + struct { + uint8_t uvw_pwm_mode:1; + uint8_t pwm:2; + uint8_t iwidth:2; + uint8_t uvw:3; + }; + uint8_t reg; +} AEAT8800Q24_CONF0_t; + + +typedef union { + struct { + uint8_t cpr1:4; + uint8_t hys:4; + }; + uint8_t reg; +} AEAT8800Q24_CONF1_t; + + +typedef union { + struct { + uint8_t dir:1; + uint8_t zero_latency:1; + uint8_t abs_res:2; + uint8_t cpr2:4; + }; + uint8_t reg; +} AEAT8800Q24_CONF2_t; + + + +typedef union { + struct { + uint8_t mhi:1; + uint8_t mlo:1; + uint8_t ready:1; + uint8_t parity:1; + uint8_t :4; + }; + uint8_t reg; +} AEAT8800Q24_Status_t; + + +#ifndef MSBFIRST +#define MSBFIRST BitOrder::MSBFIRST +#endif + +static SPISettings AEAT8800Q24SPISettings(1000000, MSBFIRST, SPI_MODE3); // @suppress("Invalid arguments") +static SPISettings AEAT8800Q24SSISettings(4000000, MSBFIRST, SPI_MODE2); // @suppress("Invalid arguments") + + +class AEAT8800Q24 { +public: + AEAT8800Q24(int nCS, int pinNSL = MOSI, SPISettings spiSettings = AEAT8800Q24SPISettings, SPISettings ssiSettings = AEAT8800Q24SSISettings); + virtual ~AEAT8800Q24(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 14bit angle value + AEAT8800Q24_Status_t getLastStatus(); // get status associated with last angle read + + uint16_t getZero(); + void setZero(uint16_t); + + AEAT8800Q24_CONF0_t getConf0(); + void setConf0(AEAT8800Q24_CONF0_t); + + AEAT8800Q24_CONF1_t getConf1(); + void setConf1(AEAT8800Q24_CONF1_t); + + AEAT8800Q24_CONF2_t getConf2(); + void setConf2(AEAT8800Q24_CONF2_t); + +private: + int nCS = -1; + int pinNSL = -1; + SPISettings spiSettings; + SPISettings ssiSettings; + SPIClass* spi; + AEAT8800Q24_Status_t lastStatus; + + uint16_t transfer16SPI(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + void writeRegister(uint8_t reg, uint8_t value); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp new file mode 100644 index 0000000..f1ad32c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp @@ -0,0 +1,24 @@ +#include "./MagneticSensorAEAT8800Q24.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAEAT8800Q24::MagneticSensorAEAT8800Q24(int nCS, int pinNSL, SPISettings spiSettings, SPISettings ssiSettings) : AEAT8800Q24(nCS, pinNSL, spiSettings, ssiSettings) { +}; + + +MagneticSensorAEAT8800Q24::~MagneticSensorAEAT8800Q24(){ +}; + + +void MagneticSensorAEAT8800Q24::init(SPIClass* _spi) { + this->AEAT8800Q24::init(_spi); + this->Sensor::init(); +}; + + +float MagneticSensorAEAT8800Q24::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AEAT8800Q24_CPR) * _2PI; + // return the shaft angle + return angle_data; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h new file mode 100644 index 0000000..01d6e04 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h @@ -0,0 +1,18 @@ +#ifndef __MAGNETICSENSORAEAT8800Q24_H__ +#define __MAGNETICSENSORAEAT8800Q24_H__ + + +#include "common/base_classes/Sensor.h" +#include "./AEAT8800Q24.h" + +class MagneticSensorAEAT8800Q24 : public Sensor, public AEAT8800Q24 { +public: + MagneticSensorAEAT8800Q24(int nCS, int pinNSL = MOSI, SPISettings spiSettings = AEAT8800Q24SPISettings, SPISettings ssiSettings = AEAT8800Q24SSISettings); + virtual ~MagneticSensorAEAT8800Q24(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md new file mode 100644 index 0000000..0afa47d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md @@ -0,0 +1,75 @@ +# AEAT-8800-Q24 SimpleFOC driver + +SPI/SSI driver for the absolute position magnetic rotary encoder. This encoder is not supported by the +normal SimpleFOC drivers due to its mixed SPI/SSI mode. + +- access to the configuration registers of the AEAT-8800-Q24, enabling you to set parameters +- angles are read via SSI, with 16 bit (!) precision +- currently only 16 bit resolution is supported, don't lower the resolution + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. nCS pin is +required. + +Note that due to the way SSI and SPI share pins, you can normally only run one of these sensors per SPI bus. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAEAT8800Q24 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAEAT8800Q24 sensor1(SENSOR1_CS, MOSI_pin, mySPISettings, mySSISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 16 bit value + uint16_t raw = sensor1.readRawAngle(); + + // check status + float angle = sensor1.getSensorAngle(); + AEAT8800Q24_Status_t status = sensor1.getLastStatus(); + if (status.mlo) + Serial.println("Sensor magnet low error"); + if (status.mhi) + Serial.println("Sensor magnet high error"); +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp new file mode 100644 index 0000000..9455775 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp @@ -0,0 +1,245 @@ +/* + * AS5047.cpp + * + * Created on: 2 May 2021 + * Author: runger + */ + +#include "./AS5047.h" + +AS5047::AS5047(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5047::~AS5047() { +} + + +void AS5047::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5047::getCurrentAngle(){ + readCorrectedAngle(); + return ((float)readCorrectedAngle())/(float)AS5047_CPR * 2.0f * (float)PI; +} + +float AS5047::getFastAngle(){ + return ((float)readCorrectedAngle())/(float)AS5047_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5047::readRawAngle(){ + uint16_t command = AS5047_ANGLEUNC_REG | AS5047_RW; // set r=1 and parity=0, result is 0x7FFE + uint16_t lastresult = spi_transfer16(command)&AS5047_RESULT_MASK; + return lastresult; +} +uint16_t AS5047::readCorrectedAngle(){ + uint16_t command = AS5047_ANGLECOM_REG | AS5047_PARITY | AS5047_RW; // set r=1 and parity=1, result is 0xFFFF + uint16_t lastresult = spi_transfer16(command)&AS5047_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047::readMagnitude(){ + uint16_t command = AS5047_MAGNITUDE_REG | AS5047_RW; // set r=1, result is 0x7FFD + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + return result; +} + + +bool AS5047::isErrorFlag(){ + return errorflag; +} + + +AS5047Error AS5047::clearErrorFlag(){ + uint16_t command = AS5047_ERROR_REG | AS5047_RW; // set r=1, result is 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + AS5047Error err = { + .framingError = ((result&0x0001)!=0x0000), + .commandInvalid = ((result&0x0002)!=0x0000), + .parityError = ((result&0x0004)!=0x0000) + }; + return err; +} + + +AS5047Settings1 AS5047::readSettings1(){ + uint16_t command = AS5047_SETTINGS1_REG | AS5047_PARITY | AS5047_RW; // set r=1, result is 0xC018 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047Settings1 result = { + .reg = nop() + }; + return result; +} + + +void AS5047::writeSettings1(AS5047Settings1 settings){ + uint16_t command = AS5047_SETTINGS1_REG; // set r=0, result is 0x0018 + spi_transfer16(command); + spi_transfer16(calcParity(settings.reg)); +} + + +AS5047Settings2 AS5047::readSettings2(){ + uint16_t command = AS5047_SETTINGS2_REG | AS5047_RW; // set r=1, result is 0x4019 + spi_transfer16(command); + AS5047Settings2 result = { + .reg = nop() + }; + return result; +} + + +void AS5047::writeSettings2(AS5047Settings2 settings){ + uint16_t command = AS5047_SETTINGS2_REG | AS5047_PARITY; // set r=0, result is 0x8019 + spi_transfer16(command); + spi_transfer16(calcParity(settings.reg)); +} + + + +AS5047Diagnostics AS5047::readDiagnostics(){ + uint16_t command = AS5047_DIAGNOSTICS_REG | AS5047_PARITY | AS5047_RW; // set r=1, result is 0xFFFC + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047Diagnostics result = { + .reg = nop() + }; + return result; +} + + +void AS5047::enablePWM(bool enable){ + AS5047Settings1 settings = readSettings1(); + if (settings.pwmon == enable) return; // no change + settings.pwmon = enable; + writeSettings1(settings); +} + +void AS5047::enableABI(bool enable){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = enable?0:1; + if (settings.uvw_abi == val) return; // no change + settings.uvw_abi = val; + writeSettings1(settings); +} + + +void AS5047::enableDEAC(bool enable){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = enable?0:1; + if (settings.daecdis == val) return; // no change + settings.daecdis = enable?0:1; + writeSettings1(settings); +} + + +void AS5047::useCorrectedAngle(bool useCorrected){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = useCorrected?0:1; + if (settings.dataselect == val) return; // no change + settings.dataselect = val; + writeSettings1(settings); +} + + + +void AS5047::setHysteresis(uint8_t hyst){ + if (hyst>3) hyst = 3; + uint8_t val = 3-hyst; + AS5047Settings2 settings = readSettings2(); + if (settings.hys == val) return; // no change + settings.hys = val; + writeSettings2(settings); +} + + + + +void AS5047::setABIResolution(AS5047ABIRes res){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = (res>>3)&0x01; + if (settings.abibin!=val || settings.uvw_abi!=0) { + settings.abibin = val; + settings.uvw_abi = 0; + writeSettings1(settings); + } + AS5047Settings2 settings2 = readSettings2(); + val = (res&0x07); + if (settings2.abires!=val) { + settings2.abires = val; + writeSettings2(settings2); + } +} + + + +uint16_t AS5047::setZero(uint16_t value){ + uint16_t command = AS5047_ZPOSL_REG | AS5047_PARITY | AS5047_RW; + spi_transfer16(command); + AS5047ZPosL posL = { + .reg = nop() + }; + command = AS5047_ZPOSM_REG | AS5047_PARITY; + spi_transfer16(command); + spi_transfer16(calcParity((value>>6)&0x00FF)); + command = AS5047_ZPOSL_REG; + posL.zposl = value&0x003F; + spi_transfer16(command); + spi_transfer16(calcParity(posL.reg)); + + return getZero(); +} + + +uint16_t AS5047::getZero() { + uint16_t command = AS5047_ZPOSM_REG | AS5047_RW; + spi_transfer16(command); + command = AS5047_ZPOSL_REG | AS5047_PARITY | AS5047_RW; + uint16_t result = spi_transfer16(command); + AS5047ZPosL posL = { + .reg = nop() + }; + return ((result&0x00FF)<<6) | posL.zposl; +} + + +uint16_t AS5047::nop(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5047_RESULT_MASK; +} + + +uint16_t AS5047::calcParity(uint16_t data){ + data = data & 0x7FFF; + int sum = 0; + for (int i=0;i<15;i++) + if ((data>>i)&0x0001) + sum++; + return (sum&0x01)==0x01?(data|0x8000):data; +} + + +uint16_t AS5047::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + // TODO check parity + errorflag = ((result&AS5047_ERRFLG)>0); + return result; +} + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.h new file mode 100644 index 0000000..e3cc80c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/AS5047.h @@ -0,0 +1,167 @@ +/* + * AS5047.h + * + * Created on: 2 May 2021 + * Author: runger + */ + +#ifndef AS5047_H_ +#define AS5047_H_ + +#include "Arduino.h" +#include "SPI.h" + + + + +union AS5047Diagnostics { + struct { + uint16_t agc:8; + uint16_t lf:1; + uint16_t cof:1; + uint16_t magh:1; + uint16_t magl:1; + uint16_t unused:4; + }; + uint16_t reg; +}; + + +union AS5047ZPosM { + struct { + uint16_t zposm:8; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047ZPosL { + struct { + uint16_t zposl:6; + uint16_t comp_l_error_en:1; + uint16_t comp_h_error_en:1; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047Settings1 { + struct { + uint16_t reserved:1; + uint16_t noiseset:1; + uint16_t dir:1; + uint16_t uvw_abi:1; + uint16_t daecdis:1; + uint16_t abibin:1; + uint16_t dataselect:1; + uint16_t pwmon:1; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047Settings2 { + struct { + uint16_t uvwpp:3; + uint16_t hys:2; + uint16_t abires:3; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +struct AS5047Error { + bool framingError; + bool commandInvalid; + bool parityError; +}; + + +enum AS5047ABIRes : uint8_t { + AS5047_ABI_1024 = 0b1010, + AS5047_ABI_2048 = 0b1001, + AS5047_ABI_4096 = 0b1000, + AS5047_ABI_100 = 0b0111, + AS5047_ABI_200 = 0b0110, + AS5047_ABI_400 = 0b0101, + AS5047_ABI_800 = 0b0100, + AS5047_ABI_1200 = 0b0011, + AS5047_ABI_1600 = 0b0010, + AS5047_ABI_2000 = 0b0001, + AS5047_ABI_4000 = 0b0000 +}; + + +#define AS5047_CPR 16384 +#define AS5047_ANGLECOM_REG 0x3FFF +#define AS5047_ANGLEUNC_REG 0x3FFE +#define AS5047_MAGNITUDE_REG 0x3FFD +#define AS5047_DIAGNOSTICS_REG 0x3FFC +#define AS5047_ERROR_REG 0x0001 +#define AS5047_PROG_REG 0x0003 +#define AS5047_ZPOSM_REG 0x0016 +#define AS5047_ZPOSL_REG 0x0017 +#define AS5047_SETTINGS1_REG 0x0018 +#define AS5047_SETTINGS2_REG 0x0019 + +#define AS5047_PARITY 0x8000 +#define AS5047_RW 0x4000 +#define AS5047_ERRFLG 0x4000 +#define AS5047_RESULT_MASK 0x3FFF + + +#define AS5047_BITORDER MSBFIRST + +static SPISettings AS5047SPISettings(8000000, AS5047_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5047 { +public: + AS5047(SPISettings settings = AS5047SPISettings, int nCS = -1); + virtual ~AS5047(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readCorrectedAngle(); // 14bit corrected angle value + uint16_t readMagnitude(); // 14bit magnitude value + + bool isErrorFlag(); + AS5047Error clearErrorFlag(); + + AS5047Diagnostics readDiagnostics(); + AS5047Settings1 readSettings1(); + void writeSettings1(AS5047Settings1 settings); + AS5047Settings2 readSettings2(); + void writeSettings2(AS5047Settings2 settings); + void enablePWM(bool enable); + void enableABI(bool enable); + void setABIResolution(AS5047ABIRes res); + void enableDEAC(bool enable); + void useCorrectedAngle(bool useCorrected); + void setHysteresis(uint8_t hyst); + + uint16_t setZero(uint16_t); + uint16_t getZero(); + + uint16_t calcParity(uint16_t data); + +private: + + uint16_t nop(); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; + +}; + +#endif /* AS5047_H_ */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp new file mode 100644 index 0000000..f5f2e5f --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp @@ -0,0 +1,29 @@ + +#include "./MagneticSensorAS5047.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5047::MagneticSensorAS5047(int nCS, bool fastMode, SPISettings settings) : AS5047(settings, nCS), fastMode(fastMode) { + +} + + +MagneticSensorAS5047::~MagneticSensorAS5047(){ + +} + + +void MagneticSensorAS5047::init(SPIClass* _spi) { + this->AS5047::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorAS5047::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AS5047_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h new file mode 100644 index 0000000..aae3f54 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5047PD_H__ +#define __MAGNETICSENSORAS5047PD_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5047.h" + +class MagneticSensorAS5047 : public Sensor, public AS5047 { +public: + MagneticSensorAS5047(int nCS = -1, bool fastMode = false, SPISettings settings = AS5047SPISettings); + virtual ~MagneticSensorAS5047(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/README.md new file mode 100644 index 0000000..22de36b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047/README.md @@ -0,0 +1,82 @@ +# AS5047 SimpleFOC driver + +While the AS5047 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5047-specific driver includes some optimisations: + +- access to the other registers of the AS5047, including the magnitude value which can be used to check the magnet strength, and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + +This driver should work with AS5047P and AS5047D models. The AS5047U has it's own driver [here](../as5047u/). + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5047/MagneticSensorAS5047.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5047 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5047 sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5047Error error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5047Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp new file mode 100644 index 0000000..bfe70be --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp @@ -0,0 +1,350 @@ +/* + * AS5047U.cpp + * + * Created on: 24 Feb 2022 + * Author: runger + */ + +#include "./AS5047U.h" + +AS5047U::AS5047U(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5047U::~AS5047U() { +} + + +void AS5047U::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5047U::getCurrentAngle(){ + readCorrectedAngle(); + return ((float)readCorrectedAngle())/(float)AS5047U_CPR * 2.0f * (float)PI; +} + +float AS5047U::getFastAngle(){ + return ((float)readCorrectedAngle())/(float)AS5047U_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5047U::readRawAngle(){ + uint16_t command = AS5047U_ANGLEUNC_REG | AS5047U_RW; + uint16_t lastresult = spi_transfer16(command)&AS5047U_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047U::readCorrectedAngle(){ + uint16_t command = AS5047U_ANGLECOM_REG | AS5047U_RW; + uint16_t lastresult = spi_transfer16(command)&AS5047U_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047U::readMagnitude(){ + uint16_t command = AS5047U_MAGNITUDE_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result; +} + + +uint16_t AS5047U::readVelocity(){ + uint16_t command = AS5047U_VELOCITY_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result; +} + + +bool AS5047U::isErrorFlag(){ + return errorflag; +} + +bool AS5047U::isWarningFlag(){ + return warningflag; +} + + +AS5047UError AS5047U::clearErrorFlag(){ + uint16_t command = AS5047U_ERROR_REG | AS5047U_RW; // set r=1, result is 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UError result; + result.reg = nop16(); + return result; +} + + +AS5047USettings1 AS5047U::readSettings1(){ + uint16_t command = AS5047U_SETTINGS1_REG | AS5047U_RW; // set r=1, result is 0xC018 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings1 result = { + .reg = nop16() + }; + return result; +} + + + + +void AS5047U::writeSettings1(AS5047USettings1 settings){ + writeRegister24(AS5047U_SETTINGS1_REG, settings.reg); +} + + + + +AS5047USettings2 AS5047U::readSettings2(){ + uint16_t command = AS5047U_SETTINGS2_REG | AS5047U_RW; // set r=1, result is 0x4019 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings2 result = { + .reg = nop16() + }; + return result; +} + + + + + +void AS5047U::writeSettings2(AS5047USettings2 settings){ + writeRegister24(AS5047U_SETTINGS2_REG, settings.reg); +} + + + + + +AS5047USettings3 AS5047U::readSettings3(){ + uint16_t command = AS5047U_SETTINGS3_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings3 result = { + .reg = nop16() + }; + return result; +} + + + + + +void AS5047U::writeSettings3(AS5047USettings3 settings){ + writeRegister24(AS5047U_SETTINGS3_REG, settings.reg); +} + + + + +AS5047UDiagnostics AS5047U::readDiagnostics(){ + uint16_t command = AS5047U_DIAGNOSTICS_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UDiagnostics result = { + .reg = nop16() + }; + return result; +} + + + + +uint8_t AS5047U::readAGC(){ + uint16_t command = AS5047U_AGC_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result & 0x00FF; +}; + + + +uint8_t AS5047U::readECCCHK(){ + uint16_t command = AS5047U_ECCCHK_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result & 0x007F; +}; + + + + +AS5047UDisableSettings AS5047U::readDisableSettings(){ + uint16_t command = AS5047U_DISABLE_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UDisableSettings result = { + .reg = nop16() + }; + return result; +}; + + + +void AS5047U::writeDisableSettings(AS5047UDisableSettings settings){ + writeRegister24(AS5047U_DISABLE_REG, settings.reg); +}; + + + +AS5047UECCSettings AS5047U::readECCSettings(){ + uint16_t command = AS5047U_ECC_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UECCSettings result = { + .reg = nop16() + }; + return result; +}; + + + +void AS5047U::writeECCSettings(AS5047UECCSettings settings){ + writeRegister24(AS5047U_ECC_REG, settings.reg); +}; + + + + + + + +void AS5047U::enablePWM(bool enable, bool pwmOnWPin){ + // AS5047UDisableSettings settings = readDisableSettings(); + // if (settings.uvw_off==1) { + // settings.uvw_off = 0; + // writeDiableSettings(settings); + // } + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = pwmOnWPin?0:1; + settings2.pwm_on = enable; + writeSettings2(settings2); +} + + + + +void AS5047U::enableABI(bool enable){ + AS5047UDisableSettings settings = readDisableSettings(); + settings.abi_off = enable?0:1; + writeDisableSettings(settings); + delayMicroseconds(50); + if (enable) { + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = 0; + writeSettings2(settings2); + } +} + + + +void AS5047U::enableUVW(bool enable){ + AS5047UDisableSettings settings = readDisableSettings(); + settings.uvw_off = enable?0:1; + writeDisableSettings(settings); + if (enable) { + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = 1; + writeSettings2(settings2); + } +} + + + + +uint16_t AS5047U::getZero(){ + uint16_t command = AS5047U_ZPOSM_REG | AS5047U_RW; + spi_transfer16(command); + command = AS5047U_ZPOSL_REG | AS5047U_RW; + uint16_t result = spi_transfer16(command); + AS5047UZPosL posL = { + .reg = nop16() + }; + return ((result&0x00FF)<<6) | posL.zposl; +} + + + +uint16_t AS5047U::setZero(uint16_t value){ + uint16_t command = AS5047U_ZPOSL_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UZPosL posL = { + .reg = nop16() + }; + posL.zposl = value&0x003F; + writeRegister24(AS5047U_ZPOSL_REG, posL.reg); + writeRegister24(AS5047U_ZPOSM_REG, (value>>6)&0x00FF); + return getZero(); +} + + + + +uint16_t AS5047U::nop16(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5047U_RESULT_MASK; +} + + + +uint16_t AS5047U::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((result&AS5047U_ERROR)>0); + warningflag = ((result&AS5047U_WARNING)>0); + return result; +} + +uint8_t AS5047U::calcCRC(uint16_t data){ + uint8_t crc = 0xC4; // Initial value + for (int i = 0; i < 16; i++) { + if ((crc ^ data) & 0x8000) { + crc = (crc << 1) ^ 0x1D; + } else { + crc <<= 1; + } + data <<= 1; + } + return crc ^ 0xFF; +} + +uint16_t AS5047U::writeRegister24(uint16_t reg, uint16_t data) { + uint8_t buff[3]; + buff[0] = (reg>>8)&0x3F; + buff[1] = reg&0xFF; + buff[2] = calcCRC(reg); + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + spi->transfer(buff, 3); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((buff[0]&0x40)>0); + warningflag = ((buff[0]&0x80)>0); + + buff[0] = (data>>8)&0x3F; + buff[1] = data&0xFF; + buff[2] = calcCRC(data); + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + spi->transfer(buff, 3); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((buff[0]&0x40)>0); + warningflag = ((buff[0]&0x80)>0); + + delayMicroseconds(50); + + return buff[0]<<8 | buff[1]; +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h new file mode 100644 index 0000000..cb9cf10 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h @@ -0,0 +1,225 @@ +/* + * AS5047.h + * + * Created on: 24 Feb 2022 + * Author: runger + */ + +#ifndef AS5047U_H_ +#define AS5047U_H_ + +#include "Arduino.h" +#include "SPI.h" + + +union AS5047UDisableSettings { + struct { + uint16_t :9; + uint16_t filter_disable:1; + uint16_t :4; + uint16_t abi_off:1; + uint16_t uvw_off:1; + }; + uint16_t reg; +}; + + + +union AS5047UDiagnostics { + struct { + uint16_t spi_cnt:2; + uint16_t :1; + uint16_t agc_finished:1; + uint16_t off_finished:1; + uint16_t sin_finished:1; + uint16_t cos_finished:1; + uint16_t maghalf_flag:1; + uint16_t comp_h:1; + uint16_t comp_l:1; + uint16_t cordic_ovf:1; + uint16_t loops_finished:1; + uint16_t vdd_mode:1; + }; + uint16_t reg; +}; + + +union AS5047UZPosM { + struct { + uint16_t zposm:8; + }; + uint16_t reg; +}; + + +union AS5047UZPosL { + struct { + uint16_t zposl:6; + uint16_t dia1_en:1; // automotive version only + uint16_t dia2_en:1; // automotive version only + }; + uint16_t reg; +}; + + +union AS5047USettings1 { + struct { + uint16_t k_max:3; + uint16_t k_min:3; + uint16_t dia3_en:1; // not applicable + uint16_t dia4_en:1; // not applicable + }; + uint16_t reg; +}; + + +union AS5047USettings2 { + struct { + uint16_t iwidth:1; + uint16_t noiseset:1; + uint16_t dir:1; + uint16_t uvw_abi:1; + uint16_t daecdis:1; + uint16_t abi_dec:1; + uint16_t data_select:1; + uint16_t pwm_on:1; + }; + uint16_t reg; +}; + + + + +union AS5047USettings3 { + struct { + uint16_t uvwpp:3; + uint16_t hys:2; + uint16_t abires:3; + }; + uint16_t reg; +}; + + + + +union AS5047UECCSettings { + struct { + uint16_t ecc_chsum:7; + uint16_t ecc_en:1; + }; + uint16_t reg; +}; + + + + +struct AS5047UError { + struct { + uint16_t cordic_ovf:1; + uint16_t off_notfinished:1; + uint16_t :1; + uint16_t wdtst:1; + uint16_t crc_error:1; + uint16_t cmd_error:1; + uint16_t frame_error:1; + uint16_t p2ram_error:1; + uint16_t p2ram_warning:1; + uint16_t maghalf:1; + uint16_t agc_warning:1; + }; + uint16_t reg; +}; + + + + + +#define AS5047U_CPR 16384 +#define AS5047U_ANGLECOM_REG 0x3FFF +#define AS5047U_ANGLEUNC_REG 0x3FFE +#define AS5047U_MAGNITUDE_REG 0x3FFD +#define AS5047U_VELOCITY_REG 0x3FFC +#define AS5047U_SIN_REG 0x3FFA +#define AS5047U_COS_REG 0x3FFB +#define AS5047U_AGC_REG 0x3FF9 +#define AS5047U_DIAGNOSTICS_REG 0x3FF5 +#define AS5047U_ERROR_REG 0x0001 +#define AS5047U_PROG_REG 0x0003 +#define AS5047U_ECCCHK_REG 0x00D1 + +#define AS5047U_DISABLE_REG 0x0015 +#define AS5047U_ZPOSM_REG 0x0016 +#define AS5047U_ZPOSL_REG 0x0017 +#define AS5047U_SETTINGS1_REG 0x0018 +#define AS5047U_SETTINGS2_REG 0x0019 +#define AS5047U_SETTINGS3_REG 0x001A +#define AS5047U_ECC_REG 0x001B + +#define AS5047U_WARNING 0x8000 +#define AS5047U_ERROR 0x4000 +#define AS5047U_RW 0x4000 +#define AS5047U_ERRFLG 0xC000 +#define AS5047U_RESULT_MASK 0x3FFF + + +#define AS5047U_BITORDER MSBFIRST + +static SPISettings AS5047USPISettings(8000000, AS5047U_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5047U { +public: + AS5047U(SPISettings settings = AS5047USPISettings, int nCS = -1); + virtual ~AS5047U(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readCorrectedAngle(); // 14bit corrected angle value + uint16_t readMagnitude(); // 14bit magnitude value + uint16_t readVelocity(); // 14bit magnitude value + + bool isErrorFlag(); + bool isWarningFlag(); + AS5047UError clearErrorFlag(); + + AS5047UDiagnostics readDiagnostics(); + uint8_t readAGC(); + uint8_t readECCCHK(); + + AS5047UDisableSettings readDisableSettings(); + void writeDisableSettings(AS5047UDisableSettings settings); + AS5047USettings1 readSettings1(); + void writeSettings1(AS5047USettings1 settings); + AS5047USettings2 readSettings2(); + void writeSettings2(AS5047USettings2 settings); + AS5047USettings3 readSettings3(); + void writeSettings3(AS5047USettings3 settings); + AS5047UECCSettings readECCSettings(); + void writeECCSettings(AS5047UECCSettings settings); + + void enablePWM(bool enable, bool pwmOnWPin = true); + void enableABI(bool enable); + void enableUVW(bool enable); + + uint16_t setZero(uint16_t); + uint16_t getZero(); + +private: + + uint16_t nop16(); + uint16_t spi_transfer16(uint16_t outdata); + uint8_t calcCRC(uint16_t data); + uint16_t writeRegister24(uint16_t reg, uint16_t data); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + bool warningflag = false; + int nCS = -1; + +}; + +#endif /* AS5047U_H_ */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp new file mode 100644 index 0000000..e5fe89a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp @@ -0,0 +1,29 @@ + +#include "./MagneticSensorAS5047U.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5047U::MagneticSensorAS5047U(int nCS, bool fastMode, SPISettings settings) : AS5047U(settings, nCS), fastMode(fastMode) { + +} + + +MagneticSensorAS5047U::~MagneticSensorAS5047U(){ + +} + + +void MagneticSensorAS5047U::init(SPIClass* _spi) { + this->AS5047U::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorAS5047U::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AS5047U_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h new file mode 100644 index 0000000..e6ef813 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5047U_H__ +#define __MAGNETICSENSORAS5047U_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5047U.h" + +class MagneticSensorAS5047U : public Sensor, public AS5047U { +public: + MagneticSensorAS5047U(int nCS = -1, bool fastMode = false, SPISettings settings = AS5047USPISettings); + virtual ~MagneticSensorAS5047U(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/README.md new file mode 100644 index 0000000..2a6743c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5047u/README.md @@ -0,0 +1,81 @@ +# AS5047U SimpleFOC driver + +While AS5047U absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5047U-specific driver includes some optimisations: + +- access to the other registers of the AS5047U, including the magnitude value which can be used to check the magnet strength, the velocity register and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5047u/MagneticSensorAS5047U.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5047U sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5047U sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5047UError error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5047Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp new file mode 100644 index 0000000..27ef254 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp @@ -0,0 +1,118 @@ +/* + * AS5048A.cpp + * + * Created on: 8 Mar 2021 + * Author: runger + */ + +#include "AS5048A.h" + +AS5048A::AS5048A(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5048A::~AS5048A() { +} + + +void AS5048A::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5048A::getCurrentAngle(){ + readRawAngle(); + return ((float)readRawAngle())/(float)AS5048A_CPR * 2.0f * (float)PI; +} + +float AS5048A::getFastAngle(){ + return ((float)readRawAngle())/(float)AS5048A_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5048A::readRawAngle(){ + uint16_t command = AS5048A_ANGLE_REG | AS5048A_PARITY | AS5048A_RW; // set r=1 and parity=1, result ix 0xFFFF + uint16_t lastresult = spi_transfer16(command)&AS5048A_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5048A::readMagnitude(){ + uint16_t command = AS5048A_MAGNITUDE_REG | AS5048A_RW; // set r=1, result ix 0x7FFE + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + return result; +} + + +bool AS5048A::isErrorFlag(){ + return errorflag; +} + + +AS5048Error AS5048A::clearErrorFlag(){ + uint16_t command = AS5048A_ERROR_REG | AS5048A_RW; // set r=1, result ix 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + AS5048Error err = { + .parityError = ((result&0x0004)!=0x0000), + .commandInvalid = ((result&0x0002)!=0x0000), + .framingError = ((result&0x0001)!=0x0000) + }; + return err; +} + + +AS5048Diagnostics AS5048A::readDiagnostics(){ + uint16_t command = AS5048A_DIAGNOSTICS_REG | AS5048A_RW; // set r=1, result ix 0x7FFD + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5048Diagnostics result = { + .reg = nop() + }; + return result; +} + + +uint16_t AS5048A::setZero(uint16_t){ + // TODO implement me! + return 0; +} + + +uint16_t AS5048A::enableOneTimeProgramming(){ + // no plans to implement this at the moment. one-time-programming a $10 chip isn't really a "maker" thing to do. The zero + // position can be easily retained in software, stored on the MCU, and thereby the sensor can be reused in another project. + return 0; +} + + +uint16_t AS5048A::programZero(){ + // no plans to implement this at the moment. one-time-programming a $10 chip isn't really a "maker" thing to do. The zero + // position can be easily retained in software, stored on the MCU, and thereby the sensor can be reused in another project. + return 0; +} + +uint16_t AS5048A::nop(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5048A_RESULT_MASK; +} + +uint16_t AS5048A::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + // TODO check parity + errorflag = ((result&AS5048A_ERRFLG)>0); + return result; +} + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h new file mode 100644 index 0000000..00a4fe6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h @@ -0,0 +1,89 @@ +/* + * AS5048A.h + * + * Created on: 8 Mar 2021 + * Author: runger + */ + +#ifndef AS5048A_H_ +#define AS5048A_H_ + +#include "Arduino.h" +#include "SPI.h" + + + + +union AS5048Diagnostics { + struct { + uint16_t agc:8; + uint16_t ocf:1; + uint16_t cof:1; + uint16_t compLow:1; + uint16_t compHigh:1; + }; + uint16_t reg; +}; + + +struct AS5048Error { + bool parityError; + bool commandInvalid; + bool framingError; +}; + + +#define AS5048A_CPR 16384 +#define AS5048A_ANGLE_REG 0x3FFF +#define AS5048A_ERROR_REG 0x0001 +#define AS5048A_PROGCTL_REG 0x0003 +#define AS5048A_OTPHIGH_REG 0x0016 +#define AS5048A_OTPLOW_REG 0x0017 +#define AS5048A_DIAGNOSTICS_REG 0x3FFD +#define AS5048A_MAGNITUDE_REG 0x3FFE +#define AS5048A_PARITY 0x8000 +#define AS5048A_RW 0x4000 +#define AS5048A_ERRFLG 0x4000 +#define AS5048A_RESULT_MASK 0x3FFF + + +#define AS5048_BITORDER MSBFIRST + + +static SPISettings AS5048SPISettings(8000000, AS5048_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5048A { +public: + AS5048A(SPISettings settings = AS5048SPISettings, int nCS = -1); + virtual ~AS5048A(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readMagnitude(); // 14bit magnitude value + + bool isErrorFlag(); + AS5048Error clearErrorFlag(); + + AS5048Diagnostics readDiagnostics(); + + uint16_t setZero(uint16_t); + uint16_t enableOneTimeProgramming(); + uint16_t programZero(); + +private: + + uint16_t nop(); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; + +}; + +#endif /* AS5048A_H_ */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp new file mode 100644 index 0000000..a58b1b9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp @@ -0,0 +1,26 @@ + +#include "./MagneticSensorAS5048A.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5048A::MagneticSensorAS5048A(int nCS, bool fastMode, SPISettings settings) : AS5048A(settings, nCS), fastMode(fastMode) { + +} + +MagneticSensorAS5048A::~MagneticSensorAS5048A(){ + +} +void MagneticSensorAS5048A::init(SPIClass* _spi) { + this->AS5048A::init(_spi); + this->Sensor::init(); +} + +float MagneticSensorAS5048A::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + + angle_data = ( angle_data / (float)AS5048A_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h new file mode 100644 index 0000000..0f7e2b7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5048A_H__ +#define __MAGNETICSENSORAS5048A_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5048A.h" + +class MagneticSensorAS5048A : public Sensor, public AS5048A { +public: + MagneticSensorAS5048A(int nCS = -1, bool fastMode = false, SPISettings settings = AS5048SPISettings); + virtual ~MagneticSensorAS5048A(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +protected: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp new file mode 100644 index 0000000..9f340c4 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp @@ -0,0 +1,50 @@ +/* + * PreciseMagneticSensorAS5048A.cpp + * + * Created on: 1 May 2021 + * Author: runger + */ + +#include +#include "common/foc_utils.h" +#include "common/time_utils.h" + +PreciseMagneticSensorAS5048A::PreciseMagneticSensorAS5048A(int nCS, bool fastMode, SPISettings settings) : AS5048A(settings, nCS), fastMode(fastMode) { } + +PreciseMagneticSensorAS5048A::~PreciseMagneticSensorAS5048A() { } + + +void PreciseMagneticSensorAS5048A::init(SPIClass* _spi) { + this->AS5048A::init(_spi); + // velocity calculation init + current_ts = _micros(); + /*uint16_t angle_data =*/ readRawAngle(); + current_angle = PreciseAngle(readRawAngle(), 0); + getAngle(); +} + + + +float PreciseMagneticSensorAS5048A::getSensorAngle() { + previous_ts = current_ts; + previous_angle = current_angle; + uint16_t angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + current_ts = _micros(); + current_angle.update(angle_data); + return current_angle.asFloat(); +} + + + +/* +unlike the normal MagneticSensorSPI implementation, this one uses the angle previously read by the last call to getAngle to do its +calculation, and does not directly poll any data from the sensor. +This is an optimisation for speed, based on the assumption that loopFOC() (which calls getAngle()) is invoked at least as often as +move() (which calls getVelocity()). If this is the case, getVelocity() should always have a sufficiently "fresh" value to work with. +If using this function in a different context, simply call getAngle() first to be sure of a fresh angle value. +*/ +float PreciseMagneticSensorAS5048A::getVelocity() { + return current_angle.velocity(previous_angle, (current_ts-previous_ts)); +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h new file mode 100644 index 0000000..389b6f7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h @@ -0,0 +1,33 @@ +/* + * PreciseMagneticSensorAS5048A.h + * + * Created on: 1 May 2021 + * Author: runger + */ + +#ifndef LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ +#define LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ + +#include "common/base_classes/Sensor.h" +#include "./AS5048A.h" +#include "utilities/PreciseAngle.h" + +class PreciseMagneticSensorAS5048A : public Sensor, public AS5048A { +public: + PreciseMagneticSensorAS5048A(int nCS = -1, bool fastMode = false, SPISettings settings = AS5048SPISettings); + virtual ~PreciseMagneticSensorAS5048A(); + + virtual float getSensorAngle() override; + virtual float getVelocity() override; + + virtual void init(SPIClass* _spi = &SPI) override; + +protected: + bool fastMode = false; + PreciseAngle previous_angle = PreciseAngle(); + PreciseAngle current_angle = PreciseAngle(); + unsigned long previous_ts = 0; + unsigned long current_ts = 1; +}; + +#endif /* LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/README.md new file mode 100644 index 0000000..468994c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5048a/README.md @@ -0,0 +1,86 @@ +# AS5048A SimpleFOC driver + +While AS5048A absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5048A-specific driver includes some optimisations: + +- access to the other registers of the AS5048A, including the magnitude value which can be used to check the magnet strength, and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5048a/MagneticSensorAS5048A.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5048A sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5048A sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5048Error error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5048Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + + +## PreciseMagneticSensorAS5048A + +This is a variant of the sensor that uses [PreciseAngle](../utilities) to represent the angle, allowing a (much) greater range of rotation. + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp new file mode 100644 index 0000000..6d60000 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp @@ -0,0 +1,34 @@ +#include "./MagneticSensorAS5145.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5145::MagneticSensorAS5145(SPISettings settings) : settings(settings) { + +} + + +MagneticSensorAS5145::~MagneticSensorAS5145() { + +} + +void MagneticSensorAS5145::init(SPIClass* _spi) { + this->spi=_spi; + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorAS5145::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( (float)angle_data / AS5145_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorAS5145::readRawAngleSSI() { + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + //uint16_t parity = spi->transfer(0x00); + spi->endTransaction(); + return (value>>3)&0x1FFF; // TODO this isn't what I expected from the datasheet... maybe there's a leading 0 bit? +}; // 12bit angle value diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h new file mode 100644 index 0000000..ee1f34a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h @@ -0,0 +1,39 @@ + +#ifndef __MAGNETIC_SENSOR_AS5145_H__ +#define __MAGNETIC_SENSOR_AS5145_H__ + +#include "Arduino.h" +#include "SPI.h" +#include "common/base_classes/Sensor.h" + +#ifndef MSBFIRST +#define MSBFIRST BitOrder::MSBFIRST +#endif + +#define AS5145_BITORDER MSBFIRST +#define AS5145_CPR 4096.0f +#define _2PI 6.28318530718f + + +static SPISettings AS5145SSISettings(1000000, AS5145_BITORDER, SPI_MODE2); // @suppress("Invalid arguments") + + +class MagneticSensorAS5145 : public Sensor { +public: + MagneticSensorAS5145(SPISettings settings = AS5145SSISettings); + virtual ~MagneticSensorAS5145(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); + + uint16_t readRawAngleSSI(); + +private: + SPIClass* spi; + SPISettings settings; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/README.md new file mode 100644 index 0000000..f0474b9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5145/README.md @@ -0,0 +1,50 @@ +# AS5145 SimpleFOC driver + +SSI protocol driver for the AMS AS5145 magnetic encoder. Any of the A, B or H variants should work. AS5045 encoders should also be supported. + +Only angle reading is supported, might get to the status bits at a later time. +The SSI protocol is "emulated" using the SPI peripheral. + +Tested with AS5145A on STM32G491 so far. + +## Hardware setup + +Wire the sensor's data (DO) line to the MISO (CIPO) pin, nCS, SCK as normal. Leave the MOSI pin unconnected. + +## Software setup + +``` +#include +#include +#include +#include "encoders/as5145/MagneticSensorAS5145.h" + +MagneticSensorAS5145 sensor; +SPIClass spi_ssi(PB15, PB14, PB13, PB12); + +long ts; + +void setup() { + Serial.begin(115200); + while (!Serial) ; + delay(2000); + + Serial.println("Initializing sensor..."); + + spi_ssi.begin(); + sensor.init(&spi_ssi); + + Serial.println("Sensor initialized."); + + ts = millis(); +} + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp new file mode 100644 index 0000000..84e66e2 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp @@ -0,0 +1,175 @@ + + +#include "./AS5600.h" + + +AS5600::AS5600(uint8_t address) : _address(address) {}; +AS5600::~AS5600() {}; + +void AS5600::init(TwoWire* wire){ + _wire = wire; + _wire->begin(); + if (!closeTransactions) { + setAngleRegister(); + } +}; + + +void AS5600::setAngleRegister() { + _wire->beginTransmission(_address); + if (useHysteresis) + _wire->write(AS5600_REG_ANGLE); + else + _wire->write(AS5600_REG_ANGLE_RAW); + _wire->endTransmission(false); +} + + +uint16_t AS5600::angle() { + uint16_t result = 0; + if (!closeTransactions) { + setAngleRegister(); + } + _wire->requestFrom(_address, (uint8_t)2, (uint8_t)closeTransactions); + result = _wire->read()<<8; + result |= _wire->read(); + return result; +}; + + +uint16_t AS5600::readRawAngle() { + return readRegister(AS5600_REG_ANGLE_RAW, 2); +}; + + +uint16_t AS5600::readAngle() { + return readRegister(AS5600_REG_ANGLE, 2); +}; + + + +uint16_t AS5600::readMagnitude() { + return readRegister(AS5600_REG_MAGNITUDE, 2); +}; + + +AS5600Status AS5600::readStatus() { + AS5600Status result; + result.reg = (uint8_t)readRegister(AS5600_REG_STATUS, 1); // TODO: shift bits around + return result; +}; + + +uint8_t AS5600::readAGC() { + return (uint8_t)readRegister(AS5600_REG_AGC, 1); +}; + + + +AS5600Conf AS5600::readConf() { + AS5600Conf result; + result.reg = readRegister(AS5600_REG_CONF, 2); + return result; +}; + + +uint16_t AS5600::readMang() { + return readRegister(AS5600_REG_MANG, 2); +}; + + +uint16_t AS5600::readMPos() { + return readRegister(AS5600_REG_MPOS, 2); +}; + + +uint16_t AS5600::readZPos() { + return readRegister(AS5600_REG_ZPOS, 2); +}; + + +uint8_t AS5600::readZMCO() { + return (readRegister(AS5600_REG_ZMCO, 1)&0x03); +}; + +uint8_t AS5600::readI2CAddr() { + return (readRegister(AS5600_REG_I2CADDR, 1)>>1); +}; + + +// set registers +void AS5600::setConf(AS5600Conf value) { + // TODO: read before write + writeRegister(AS5600_REG_CONF, value.reg); +}; + + +void AS5600::setMang(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_MANG, value); +}; + + +void AS5600::setMPos(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_MPOS, value); +}; + + +void AS5600::setZPos(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_ZPOS, value); +}; + +void AS5600::setI2CAddr(uint8_t value) { + uint8_t val = (uint8_t)readRegister(AS5600_REG_I2CADDR, 1); + val = (value<<1) | (val&0x01); + writeRegister(AS5600_REG_I2CADDR, val); +}; + +void AS5600::setI2CUpdt(uint8_t value) { + uint8_t val = (uint8_t)readRegister(AS5600_REG_I2CUPDT, 1); + val = (value<<1) | (val&0x01); + writeRegister(AS5600_REG_I2CUPDT, val); +}; + + +void AS5600::burnSettings(){ + writeRegister(AS5600_REG_BURN, 0x40, 1); +} + + + +uint16_t AS5600::readRegister(uint8_t reg, uint8_t len){ + uint16_t result = 0; + _wire->beginTransmission(_address); + _wire->write(reg); + _wire->endTransmission(false); + _wire->requestFrom(_address, len, (uint8_t)closeTransactions); + if (!closeTransactions) { + setAngleRegister(); + } + result = _wire->read(); + if (len == 2) { + result <<= 8; + result |= _wire->read(); + } + return result; +}; + + + +void AS5600::writeRegister(uint8_t reg, uint16_t val, uint8_t len){ + _wire->beginTransmission(_address); + _wire->write(reg); + if (len == 2) { + _wire->write(val>>8); + } + _wire->write(val&0xFF); + _wire->endTransmission(closeTransactions); + if (!closeTransactions) { + setAngleRegister(); + } +}; + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.h new file mode 100644 index 0000000..8518c17 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/AS5600.h @@ -0,0 +1,104 @@ + +#pragma once + + +#include +#include + + +#define AS5600_REG_ZMCO 0x00 +#define AS5600_REG_ZPOS 0x01 +#define AS5600_REG_MPOS 0x03 +#define AS5600_REG_MANG 0x05 +#define AS5600_REG_CONF 0x07 + +#define AS5600_REG_I2CADDR 0x20 +#define AS5600_REG_I2CUPDT 0x21 + +#define AS5600_REG_ANGLE 0x0E +#define AS5600_REG_ANGLE_RAW 0x0C + +#define AS5600_REG_STATUS 0x0B +#define AS5600_REG_AGC 0x1A +#define AS5600_REG_MAGNITUDE 0x1B + +#define AS5600_REG_BURN 0xFF + +#define AS5600_CPR (4096.0f) + + +union AS5600Conf { + struct { + uint16_t pm:2; + uint16_t hyst:2; + uint16_t outs:2; + uint16_t pwmf:2; + uint16_t sf:2; + uint16_t fth:3; + uint16_t wd:1; + uint16_t unused:2; + }; + uint16_t reg; +}; + +union AS5600Status { + struct { + uint8_t unused:3; + uint8_t mh:1; + uint8_t ml:1; + uint8_t md:1; + uint8_t unused2:2; + }; + uint8_t reg; +}; + + + + +class AS5600 { +public: + AS5600(uint8_t address = 0x36); + ~AS5600(); + + virtual void init(TwoWire* wire = &Wire); + + // read an angle, either with or without hysteresis, depending on the useHysteresis flag + // and using fast mode (not closing transactions) if so configured + uint16_t angle(); + + // read registers + uint16_t readRawAngle(); + uint16_t readAngle(); + + uint16_t readMagnitude(); + AS5600Status readStatus(); + uint8_t readAGC(); + + AS5600Conf readConf(); + uint16_t readMang(); + uint16_t readMPos(); + uint16_t readZPos(); + uint8_t readZMCO(); + uint8_t readI2CAddr(); + + // set registers + void setConf(AS5600Conf value); + void setMang(uint16_t value); + void setMPos(uint16_t value); + void setZPos(uint16_t value); + void setI2CAddr(uint8_t value); + void setI2CUpdt(uint8_t value); + + void burnSettings(); + + bool closeTransactions = true; + bool useHysteresis = true; + uint8_t _address; +protected: + TwoWire* _wire; + + void setAngleRegister(); + uint16_t readRegister(uint8_t reg, uint8_t len); + void writeRegister(uint8_t reg, uint16_t val, uint8_t len = 2); +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp new file mode 100644 index 0000000..4cea5cf --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp @@ -0,0 +1,18 @@ + + +#include "./MagneticSensorAS5600.h" +#include "common/foc_utils.h" + +MagneticSensorAS5600::MagneticSensorAS5600(uint8_t _address) : AS5600(_address) {}; +MagneticSensorAS5600::~MagneticSensorAS5600() {}; + +void MagneticSensorAS5600::init(TwoWire* wire) { + AS5600::init(wire); + Sensor::init(); +}; + + +float MagneticSensorAS5600::getSensorAngle() { + uint16_t raw = readRawAngle(); + return raw / AS5600_CPR * _2PI; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h new file mode 100644 index 0000000..82a8419 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h @@ -0,0 +1,22 @@ + +#pragma once + + +#include "./AS5600.h" +#include "common/base_classes/Sensor.h" + + +class MagneticSensorAS5600 : public Sensor, public AS5600 { + +public: + MagneticSensorAS5600(uint8_t _address = 0x36); + ~MagneticSensorAS5600(); + + virtual void init(TwoWire* wire = &Wire); + + virtual float getSensorAngle() override; + +protected: + +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/README.md new file mode 100644 index 0000000..eb0f37a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/as5600/README.md @@ -0,0 +1,131 @@ +# AS5600 SimpleFOC driver + +I2C protocol driver for the AMS AS5600 magnetic encoder (digital potentiometer). Also supports the newer AS5600L variant. + +:warning: work in progress + +## Hardware setup + +Connect the sensor to 3.3V or 5V power as appropriate, and to I2C (SDA and SCL). + +Important: please make sure the direction pin (DIR) is either pulled up to VDD, or down to GND. Do not leave the direction pin floating. + +## Software setup + +The sensor driver is easy to use. + +```c++ +#include +#include +#include +#include "encoders/as5600/MagneticSensorAS5600.h" + +MagneticSensorAS5600 sensor; + +void setup() { + sensor.init(); +} + +long ts; + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` + +You can use a different I2C bus by passing a pointer to its TwoWire object in the init method: + +```c++ +MagneticSensorAS5600 sensor1; +MagneticSensorAS5600 sensor2; +TwoWire myI2C; + +void setup() { + + ... + + sensor1.init(&myI2C); + sensor2.init(&Wire2); +} +``` + +Using the sensor without releasing the bus should give you considerably more speed. Setting closeTransactions to false means the sensor will not release the bus between angle reads, and also will not re-write the register to the device, which boosts angle reading throughput significantly. + +```c++ +MagneticSensorAS5600 sensor; + +void setup() { + sensor.closeTransactions = false; + sensor.init(); +} +``` + +The sensor's other registers are exposed. Note that using the setter functions to set register values only performs a normal write, not a permanent programing of the register. Permanent programming (BURN function) is not supported by this driver. + +```c++ + +MagneticSensorAS5600 sensor; + +void setup() { + sensor.closeTransactions = false; + sensor.init(); + + uint16_t magnitude = sensor.readMagnitude(); + AS5600Status status = sensor.readStatus(); + // print the values or something +} + +``` + +The AS5600L has a default address of 0x40, and you can set the I2C address. To temporarily change the address (resets to default on restart): + +```c++ + +MagneticSensorAS5600 sensor(0x40); // default address of AS5600L is 0x40 + +void setup() { + Serial.begin(115200); + delay(2000); + + sensor.closeTransactions = true; // use normal transactons + sensor.init(); + sensor.setI2CAddr(0x41); // set address to 0x41 + sensor.setI2CUpdt(0x41); // i2c address is now 0x41 + sensor._address = 0x41; + delay(10); + // sensor is using new address + uint16_t magnitude = sensor.readMagnitude(); +} +``` + + +You can program an I2C address permanently (AS5600L only). The address will remain after reset. This programming operation can only be done once. + +```c++ + +MagneticSensorAS5600 sensor(0x40); // default address of AS5600L is 0x40 + +void setup() { + Serial.begin(115200); + delay(2000); + + sensor.closeTransactions = true; // use normal transactons + sensor.init(); + + uint8_t addr = sensor.readI2CAddr(); // read I2C address, should be 0x40 + Serial.print("Current Address: "); + Serial.println(addr); + + sensor.setI2CAddr(0x41); // set address to 0x41 + sensor.burnSettings(); // permanently set new address + Serial.println("Permanently changed address to 0x41. Restart the system."); + while (1); +} + + +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp new file mode 100644 index 0000000..e3d579c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp @@ -0,0 +1,257 @@ +#include "CalibratedSensor.h" + + +// CalibratedSensor() +// sensor - instance of original sensor object +CalibratedSensor::CalibratedSensor(Sensor& wrapped) : _wrapped(wrapped) +{ +}; + + +CalibratedSensor::~CalibratedSensor() +{ + delete calibrationLut; +}; + +// call update of calibrated sensor +void CalibratedSensor::update(){ + _wrapped.update(); + this->Sensor::update(); +}; + +// Retrieve the calibrated sensor angle +void CalibratedSensor::init() { + // assume wrapped sensor has already been initialized + this->Sensor::init(); // call superclass init +} + +// Retrieve the calibrated sensor angle +float CalibratedSensor::getSensorAngle(){ + // raw encoder position e.g. 0-2PI + float rawAngle = _wrapped.getMechanicalAngle(); + + // index of the bucket that rawAngle is part of. + // e.g. rawAngle = 0 --> bucketIndex = 0. + // e.g. rawAngle = 2PI --> bucketIndex = 128. + int bucketIndex = floor(rawAngle/(_2PI/n_lut)); + float remainder = rawAngle - ((_2PI/n_lut)*bucketIndex); + + // Extract the lower and upper LUT value in counts + float y0 = calibrationLut[bucketIndex]; + float y1 = calibrationLut[(bucketIndex+1)%n_lut]; + + // Linear Interpolation Between LUT values y0 and y1 using the remainder + // If remainder = 0, interpolated offset = y0 + // If remainder = 2PI/n_lut, interpolated offset = y1 + float interpolatedOffset = (((_2PI/n_lut)-remainder)/(_2PI/n_lut))*y0 + (remainder/(_2PI/n_lut))*y1; + + // add offset to the raw sensor count. Divide multiply by 2PI/CPR to get radians + float calibratedAngle = rawAngle+interpolatedOffset; + + // return calibrated angle in radians + return calibratedAngle; +} + +void CalibratedSensor::calibrate(BLDCMotor& motor){ + + Serial.println("Starting Sensor Calibration."); + + int _NPP = motor.pole_pairs; // number of pole pairs which is user input + const int n_ticks = 128*_NPP; // number of positions to be sampled per mechanical rotation. Multiple of NPP for filtering reasons (see later) + const int n2_ticks = 40; // increments between saved samples (for smoothing motion) + float deltaElectricalAngle = _2PI*_NPP/(n_ticks*n2_ticks); // Electrical Angle increments for calibration steps + float* error_f = new float[n_ticks](); // pointer to error array rotating forwards + // float* raw_f = new float[n_ticks](); // pointer to raw forward position + float* error_b = new float[n_ticks](); // pointer to error array rotating forwards + // float* raw_b = new float[n_ticks](); // pointer to raw backword position + float* error = new float[n_ticks](); // pointer to error array (average of forward & backward) + float* error_filt = new float[n_ticks](); // pointer to filtered error array (low pass filter) + const int window = 128; // window size for moving average filter of raw error + motor.zero_electric_angle = 0; // Set position sensor offset + + // find natural direction (this is copy of the init code) + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + motor.setPhaseVoltage(voltage_calibration, 0, angle); + _wrapped.update(); + _delay(2); + } + // take and angle in the middle + _wrapped.update(); + float mid_angle = _wrapped.getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + motor.setPhaseVoltage(voltage_calibration, 0, angle); + _wrapped.update(); + _delay(2); + } + _wrapped.update(); + float end_angle = _wrapped.getAngle(); + motor.setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + int directionSensor; + if (mid_angle < end_angle) { + Serial.println("MOT: sensor_direction==CCW"); + directionSensor = -1; + motor.sensor_direction = Direction::CCW; + + } else{ + Serial.println("MOT: sensor_direction==CW"); + directionSensor = 1; + motor.sensor_direction = Direction::CW; + + } + + //Set voltage angle to zero, wait for rotor position to settle + // keep the motor in position while getting the initial positions + motor.setPhaseVoltage(voltage_calibration, 0, elec_angle); + _delay(1000); + _wrapped.update(); + float theta_init = _wrapped.getAngle(); + float theta_absolute_init = _wrapped.getMechanicalAngle(); + + /* + Start Calibration + Loop over electrical angles from 0 to NPP*2PI, once forward, once backward + store actual position and error as compared to electrical angle + */ + + /* + forwards rotation + */ + Serial.println("Rotating forwards"); + int k = 0; + for(int i = 0; i n_ticks-1) { + ind -= n_ticks;} + error_filt[i] += error[ind]/(float)window; + } + mean += error_filt[i]/n_ticks; + } + + // calculate offset index + int index_offset = floor(raw_offset/(_2PI/n_lut)); + + // Build Look Up Table + for (int i = 0; i (n_lut-1)){ + ind -= n_lut; + } + if(ind < 0 ){ + ind += n_lut; + } + calibrationLut[ind] = (float) (error_filt[i*_NPP] - mean); + //Serial.print(ind); + //Serial.print('\t'); + //Serial.println(calibrationLut[ind],5); + _delay(1); + } + + // de-allocate memory + delete error_filt; + delete error; + // delete raw_b; + delete error_b; + // delete raw_f; + delete error_f; + + Serial.println("Sensor Calibration Done."); + +} + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h new file mode 100644 index 0000000..414c53a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h @@ -0,0 +1,61 @@ +#ifndef __CALIBRATEDSENSOR_H__ +#define __CALIBRATEDSENSOR_H__ + +#include "common/base_classes/Sensor.h" +#include "BLDCMotor.h" +#include "common/base_classes/FOCMotor.h" + + +class CalibratedSensor: public Sensor{ + +public: + // constructor of class with pointer to base class sensor and driver + CalibratedSensor(Sensor& wrapped); + ~CalibratedSensor(); + + /* + Override the update function + */ + virtual void update() override; + + /** + * Calibrate method computes the LUT for the correction + */ + virtual void calibrate(BLDCMotor& motor); + + // voltage to run the calibration: user input + float voltage_calibration = 1; + +protected: + + /** + * getSenorAngle() method of CalibratedSensor class. + * This should call getAngle() on the wrapped instance, and then apply the correction to + * the value returned. + */ + virtual float getSensorAngle() override; + /** + * init method of CaibratedSensor - call after calibration + */ + virtual void init() override; + /** + * delegate instance of Sensor class + */ + Sensor& _wrapped; + + // lut size, currently constan. Perhaps to be made variable by user? + const int n_lut { 128 } ; + // create pointer for lut memory + float* calibrationLut = new float[n_lut](); + + // Init inital angles + float theta_actual { 0.0 }; + float elecAngle { 0.0 }; + float elec_angle { 0.0 }; + float theta_absolute_post { 0.0 }; + float theta_absolute_init { 0.0 }; + float theta_init { 0.0 }; + float avg_elec_angle { 0.0 }; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/README.md new file mode 100644 index 0000000..5768d92 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/calibrated/README.md @@ -0,0 +1,91 @@ +# Calibrated Sensor + +by [@MarethyuPrefect](https://github.com/MarethyuPrefect) + +A SimpleFOC Sensor wrapper implementation which adds sensor eccentricity calibration. + +Please also see our [forum thread](https://community.simplefoc.com/t/simplefoc-sensor-eccentricity-calibration/2212) on this topic. + + +When you mount your (magnetic) sensor on your frame or motor, there will always be a slight misalignment between magnet and sensor (measurement system). This misalignment between center of rotation and the center of the sensor is called the eccentricity error. + +As a result your measurement system output is non-linear with respect to the rotor of the motor. This will cause an error with respect to the ideal torque you attempt to create with the I_q vector as function of the position. You could interpret this as a disturbance on your control loop which you want to minimize for optimal performance. + +This calibration compensates the sensor reading in a feed forward fashion such that your performance improves. + + +## Hardware setup + +Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without calibration first. Once things are working and tuned without sensor calibration, you can add the CalibratedSensor to see if you get an improvement. + +Note that during calibration, the motor is turned through several turns, and should be in an unloaded condition. Please ensure your hardware setup can support the motor rotating through full turns. + + +## Softwate setup + +The CalibratedSensor acts as a wrapper to the actual sensor class. When creating the CalibratedSensor object, provide the real +sensor to the constructor of CalibratedSensor. + +First, initialize the real sensor instance as normal. Then, call calibrate() on the CalibratedSensor instance. Then link the +CalibratedSensor to the motor and call motor.initFOC(). + +The motor will then use the calibrated sensor instance. + + +```c++ +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5048_SPI, PB6); +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// instantiate the calibrated sensor, providing the real sensor as a constructor argument +CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); + +void setup() { + sensor.init(); + // Link motor to sensor + motor.linkSensor(&sensor); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + + // set voltage to run calibration + sensor_calibrated.voltage_calibration = 6; + // Running calibration + sensor_calibrated.calibrate(motor); + + //Serial.println("Calibrating Sensor Done."); + // Linking sensor to motor object + motor.linkSensor(&sensor_calibrated); + + // calibrated init FOC + motor.initFOC(); +} +``` + +Please see the more complete [example](https://github.com/simplefoc/Arduino-FOC-drivers/blob/master/examples/encoders/calibrated/sensor_calibration.ino) in our examples directory. + + +## Roadmap + +Possible future improvements we've thought about: + +- Improve memory usage and performance +- Make calibration able to be saved/restored + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp new file mode 100644 index 0000000..6e5eac0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp @@ -0,0 +1,146 @@ +#include "MA330.h" + +MA330::MA330(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + +}; +MA330::~MA330() { + +}; + +void MA330::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } +}; + +float MA330::getCurrentAngle() { + return (readRawAngle() * _2PI)/MA330_CPR; +}; // angle in radians, return current value + +uint16_t MA330::readRawAngle() { + uint16_t angle = transfer16(0x0000); + return angle; +}; // 9-14bit angle value + +uint16_t MA330::getZero() { + uint16_t result = readRegister(MA330_REG_ZERO_POSITION_MSB)<<8; + result |= readRegister(MA330_REG_ZERO_POSITION_LSB); + return result; +}; +uint8_t MA330::getBiasCurrentTrimming() { + return readRegister(MA330_REG_BCT); +}; +bool MA330::isBiasCurrrentTrimmingX() { + return (readRegister(MA330_REG_ET) & 0x01)==0x01; +}; +bool MA330::isBiasCurrrentTrimmingY() { + return (readRegister(MA330_REG_ET) & 0x02)==0x02; +}; +uint16_t MA330::getPulsesPerTurn() { + uint16_t result = readRegister(MA330_REG_ILIP_PPT_LSB)>>6; + result |= ((uint16_t)readRegister(MA330_REG_PPT_MSB))<<2; + return result+1; +}; +uint8_t MA330::getIndexLength() { + return (readRegister(MA330_REG_ILIP_PPT_LSB)>>2)&0x0F; +}; +uint8_t MA330::getNumberPolePairs() { + return (readRegister(MA330_REG_NPP)>>5)&0x07;; +}; +uint8_t MA330::getRotationDirection() { + return (readRegister(MA330_REG_RD)>>7); +}; +uint8_t MA330::getFieldStrengthHighThreshold() { + return (readRegister(MA330_REG_MGLT_MGHT)>>2)&0x07; +}; +uint8_t MA330::getFieldStrengthLowThreshold() { + return (readRegister(MA330_REG_MGLT_MGHT)>>5)&0x07; +}; +uint8_t MA330::getFilterWidth() { + return readRegister(MA330_REG_FW); +}; +uint8_t MA330::getHysteresis() { + return readRegister(MA330_REG_HYS); +}; +FieldStrength MA330::getFieldStrength() { + return (FieldStrength)(readRegister(MA330_REG_MGH_MGL)>>6); +}; + + + +void MA330::setZero(uint16_t value) { + writeRegister(MA330_REG_ZERO_POSITION_MSB, value>>8); + writeRegister(MA330_REG_ZERO_POSITION_LSB, value&0x00FF); +}; +void MA330::setBiasCurrentTrimming(uint8_t value) { + writeRegister(MA330_REG_BCT, value); +}; +void MA330::setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled) { + uint8_t val = Xenabled ? 0x01 : 0x00; + val |= (Yenabled ? 0x02 : 0x00); + writeRegister(MA330_REG_ET, val); +}; +void MA330::setPulsesPerTurn(uint16_t value) { + uint16_t pptVal = value - 1; + writeRegister(MA330_REG_PPT_MSB, pptVal>>2); + uint8_t val = readRegister(MA330_REG_ILIP_PPT_LSB); + val &= 0x3F; + val |= (pptVal&0x03)<<6; + writeRegister(MA330_REG_ILIP_PPT_LSB, val); +}; +void MA330::setIndexLength(uint8_t value) { + uint8_t val = readRegister(MA330_REG_ILIP_PPT_LSB); + val &= 0xC0; + val |= ((value<<2)&0x3F); + writeRegister(MA330_REG_ILIP_PPT_LSB, val); +}; +void MA330::setNumberPolePairs(uint8_t value) { + uint8_t val = readRegister(MA330_REG_NPP); + val &= 0x1F; + val |= (value<<5); + writeRegister(MA330_REG_NPP, val); +}; +void MA330::setRotationDirection(uint8_t value) { + if (value==0) + writeRegister(MA330_REG_RD, 0x00); + else + writeRegister(MA330_REG_RD, 0x80); +}; +void MA330::setFilterWidth(uint8_t value) { + writeRegister(MA330_REG_FW, value); +}; +void MA330::setHysteresis(uint8_t value) { + writeRegister(MA330_REG_HYS, value); +}; +void MA330::setFieldStrengthThresholds(uint8_t high, uint8_t low) { + uint8_t val = (low<<5) | (high<<2); + writeRegister(MA330_REG_MGLT_MGHT, val); +}; + + +uint16_t MA330::transfer16(uint16_t outValue) { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return value; +}; +uint8_t MA330::readRegister(uint8_t reg) { + uint16_t cmd = 0x4000 | ((reg&0x001F)<<8); + uint16_t value = transfer16(cmd); + delayMicroseconds(1); + value = transfer16(0x0000); + return value>>8; +}; +uint8_t MA330::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x8000 | ((reg&0x1F)<<8) | value; + uint16_t result = transfer16(cmd); + delay(20); // 20ms delay required + result = transfer16(0x0000); + return result>>8; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.h new file mode 100644 index 0000000..4b89bc9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MA330.h @@ -0,0 +1,82 @@ +#ifndef __MA330_H__ +#define __MA330_H__ + + +#include "Arduino.h" +#include "SPI.h" + +enum FieldStrength : uint8_t { + FS_NORMAL = 0x00, + FS_LOW = 0x01, + FS_HIGH = 0x02, + FS_ERR = 0x03 // impossible state +}; + + +#define _2PI 6.28318530718f +#define MA330_CPR 65536.0f + +#define MA330_REG_ZERO_POSITION_LSB 0x00 +#define MA330_REG_ZERO_POSITION_MSB 0x01 +#define MA330_REG_BCT 0x02 +#define MA330_REG_ET 0x03 +#define MA330_REG_ILIP_PPT_LSB 0x04 +#define MA330_REG_PPT_MSB 0x05 +#define MA330_REG_MGLT_MGHT 0x06 +#define MA330_REG_NPP 0x07 +#define MA330_REG_RD 0x09 +#define MA330_REG_FW 0x0E +#define MA330_REG_HYS 0x10 +#define MA330_REG_MGH_MGL 0x1B + +#define MA330_BITORDER MSBFIRST + +static SPISettings MA330SPISettings(1000000, MA330_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + +class MA330 { +public: + MA330(SPISettings settings = MA330SPISettings, int nCS = -1); + virtual ~MA330(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 9-14bit angle value + + uint16_t getZero(); + uint8_t getBiasCurrentTrimming(); + bool isBiasCurrrentTrimmingX(); + bool isBiasCurrrentTrimmingY(); + uint16_t getPulsesPerTurn(); + uint8_t getIndexLength(); + uint8_t getNumberPolePairs(); + uint8_t getRotationDirection(); + uint8_t getFilterWidth(); + uint8_t getHysteresis(); + uint8_t getFieldStrengthHighThreshold(); + uint8_t getFieldStrengthLowThreshold(); + FieldStrength getFieldStrength(); + + void setZero(uint16_t); + void setBiasCurrentTrimming(uint8_t); + void setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled); + void setPulsesPerTurn(uint16_t); + void setIndexLength(uint8_t); + void setNumberPolePairs(uint8_t); + void setRotationDirection(uint8_t); + void setFilterWidth(uint8_t); + void setHysteresis(uint8_t); + void setFieldStrengthThresholds(uint8_t high, uint8_t low); + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + + uint16_t transfer16(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + uint8_t writeRegister(uint8_t reg, uint8_t value); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp new file mode 100644 index 0000000..a884065 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp @@ -0,0 +1,26 @@ +#include "./MagneticSensorMA330.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA330::MagneticSensorMA330(int nCS, SPISettings settings) : MA330(settings, nCS) { + +} + + +MagneticSensorMA330::~MagneticSensorMA330(){ + +} + + +void MagneticSensorMA330::init(SPIClass* _spi) { + this->MA330::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorMA330::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)MA330_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h new file mode 100644 index 0000000..1befbdf --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h @@ -0,0 +1,19 @@ +#ifndef __MAGNETIC_SENSOR_MA330_H__ +#define __MAGNETIC_SENSOR_MA330_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA330.h" + +class MagneticSensorMA330 : public Sensor, public MA330 { +public: + MagneticSensorMA330(int nCS = -1, SPISettings settings = MA330SPISettings); + virtual ~MagneticSensorMA330(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/README.md new file mode 100644 index 0000000..fcf635d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma330/README.md @@ -0,0 +1,66 @@ +# MA330 SimpleFOC driver + +While MA330 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this MA330-specific driver includes some optimisations: + +- access to the other registers of the MA330 +- this driver directly reads the angle with one call to SPI +- this will halve the number of 16-bit SPI transfers per simpleFOC loop iteration + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MA330/MagneticSensorMA330.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMA330 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorMA330 sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp new file mode 100644 index 0000000..7a1b7b8 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp @@ -0,0 +1,125 @@ +#include "MA730.h" + +MA730::MA730(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + +}; +MA730::~MA730() { + +}; + +void MA730::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } +}; + +float MA730::getCurrentAngle() { + return (readRawAngle() * _2PI)/MA730_CPR; +}; // angle in radians, return current value + +uint16_t MA730::readRawAngle() { + uint16_t angle = transfer16(0x0000); + return angle; +}; // 14bit angle value + +uint16_t MA730::getZero() { + uint16_t result = readRegister(MA730_REG_ZERO_POSITION_MSB)<<8; + result |= readRegister(MA730_REG_ZERO_POSITION_LSB); + return result; +}; +uint8_t MA730::getBiasCurrentTrimming() { + return readRegister(MA730_REG_BCT); +}; +bool MA730::isBiasCurrrentTrimmingX() { + return (readRegister(MA730_REG_ET) & 0x01)==0x01; +}; +bool MA730::isBiasCurrrentTrimmingY() { + return (readRegister(MA730_REG_ET) & 0x02)==0x02; +}; +uint16_t MA730::getPulsesPerTurn() { + uint16_t result = readRegister(MA730_REG_ILIP_PPT_LSB)>>6; + result |= ((uint16_t)readRegister(MA730_REG_PPT_MSB))<<2; + return result+1; +}; +uint8_t MA730::getIndexLength() { + return (readRegister(MA730_REG_ILIP_PPT_LSB)>>2)&0x0F; +}; +uint8_t MA730::getRotationDirection() { + return (readRegister(MA730_REG_RD)>>7); +}; +uint8_t MA730::getFieldStrengthHighThreshold() { + return (readRegister(MA730_REG_MGLT_MGHT)>>2)&0x07; +}; +uint8_t MA730::getFieldStrengthLowThreshold() { + return (readRegister(MA730_REG_MGLT_MGHT)>>5)&0x07; +}; +FieldStrength MA730::getFieldStrength() { + return (FieldStrength)(readRegister(MA730_REG_MGH_MGL)>>6); +}; + + + +void MA730::setZero(uint16_t value) { + writeRegister(MA730_REG_ZERO_POSITION_MSB, value>>8); + writeRegister(MA730_REG_ZERO_POSITION_LSB, value&0x00FF); +}; +void MA730::setBiasCurrentTrimming(uint8_t value) { + writeRegister(MA730_REG_BCT, value); +}; +void MA730::setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled) { + uint8_t val = Xenabled ? 0x01 : 0x00; + val |= (Yenabled ? 0x02 : 0x00); + writeRegister(MA730_REG_ET, val); +}; +void MA730::setPulsesPerTurn(uint16_t value) { + uint16_t pptVal = value - 1; + writeRegister(MA730_REG_PPT_MSB, pptVal>>2); + uint8_t val = readRegister(MA730_REG_ILIP_PPT_LSB); + val &= 0x3F; + val |= (pptVal&0x03)<<6; + writeRegister(MA730_REG_ILIP_PPT_LSB, val); +}; +void MA730::setIndexLength(uint8_t value) { + uint8_t val = readRegister(MA730_REG_ILIP_PPT_LSB); + val &= 0xC0; + val |= ((value<<2)&0x3F); + writeRegister(MA730_REG_ILIP_PPT_LSB, val); +}; +void MA730::setRotationDirection(uint8_t value) { + if (value==0) + writeRegister(MA730_REG_RD, 0x00); + else + writeRegister(MA730_REG_RD, 0x80); +}; +void MA730::setFieldStrengthThresholds(uint8_t high, uint8_t low) { + uint8_t val = (low<<5) | (high<<2); + writeRegister(MA730_REG_MGLT_MGHT, val); +}; + + +uint16_t MA730::transfer16(uint16_t outValue) { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return value; +}; +uint8_t MA730::readRegister(uint8_t reg) { + uint16_t cmd = 0x4000 | ((reg&0x001F)<<8); + uint16_t value = transfer16(cmd); + delayMicroseconds(1); + value = transfer16(0x0000); + return value>>8; +}; +uint8_t MA730::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x8000 | ((reg&0x1F)<<8) | value; + uint16_t result = transfer16(cmd); + delay(20); // 20ms delay required + result = transfer16(0x0000); + return result>>8; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.h new file mode 100644 index 0000000..56b0b5a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MA730.h @@ -0,0 +1,76 @@ +#ifndef __MA730_H__ +#define __MA730_H__ + + +#include "Arduino.h" +#include "SPI.h" + +enum FieldStrength : uint8_t { + FS_NORMAL = 0x00, + FS_LOW = 0x01, + FS_HIGH = 0x02, + FS_ERR = 0x03 // impossible state +}; + + +#define _2PI 6.28318530718f +#define MA730_CPR 65536.0f + +#define MA730_REG_ZERO_POSITION_LSB 0x00 +#define MA730_REG_ZERO_POSITION_MSB 0x01 +#define MA730_REG_BCT 0x02 +#define MA730_REG_ET 0x03 +#define MA730_REG_ILIP_PPT_LSB 0x04 +#define MA730_REG_PPT_MSB 0x05 +#define MA730_REG_MGLT_MGHT 0x06 +#define MA730_REG_RD 0x09 +#define MA730_REG_MGH_MGL 0x1B + +#define MA730_BITORDER MSBFIRST + +static SPISettings MA730SPISettings(1000000, MA730_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") +static SPISettings MA730SSISettings(4000000, MA730_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class MA730 { +public: + MA730(SPISettings settings = MA730SPISettings, int nCS = -1); + virtual ~MA730(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readRawAngleSSI(); // 14bit angle value + + uint16_t getZero(); + uint8_t getBiasCurrentTrimming(); + bool isBiasCurrrentTrimmingX(); + bool isBiasCurrrentTrimmingY(); + uint16_t getPulsesPerTurn(); + uint8_t getIndexLength(); + uint8_t getRotationDirection(); + uint8_t getFieldStrengthHighThreshold(); + uint8_t getFieldStrengthLowThreshold(); + FieldStrength getFieldStrength(); + + void setZero(uint16_t); + void setBiasCurrentTrimming(uint8_t); + void setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled); + void setPulsesPerTurn(uint16_t); + void setIndexLength(uint8_t); + void setRotationDirection(uint8_t); + void setFieldStrengthThresholds(uint8_t high, uint8_t low); + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + + uint16_t transfer16(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + uint8_t writeRegister(uint8_t reg, uint8_t value); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp new file mode 100644 index 0000000..320618e --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp @@ -0,0 +1,26 @@ +#include "./MagneticSensorMA730.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA730::MagneticSensorMA730(int nCS, SPISettings settings) : MA730(settings, nCS) { + +} + + +MagneticSensorMA730::~MagneticSensorMA730(){ + +} + + +void MagneticSensorMA730::init(SPIClass* _spi) { + this->MA730::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorMA730::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)MA730_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h new file mode 100644 index 0000000..d579657 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h @@ -0,0 +1,19 @@ +#ifndef __MAGNETIC_SENSOR_MA730_H__ +#define __MAGNETIC_SENSOR_MA730_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA730.h" + +class MagneticSensorMA730 : public Sensor, public MA730 { +public: + MagneticSensorMA730(int nCS = -1, SPISettings settings = MA730SPISettings); + virtual ~MagneticSensorMA730(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp new file mode 100644 index 0000000..bf87b2b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp @@ -0,0 +1,34 @@ +#include "./MagneticSensorMA730SSI.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA730SSI::MagneticSensorMA730SSI(SPISettings settings) : settings(settings) { + +} + + +MagneticSensorMA730SSI::~MagneticSensorMA730SSI() { + +} + +void MagneticSensorMA730SSI::init(SPIClass* _spi) { + this->spi=_spi; + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorMA730SSI::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( angle_data / (float)MA730_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorMA730SSI::readRawAngleSSI() { + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + //uint16_t parity = spi->transfer(0x00); + spi->endTransaction(); + return (value<<1); //>>1)&0x3FFF; +}; // 14bit angle value diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h new file mode 100644 index 0000000..f45071f --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h @@ -0,0 +1,25 @@ +#ifndef __MAGNETIC_SENSOR_MA730SSI_H__ +#define __MAGNETIC_SENSOR_MA730SSI_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA730.h" + +class MagneticSensorMA730SSI : public Sensor { +public: + MagneticSensorMA730SSI(SPISettings settings = MA730SSISettings); + virtual ~MagneticSensorMA730SSI(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); + + uint16_t readRawAngleSSI(); + +private: + SPIClass* spi; + SPISettings settings; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/README.md new file mode 100644 index 0000000..1f88290 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/ma730/README.md @@ -0,0 +1,74 @@ +# MA730 SimpleFOC driver + +While MA730 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this MA730-specific driver includes some optimisations: + +- access to the other registers of the MA730 +- this driver directly reads the angle with one call to SPI +- this will halve the number of 16-bit SPI transfers per simpleFOC loop iteration + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/ma730/MagneticSensorMA730.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMA730 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorMA730 sensor1(SENSOR1_CS, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(&SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // get the field strength + FieldStrength fs = sensor1.getFieldStrength(); + Serial.print("Field strength: "); + Serial.println(fs); + + // set pulses per turn for encoder mode + sensor1.setPulsesPerTurn(999); // set to 999 if we want 1000 PPR == 4000 CPR +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp new file mode 100644 index 0000000..01e8d97 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp @@ -0,0 +1,42 @@ +#include "./MagneticSensorMT6701SSI.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMT6701SSI::MagneticSensorMT6701SSI(int nCS, SPISettings settings) : settings(settings), nCS(nCS) { + +} + + +MagneticSensorMT6701SSI::~MagneticSensorMT6701SSI() { + +} + +void MagneticSensorMT6701SSI::init(SPIClass* _spi) { + this->spi=_spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } + this->spi->begin(); + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorMT6701SSI::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( angle_data / (float)MT6701_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorMT6701SSI::readRawAngleSSI() { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return (value>>MT6701_DATA_POS)&0x3FFF; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h new file mode 100644 index 0000000..32abd1d --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h @@ -0,0 +1,43 @@ +#ifndef __MAGNETIC_SENSOR_MT6701_SSI_H__ +#define __MAGNETIC_SENSOR_MT6701_SSI_H__ + +#include "Arduino.h" +#include "SPI.h" +#include "common/base_classes/Sensor.h" + + +#define MT6701_CPR 16384.0f + +#define MT6701_BITORDER MSBFIRST + +#if defined(TARGET_RP2040)||defined(ESP_H)||defined(CORE_TEENSY) +#define MT6701_DATA_POS 1 +#else +#define MT6701_DATA_POS 2 +#endif + +// Use SPI mode 2, capture on falling edge. First bit is not valid data, so have to read 25 bits to get a full SSI frame. +// SSI frame is 1 bit ignore, 14 bits angle, 4 bit status and 6 bit CRC. +static SPISettings MT6701SSISettings(1000000, MT6701_BITORDER, SPI_MODE2); // @suppress("Invalid arguments") + + + +class MagneticSensorMT6701SSI : public Sensor { +public: + MagneticSensorMT6701SSI(int nCS = -1, SPISettings settings = MT6701SSISettings); + virtual ~MagneticSensorMT6701SSI(); + + virtual void init(SPIClass* _spi = &SPI); + + float getSensorAngle() override; // angle in radians, return current value + +protected: + uint16_t readRawAngleSSI(); + + SPISettings settings; + SPIClass* spi; + int nCS = -1; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/README.md new file mode 100644 index 0000000..df04808 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6701/README.md @@ -0,0 +1,82 @@ +# MT6701 SimpleFOC driver + +:warning: work in progress... SSI driver is working. I2C not yet complete. + +Due to the peculiarities of its interfaces, this very versatile sensor is not directly supported by SimpleFOC's SPI or I2C magnetic sensor implementations. This folder contains dedicated SimpleFOC sensor drivers for the MT6701 for I2C and SSI. + +Note: the ABZ, UVW and Analog outputs of this sensor are supported by the standard SimpleFOC encoder, hall-sensor or analog sensor classes respectively. + +:warning: Note: the I2C output of this sensor is probably too slow for high performance motor control, but could be useful to program the sensor IC, and to read the absolute angle values for intializing ABZ or UVW modes. + +## Hardware setup + +For I2C, connect the sensor as normal for I2C, using the SDA and SCL lines. + +:warning: Note: to program the sensor via I2C, it has to be operated at 5V. + +For SSI, connect the CSN line to the nCS output of your MCU, the CLK line to the SCLK output of the MCU, and the DO line to the CIPO (MISO) input of the MCU. + +In my tests, the sensor was not able to work correctly together with other SPI devices on the same bus, but your experience might differ from mine. + +## Software setup + +### SSI code sample + +```c++ +#include "Arduino.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MT6701/MagneticSensorMT6701SSI.h" + + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMT6701SSI sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +To use a custom SPI bus: + +```c++ +void setup() { + sensor1.init(&SPI2); +} +``` + + + +### I2C code sample + +I2C usage is quite simple: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MT6701/MagneticSensorMT6701I2C.h" + +MagneticSensorMT6701I2C sensor1(); + + +void setup() { + sensor1.init(); +} + +``` + +If you've programmed a different I2C address or want to use a different I2C bus you can: + +```c++ +#define I2C_ADDR 0x70 +MagneticSensorMT6701I2C sensor1(I2C_ADDR); + + +void setup() { + sensor1.init(&Wire2); +} +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp new file mode 100644 index 0000000..d874bad --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp @@ -0,0 +1,56 @@ + +#include "MT6816.h" + +MT6816::MT6816(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { +}; + +MT6816::~MT6816() { +}; + +void MT6816::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + spi->begin(); + } +}; + +uint16_t MT6816::readRawAngle() { + uint16_t angle_data = 0; + angle_data = spi_transfer16(MT6816_READ_REG_03) << 8; + angle_data |= spi_transfer16(MT6816_READ_REG_04); + + if ((angle_data & MT6816_NO_MAGNET_WARNING_BIT) == MT6816_NO_MAGNET_WARNING_BIT) { + this->no_magnetic_reading = true; + } else { + this->no_magnetic_reading = false; + } + + if (!this->parityCheck(angle_data)) { + return 0; + } + + return (angle_data >> 2); +} + +bool MT6816::parityCheck(uint16_t data) { + data ^= data >> 8; + data ^= data >> 4; + data ^= data >> 2; + data ^= data >> 1; + + return (~data) & 1; +} + +uint16_t MT6816::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + + return result; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h new file mode 100644 index 0000000..68b448a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h @@ -0,0 +1,40 @@ + +#ifndef MT6816_H +#define MT6816_H + +#include "Arduino.h" +#include "SPI.h" + +#define _2PI 6.28318530718f +#define MT6816_CPR 16384.0f + +#define MT6816_READ_REG_03 0x8300 +#define MT6816_READ_REG_04 0x8400 + +#define MT6816_NO_MAGNET_WARNING_BIT 0x0002 +#define MT6816_BITORDER MSBFIRST + + +static SPISettings MT6816SPISettings(1000000, MT6816_BITORDER, SPI_MODE3); + +class MT6816 { +public: + MT6816(SPISettings settings = MT6816SPISettings, int nCS = -1); + virtual ~MT6816(); + + virtual void init(SPIClass* _spi = &SPI); + uint16_t readRawAngle(); + bool isNoMagneticReading() { + return no_magnetic_reading; + } + +private: + bool parityCheck(uint16_t data); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool no_magnetic_reading = false; + int nCS = -1; +}; + +#endif /* MT6816_H */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp new file mode 100644 index 0000000..4167d78 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp @@ -0,0 +1,26 @@ + +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "MagneticSensorMT6816.h" + + +MagneticSensorMT6816::MagneticSensorMT6816(int nCS, SPISettings settings) : MT6816(settings, nCS) { +} + +MagneticSensorMT6816::~MagneticSensorMT6816(){ +} + +void MagneticSensorMT6816::init(SPIClass* _spi) { + this->MT6816::init(_spi); + this->Sensor::init(); +} + +float MagneticSensorMT6816::getSensorAngle() { + uint16_t raw_angle_data = readRawAngle(); + + if (this->MT6816::isNoMagneticReading()) { + return 0; + } + + return static_cast(raw_angle_data) / MT6816_CPR * _2PI; +} \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h new file mode 100644 index 0000000..51628fc --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h @@ -0,0 +1,17 @@ + +#ifndef MAGNETICSENSOR_MT6816_H +#define MAGNETICSENSOR_MT6816_H + +#include "common/base_classes/Sensor.h" +#include "MT6816.h" + +class MagneticSensorMT6816 : public Sensor, public MT6816 { +public: + MagneticSensorMT6816(int nCS = -1, SPISettings settings = MT6816SPISettings); + virtual ~MagneticSensorMT6816(); + + virtual float getSensorAngle() override; + virtual void init(SPIClass* _spi = &SPI); +}; + +#endif /* MAGNETICSENSOR_MT6816_H */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp new file mode 100644 index 0000000..aeaa200 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp @@ -0,0 +1,278 @@ + +#include "./MT6835.h" +#include "common/foc_utils.h" + + +MT6835::MT6835(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + +MT6835::~MT6835() { + // nix +}; + + + +void MT6835::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } + spi->begin(); +}; + + + + +float MT6835::getCurrentAngle(){ + return readRawAngle21() / (float)MT6835_CPR * _2PI; +}; + + + +uint32_t MT6835::readRawAngle21(){ + uint8_t data[6]; // transact 48 bits + data[0] = (MT6835_OP_ANGLE<<4); + data[1] = MT6835_REG_ANGLE1; + data[2] = 0; + data[3] = 0; + data[4] = 0; + data[5] = 0; + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + spi->transfer(data, 6); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + laststatus = data[4]&0x07; + return (data[2] << 13) | (data[3] << 5) | (data[4] >> 3); +}; + + +uint8_t MT6835::getStatus(){ + return laststatus; +}; + + +bool MT6835::setZeroFromCurrentPosition(){ + MT6835Command cmd; + cmd.cmd = MT6835_OP_ZERO; + cmd.addr = 0x000; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; + + +/** + * Wait 6s after calling this method + */ +bool MT6835::writeEEPROM(){ + delay(1); // wait at least 1ms + MT6835Command cmd; + cmd.cmd = MT6835_OP_PROG; + cmd.addr = 0x000; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; + + + + + +uint8_t MT6835::getBandwidth(){ + MT6835Options5 opts = { .reg = readRegister(MT6835_REG_OPTS5) }; + return opts.bw; +}; +void MT6835::setBandwidth(uint8_t bw){ + MT6835Options5 opts = { .reg = readRegister(MT6835_REG_OPTS5) }; + opts.bw = bw; + writeRegister(MT6835_REG_OPTS5, opts.reg); +}; + +uint8_t MT6835::getHysteresis(){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + return opts.hyst; +}; +void MT6835::setHysteresis(uint8_t hyst){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + opts.hyst = hyst; + setOptions3(opts); +}; + +uint8_t MT6835::getRotationDirection(){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + return opts.rot_dir; +}; +void MT6835::setRotationDirection(uint8_t dir){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + opts.rot_dir = dir; + setOptions3(opts); +}; + + +uint16_t MT6835::getABZResolution(){ + uint8_t hi = readRegister(MT6835_REG_ABZ_RES1); + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return (hi << 6) | lo.abz_res_low; +}; +void MT6835::setABZResolution(uint16_t res){ + uint8_t hi = (res >> 6); + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.abz_res_low = (res & 0x3F); + writeRegister(MT6835_REG_ABZ_RES1, hi); + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +bool MT6835::isABZEnabled(){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return lo.abz_off==0; +}; +void MT6835::setABZEnabled(bool enabled){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.abz_off = enabled?0:1; + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +bool MT6835::isABSwapped(){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return lo.ab_swap==1; +}; +void MT6835::setABSwapped(bool swapped){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.ab_swap = swapped?1:0; + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +uint16_t MT6835::getZeroPosition(){ + uint8_t hi = readRegister(MT6835_REG_ZERO1); + MT6835Options0 lo = { + .reg = readRegister(MT6835_REG_ZERO2) + }; + return (hi << 4) | lo.zero_pos_low; +}; +void MT6835::setZeroPosition(uint16_t pos){ + uint8_t hi = (pos >> 4); + MT6835Options0 lo = { + .reg = readRegister(MT6835_REG_ZERO2) + }; + lo.zero_pos_low = pos & 0x0F; + writeRegister(MT6835_REG_ZERO1, hi); + writeRegister(MT6835_REG_ZERO2, lo.reg); +}; + + + +MT6835Options1 MT6835::getOptions1(){ + MT6835Options1 result = { + .reg = readRegister(MT6835_REG_OPTS1) + }; + return result; +}; +void MT6835::setOptions1(MT6835Options1 opts){ + writeRegister(MT6835_REG_OPTS1, opts.reg); +}; + + + +MT6835Options2 MT6835::getOptions2(){ + MT6835Options2 result = { + .reg = readRegister(MT6835_REG_OPTS2) + }; + return result; +}; +void MT6835::setOptions2(MT6835Options2 opts){ + MT6835Options2 val = getOptions2(); + val.nlc_en = opts.nlc_en; + val.pwm_fq = opts.pwm_fq; + val.pwm_pol = opts.pwm_pol; + val.pwm_sel = opts.pwm_sel; + writeRegister(MT6835_REG_OPTS2, val.reg); +}; + + + +MT6835Options3 MT6835::getOptions3(){ + MT6835Options3 result = { + .reg = readRegister(MT6835_REG_OPTS3) + }; + return result; +}; +void MT6835::setOptions3(MT6835Options3 opts){ + MT6835Options3 val = getOptions3(); + val.rot_dir = opts.rot_dir; + val.hyst = opts.hyst; + writeRegister(MT6835_REG_OPTS3, val.reg); +}; + + + +MT6835Options4 MT6835::getOptions4(){ + MT6835Options4 result = { + .reg = readRegister(MT6835_REG_OPTS4) + }; + return result; +}; +void MT6835::setOptions4(MT6835Options4 opts){ + MT6835Options4 val = getOptions4(); + val.gpio_ds = opts.gpio_ds; + val.autocal_freq = opts.autocal_freq; + writeRegister(MT6835_REG_OPTS4, val.reg); +}; + + + +uint32_t swap_bytes(uint32_t net) +{ + return __builtin_bswap32(net); +} + + + + + +void MT6835::transfer24(MT6835Command* outValue) { + uint32_t buff = swap_bytes(outValue->val); + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + spi->transfer(&buff, 3); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + outValue->val = swap_bytes(buff); +}; +uint8_t MT6835::readRegister(uint16_t reg) { + MT6835Command cmd; + cmd.cmd = MT6835_OP_READ; + cmd.addr = reg; + transfer24(&cmd); + return cmd.data; +}; +bool MT6835::writeRegister(uint16_t reg, uint8_t value) { + MT6835Command cmd; + cmd.cmd = MT6835_OP_WRITE; + cmd.addr = reg; + cmd.data = value; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h new file mode 100644 index 0000000..5751901 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h @@ -0,0 +1,215 @@ + + +#pragma once + + + +#include "Arduino.h" +#include "SPI.h" + +#define MT6835_OP_READ 0b0011 +#define MT6835_OP_WRITE 0b0110 +#define MT6835_OP_PROG 0b1100 +#define MT6835_OP_ZERO 0b0101 +#define MT6835_OP_ANGLE 0b1010 + +#define MT6835_CMD_MASK 0b111100000000000000000000 +#define MT6835_ADDR_MASK 0b000011111111111100000000 +#define MT6835_DATA_MASK 0b000000000000000011111111 + +#define MT6835_CPR 2097152 + +#define MT6835_STATUS_OVERSPEED 0x01 +#define MT6835_STATUS_WEAKFIELD 0x02 +#define MT6835_STATUS_UNDERVOLT 0x04 + +#define MT6835_WRITE_ACK 0x55 + +#define MT6835_REG_USERID 0x001 + +#define MT6835_REG_ANGLE1 0x003 +#define MT6835_REG_ANGLE2 0x004 +#define MT6835_REG_ANGLE3 0x005 +#define MT6835_REG_ANGLE4 0x006 + +#define MT6835_REG_ABZ_RES1 0x007 +#define MT6835_REG_ABZ_RES2 0x008 + +#define MT6835_REG_ZERO1 0x009 +#define MT6835_REG_ZERO2 0x00A + +#define MT6835_REG_OPTS0 0x00A +#define MT6835_REG_OPTS1 0x00B +#define MT6835_REG_OPTS2 0x00C +#define MT6835_REG_OPTS3 0x00D +#define MT6835_REG_OPTS4 0x00E +#define MT6835_REG_OPTS5 0x011 + +// NLC table, 192 bytes +#define MT6835_REG_NLC_BASE 0x013 + +#define MT6835_REG_CAL_STATUS 0x113 + + +union MT6835ABZRes { + struct { + uint8_t ab_swap:1; + uint8_t abz_off:1; + uint8_t abz_res_low:6; + }; + uint8_t reg; +}; + + + +union MT6835Options0 { + struct { + uint8_t z_pul_wid:3; + uint8_t z_edge:1; + uint8_t zero_pos_low:4; + }; + uint8_t reg; +}; + + + +union MT6835Options1 { + struct { + uint8_t uvw_res:4; + uint8_t uvw_off:1; + uint8_t uvw_mux:1; + uint8_t z_phase:2; + }; + uint8_t reg; +}; + + + +union MT6835Options2 { + struct { + uint8_t pwm_sel:3; + uint8_t pwm_pol:1; + uint8_t pwm_fq:1; + uint8_t nlc_en:1; + uint8_t reserved:2; + }; + uint8_t reg; +}; + + + +union MT6835Options3 { + struct { + uint8_t hyst:3; + uint8_t rot_dir:1; + uint8_t reserved:4; + }; + uint8_t reg; +}; + + + +union MT6835Options4 { + struct { + uint8_t reserved:4; + uint8_t autocal_freq:3; + uint8_t gpio_ds:1; + }; + uint8_t reg; +}; + + + +union MT6835Options5 { + struct { + uint8_t bw:3; + uint8_t reserved:5; + }; + uint8_t reg; +}; + + + + +union MT6835Command { + struct { + uint32_t unused:8; + uint32_t data:8; + uint32_t addr:12; + uint32_t cmd:4; + }; + uint32_t val; +}; + + + + +#define MT6835_BITORDER MSBFIRST + +static SPISettings MT6835SPISettings(1000000, MT6835_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + + + + +class MT6835 { +public: + MT6835(SPISettings settings = MT6835SPISettings, int nCS = -1); + virtual ~MT6835(); + + virtual void init(SPIClass* _spi = &SPI); + + + float getCurrentAngle(); // angle in radians, return current value + + uint32_t readRawAngle21(); // up to 21bit precision angle value + + + uint8_t getBandwidth(); + void setBandwidth(uint8_t bw); + + uint8_t getHysteresis(); + void setHysteresis(uint8_t hyst); + + uint8_t getRotationDirection(); + void setRotationDirection(uint8_t dir); + + uint16_t getABZResolution(); + void setABZResolution(uint16_t res); + + bool isABZEnabled(); + void setABZEnabled(bool enabled); + + bool isABSwapped(); + void setABSwapped(bool swapped); + + uint16_t getZeroPosition(); + void setZeroPosition(uint16_t pos); + + MT6835Options1 getOptions1(); + void setOptions1(MT6835Options1 opts); + + MT6835Options2 getOptions2(); + void setOptions2(MT6835Options2 opts); + + MT6835Options3 getOptions3(); + void setOptions3(MT6835Options3 opts); + + MT6835Options4 getOptions4(); + void setOptions4(MT6835Options4 opts); + + uint8_t getStatus(); + + bool setZeroFromCurrentPosition(); + bool writeEEPROM(); // wait 6s after calling this method + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + uint8_t laststatus = 0; + + void transfer24(MT6835Command* outValue); + uint8_t readRegister(uint16_t reg); + bool writeRegister(uint16_t reg, uint8_t value); + +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp new file mode 100644 index 0000000..73c8bbc --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp @@ -0,0 +1,24 @@ + +#include "MagneticSensorMT6835.h" + + +MagneticSensorMT6835::MagneticSensorMT6835(int nCS, SPISettings settings) : Sensor(), MT6835(settings, nCS) { + // nix +}; + + +MagneticSensorMT6835::~MagneticSensorMT6835() { + // nix +}; + + +float MagneticSensorMT6835::getSensorAngle() { + return getCurrentAngle(); +}; + + +void MagneticSensorMT6835::init(SPIClass* _spi) { + this->MT6835::init(_spi); + this->Sensor::init(); +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h new file mode 100644 index 0000000..e67ab0a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h @@ -0,0 +1,17 @@ + +#pragma once + +#include "common/base_classes/Sensor.h" +#include "./MT6835.h" + +class MagneticSensorMT6835 : public Sensor, public MT6835 { +public: + MagneticSensorMT6835(int nCS = -1, SPISettings settings = MT6835SPISettings); + virtual ~MagneticSensorMT6835(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/README.md new file mode 100644 index 0000000..66cbb33 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/mt6835/README.md @@ -0,0 +1,64 @@ +# MT6835 SimpleFOC driver + +Driver for the MagnTek MT6835 precision magnetic rotary encoder. + +This sensor features support for up to 21 bit resolution (!) and speeds up to 120,000RPM. While its full precision requires calibration using an external calibration system, it is impressively precise even in uncalibrated state, and it offers an internal self-calibration mode whose precision is somewhere between the other two. + +It has ABZ, UVW and SPI interfaces, and this driver is for its SPI inteface. You can use its ABZ interface with our Encoder classes, but due to the high resolution the interrupt-based Encoder might cause high MCU load. You can use its UVW interface with our Hall Sensor classes. + +# Hardware setup + +Connect the sensor to an SPI bus on your MCU. Pay attention to the voltage levels needed by your PCBs. The nCS line should have a pull-up resistor. + +# Software setup + +Usage example: + +```c++ +#include + +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" + +#include "encoders/mt6835/MagneticSensorMT6835.h" + +#define SENSOR_nCS PB6 + +SPISettings myMT6835SPISettings(1000000, MT6835_BITORDER, SPI_MODE3); +MagneticSensorMT6835 sensor = MagneticSensorMT6835(SENSOR_nCS, myMT6835SPISettings); + +long ts; + +void setup() { + sensor.init(); + ts = millis(); +} + +void loop() { + sensor.update(); + long now = millis(); + if (now - ts > 1000) { + ts = now; + SimpleFOCDebug::print("A: "); + SimpleFOCDebug::print(sensor.getAngle()); + SimpleFOCDebug::print(" V: "); + SimpleFOCDebug::println(sensor.getVelocity()); + } + delay(10); +} + +``` + +Set the zero position: + +```c++ +uint16_t pos = sensor.getZeroPosition(); // current value +sensor.setZeroFromCurrentPosition(); // set zero to current position +``` + +Set the ABZ resolution (needed if you want to use ABZ as the default is 1): + +```c++ +sensor.setABZResolution(2048); +``` + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp new file mode 100644 index 0000000..a127358 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp @@ -0,0 +1,24 @@ + +#include "./MagneticSensorSC60228.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorSC60228::MagneticSensorSC60228(int nCS, SPISettings settings) : SC60228(settings, nCS){ + // nix +}; +MagneticSensorSC60228::~MagneticSensorSC60228(){ }; + + + +float MagneticSensorSC60228::getSensorAngle(){ + SC60228Angle angle_data = readRawAngle(); + float result = ( angle_data.angle / (float)SC60228_CPR ) * _2PI; + return result; +}; + + + +void MagneticSensorSC60228::init(SPIClass* _spi){ + this->SC60228::init(_spi); + this->Sensor::init(); +}; \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h new file mode 100644 index 0000000..7720182 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h @@ -0,0 +1,20 @@ +#ifndef __MAGNETICSENSORSC60228_H__ +#define __MAGNETICSENSORSC60228_H__ + + +#include "common/base_classes/Sensor.h" +#include "./SC60228.h" + + +class MagneticSensorSC60228 : public Sensor, public SC60228 { +public: + MagneticSensorSC60228(int nCS = -1, SPISettings settings = SC60228SPISettings); + virtual ~MagneticSensorSC60228(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI) override; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/README.md new file mode 100644 index 0000000..49f7901 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/README.md @@ -0,0 +1,47 @@ +# SC60228 SimpleFOC driver + +Driver for the Semiment 12bit magnetic encoder IC SC60288. According to specs it should support 12 bit accuracy, 10 bit effective accuracy and up to 20kRPM. + +Link to [Datasheet](https://semiment.com/wp-content/uploads/2021/04/SC60228_EN-VA1.0.pdf) + +The encoder and this driver were tested with a small gimbal motor and MKR1000 (SAMD21 MCU). + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/sc60228/MagneticSensorSC60228.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorSC60228 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some SPI options: + +```c++ +SPISettings mySPISettings(1000000, SC60228_BITORDER, SPI_MODE0); // lower speed to 1Mhz +MagneticSensorSC60228 sensor1(SENSOR1_CS, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp new file mode 100644 index 0000000..4c63920 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp @@ -0,0 +1,57 @@ + +#include "./SC60228.h" + + +SC60228::SC60228(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + +SC60228::~SC60228() { }; + +void SC60228::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + spi->begin(); + readRawAngle(); +}; + + + +SC60228Angle SC60228::readRawAngle(){ + SC60228Angle result; + result.reg = spi_transfer16(0x0000); + errorflag = (result.err==1); + // TODO check parity + // Serial.print("0x"); + // Serial.print(result.angle, HEX); + // Serial.print(" - 0x"); + // Serial.println(result.reg, HEX); + return result; +}; + + + +bool SC60228::isError() { + return errorflag; +}; + + + +uint16_t SC60228::spi_transfer16(uint16_t outdata){ + uint16_t result; + if (nCS>=0) + digitalWrite(nCS, LOW); + // min delay here: 250ns + spi->beginTransaction(settings); + result = spi->transfer16(outdata); + // min delay here: clock period / 2 + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, HIGH); + // min delay until next read: 250ns + return result; +}; + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h new file mode 100644 index 0000000..03b4e50 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h @@ -0,0 +1,50 @@ + +#ifndef __SC60228_H__ +#define __SC60228_H__ + +#include "Arduino.h" +#include "SPI.h" + + +typedef union { + struct { + uint16_t parity:1; + uint16_t df2:1; + uint16_t df1:1; + uint16_t err:1; + uint16_t angle:12; + }; + uint16_t reg; +} SC60228Angle; + + +#define SC60228_CPR 4096 +#define SC60228_BITORDER MSBFIRST + +static SPISettings SC60228SPISettings(8000000, SC60228_BITORDER, SPI_MODE0); // @suppress("Invalid arguments") + + + +class SC60228 { +public: + SC60228(SPISettings settings = SC60228SPISettings, int nCS = -1); + virtual ~SC60228(); + + virtual void init(SPIClass* _spi = &SPI); + + SC60228Angle readRawAngle(); // 12bit angle value + error flag + bool isError(); // true if the last call to readRawAngle() returned an error + +protected: + uint16_t spi_transfer16(uint16_t outdata); + + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/README.md new file mode 100644 index 0000000..733284c --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/README.md @@ -0,0 +1,69 @@ +# Smoothing Sensor + +by [@dekutree64](https://github.com/dekutree64) + +A SimpleFOC Sensor wrapper implementation which adds angle extrapolation + +Please also see our [forum thread](https://community.simplefoc.com/t/smoothingsensor-experimental-sensor-angle-extrapoltion/3105) on this topic. + + +SmoothingSensor is a wrapper class which is inserted inbetween a sensor and motor to provide better quality angle readings from sensors that are low resolution or are slow to communicate. It provides no improvement for sensors that are high resolution and quick to update. It uses the timestamp of the last angle reading from the sensor and the low-pass filtered velocity from the motor to predict what the angle should be at the current time. + + +## Hardware setup + +Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without smoothing first. Once things are working and tuned without smoothing, you can add the SmoothingSensor to see if you get an improvement. + + +## Softwate setup + +The SmoothingSensor acts as a wrapper to the actual sensor class. When creating the SmoothingSensor object, provide the real sensor to the constructor of SmoothingSensor. Initialize the real sensor instance as normal. Then link the SmoothingSensor to the motor and call motor.initFOC(). + + +```c++ +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// real sensor instance +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +// instantiate the smoothing sensor, providing the real sensor as a constructor argument +SmoothingSensor smooth = SmoothingSensor(sensor, motor); + +void doPWM(){sensor.handlePWM();} + +void setup() { + sensor.init(); + sensor.enableInterrupt(doPWM); + // Link motor to sensor + motor.linkSensor(&smooth); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + motor.initFOC(); +} +``` + + +## Roadmap + +Possible future improvements we've thought about: + +- Add extrapolation of acceleration as well +- Switch to open loop control at very low speed with hall sensors, which otherwise move in steps even with smoothing. + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp new file mode 100644 index 0000000..ef0d322 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp @@ -0,0 +1,57 @@ +#include "SmoothingSensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + + +SmoothingSensor::SmoothingSensor(Sensor& s, const FOCMotor& m) : _wrapped(s), _motor(m) +{ +} + + +void SmoothingSensor::update() { + // Update sensor, with optional downsampling of update rate + if(sensor_cnt++ >= sensor_downsample) { + sensor_cnt = 0; + _wrapped.update(); + } + + // Copy state variables from the sensor + angle_prev = _wrapped.angle_prev; + angle_prev_ts = _wrapped.angle_prev_ts; + full_rotations = _wrapped.full_rotations; + + // Perform angle prediction, using low-pass filtered velocity. But don't advance more than + // pi/3 (equivalent to one step of block commutation) from the last true angle reading. + float dt = (_micros() - angle_prev_ts) * 1e-6f; + angle_prev += _motor.sensor_direction * _constrain(_motor.shaft_velocity * dt, -_PI_3 / _motor.pole_pairs, _PI_3 / _motor.pole_pairs); + + // Apply phase correction if needed + if (phase_correction != 0) { + if (_motor.shaft_velocity < -0) angle_prev -= _motor.sensor_direction * phase_correction / _motor.pole_pairs; + else if (_motor.shaft_velocity > 0) angle_prev += _motor.sensor_direction * phase_correction / _motor.pole_pairs; + } + + // Handle wraparound of the projected angle + if (angle_prev < 0) full_rotations -= 1, angle_prev += _2PI; + else if (angle_prev >= _2PI) full_rotations += 1, angle_prev -= _2PI; +} + + +float SmoothingSensor::getVelocity() { + return _wrapped.getVelocity(); +} + + +int SmoothingSensor::needsSearch() { + return _wrapped.needsSearch(); +} + + +float SmoothingSensor::getSensorAngle() { + return _wrapped.getSensorAngle(); +} + + +void SmoothingSensor::init() { + _wrapped.init(); +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h new file mode 100644 index 0000000..c10c2a0 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h @@ -0,0 +1,46 @@ +#ifndef SMOOTHING_SENSOR_H +#define SMOOTHING_SENSOR_H + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/Sensor.h" + +/** + SmoothingSensor is a wrapper class which is inserted inbetween a sensor and motor to provide better + quality angle readings from sensors that are low resolution or are slow to communicate. It provides + no improvement for sensors that are high resolution and quick to update. + It uses the timestamp of the last angle reading from the sensor and the low-pass filtered velocity + from the motor to predict what the angle should be at the current time. +*/ + +class SmoothingSensor : public Sensor +{ + public: + /** + SmoothingSensor class constructor + @param s Wrapped sensor + @param m Motor that the SmoothingSensor will be linked to + */ + SmoothingSensor(Sensor& s, const FOCMotor& m); + + void update() override; + float getVelocity() override; + int needsSearch() override; + + // For sensors with slow communication, use these to poll less often + unsigned int sensor_downsample = 0; // parameter defining the ratio of downsampling for sensor update + unsigned int sensor_cnt = 0; // counting variable for downsampling + + // For hall sensors, the predicted angle is always 0 to 60 electrical degrees ahead of where it would be without + // smoothing, so offset it backward by 30 degrees (set this to -PI_6) to get the average in phase with the rotor + float phase_correction = 0; + + protected: + float getSensorAngle() override; + void init() override; + + Sensor& _wrapped; + const FOCMotor& _motor; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md new file mode 100644 index 0000000..1bdb1ca --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md @@ -0,0 +1,49 @@ +# SimpleFOC Driver for STM32 hardware encoder + +This encoder driver uses the STM32 timer hardware to track the A/B impulses, which is much more efficient (and will support higher speeds) than the generic interrupt-based solution used by the standard encoder driver. + +Big thank you to @conroy-cheers for originally contributing this code in pull request #114 to the simplefoc repository. Due its hardware-specific nature we moved the code to this drivers repository. + +## Warning + +This code has not been tested much! Use at your own risk, your milage may vary. +I have tested on: STM32F401CC/TIM4-PB6,PB7 + +## Hardware setup + +The ABI signals of your encoder will have to be connected to the STM32 MCU in such a way that all signals are connected to the same timer. Consult your STM32 chip's datasheet for more information on which pins connect to which timer. + +Not all of the timers support the encoder mode, so check the datasheet which timers can be used. + +An excellent option can be to use the STM32CubeIDE software's pin assignment view to quickly check which pins connect to which timer. + +Note that the index (I) pin is currently not used. + + +## Software setup + +There is no need for interrupt functions or their configuration with this encoder class, so usage is quite simple: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/stm32hwencoder/STM32HWEncoder.h" + + +#define ENCODER_PPR 500 +#define ENCODER_PIN_A PB6 +#define ENCODER_PIN_B PB7 +#define ENCODER_PIN_I PC13 + + +STM32HWEncoder encoder = STM32HWEncoder(ENCODER_PPR, ENCODER_PIN_A, ENCODER_PIN_B, ENCODER_PIN_I); + + +void setup() { + encoder.init(); +} + +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp new file mode 100644 index 0000000..0525e3f --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp @@ -0,0 +1,78 @@ +#include "STM32HWEncoder.h" + +#if defined(_STM32_DEF_) + +/* + HardwareEncoder(int cpr) +*/ +STM32HWEncoder::STM32HWEncoder(unsigned int _ppr, int8_t pinA, int8_t pinB, int8_t pinI) { + cpr = _ppr * 4; // 4x for quadrature + _pinA = digitalPinToPinName(pinA); + _pinB = digitalPinToPinName(pinB); + _pinI = digitalPinToPinName(pinI); +} + +/* + Shaft angle calculation +*/ +float STM32HWEncoder::getSensorAngle() { + return _2PI * encoder_handle.Instance->CNT / static_cast(cpr); +} + +// getter for index pin +int STM32HWEncoder::needsSearch() { return false; } + +// private function used to determine if encoder has index +int STM32HWEncoder::hasIndex() { return 0; } + +// encoder initialisation of the hardware pins +void STM32HWEncoder::init() { + // GPIO configuration + TIM_TypeDef *InstanceA = (TIM_TypeDef *)pinmap_peripheral(_pinA, PinMap_TIM); + TIM_TypeDef *InstanceB = (TIM_TypeDef *)pinmap_peripheral(_pinB, PinMap_TIM); + if (InstanceA != InstanceB) { + initialized = false; + return; + } + pinmap_pinout(_pinA, PinMap_TIM); + pinmap_pinout(_pinB, PinMap_TIM); + + // set up timer for encoder + encoder_handle.Init.Period = cpr - 1; + encoder_handle.Init.Prescaler = 0; + encoder_handle.Init.ClockDivision = 0; + encoder_handle.Init.CounterMode = TIM_COUNTERMODE_UP; + encoder_handle.Init.RepetitionCounter = 0; + encoder_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + + TIM_Encoder_InitTypeDef encoder_config; + + encoder_config.EncoderMode = TIM_ENCODERMODE_TI12; + + encoder_config.IC1Polarity = TIM_ICPOLARITY_RISING; + encoder_config.IC1Selection = TIM_ICSELECTION_DIRECTTI; + encoder_config.IC1Prescaler = TIM_ICPSC_DIV1; + encoder_config.IC1Filter = 0; + + encoder_config.IC2Polarity = TIM_ICPOLARITY_RISING; + encoder_config.IC2Selection = TIM_ICSELECTION_DIRECTTI; + encoder_config.IC2Prescaler = TIM_ICPSC_DIV1; + encoder_config.IC2Filter = 0; + + encoder_handle.Instance = InstanceA; // e.g. TIM4; + enableTimerClock(&encoder_handle); + + if (HAL_TIM_Encoder_Init(&encoder_handle, &encoder_config) != HAL_OK) { + initialized = false; + return; + } + + if (HAL_TIM_Encoder_Start(&encoder_handle, TIM_CHANNEL_1) != HAL_OK) { + initialized = false; + return; + } + + initialized = true; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h new file mode 100644 index 0000000..98b14e5 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h @@ -0,0 +1,36 @@ + +#pragma once + +#include + + +#if defined(_STM32_DEF_) + +#include +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" + +class STM32HWEncoder : public Sensor { + public: + /** + Encoder class constructor + @param ppr impulses per rotation (cpr=ppr*4) + */ + explicit STM32HWEncoder(unsigned int ppr, int8_t pinA, int8_t pinB, int8_t pinI=-1); + + void init() override; + int needsSearch() override; + int hasIndex(); // !< function returning 1 if encoder has index pin and 0 if not. + + bool initialized = false; + uint32_t cpr; //!< encoder cpr number + PinName _pinA, _pinB, _pinI; + + protected: + float getSensorAngle() override; + + TIM_HandleTypeDef encoder_handle; + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md new file mode 100644 index 0000000..fd98be9 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md @@ -0,0 +1,46 @@ + +# STM32MagneticSensorPWM + +STM32 MCU specific PWM sensor class. This class uses the STM32's hardware timers to precisely capture the PWM input signal, and doesn't use interrupts or have MCU overhead. + +:warning: this code is not yet well tested. + +## Setup + +Please use an advanced control or general purpose timer pin on your STM32. Connect the sensor's PWM output to the MCU's input pin. Usually, pull-ups or pull-downs are not needed, but check your sensor's datasheet. + +:warning: only tested on 16 bit timers. Code changes may be needed to make it work on 32 but timers. Avoid using TIM2 and TIM5 unless you want to test it. + +The sensor needs the values `min_ticks` and `max-ticks` to be configured correctly to convert the PWM input into an angle. These values will depend on the sensor, but also on the MCU's timer clock speed. + +To print the current tick value, use: + +``` +sensor.getDutyCycleTicks(); +``` + +By rotating the motor through several full turns while printing the ticks to the screen you will be able to determine the correct values empirically. + +## Usage + +``` +STM32MagneticSensorPWM sensor = STM32MagneticSensorPWM(PB7, 412, 6917); // sample values, yours will be different + +void setup() { + ... + sensor.init(); + ... +} +``` + +To use alternate function timers, set the PinName directly: + +``` +sensor._pin = PB_7_ALT1; // manually set a PinName to use alternate timer function +``` + +PWM sensor may have a slow update time (not more often than once per PWM-period, e.g. at the PWM frequency): + +``` +sensor.min_elapsed_time = 0.001; // 1ms minimum sample time for velocity +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp new file mode 100644 index 0000000..3c3e1d6 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp @@ -0,0 +1,35 @@ + +#include "./STM32MagneticSensorPWM.h" + +#if defined(_STM32_DEF_) + +#include "common/foc_utils.h" + + +STM32MagneticSensorPWM::STM32MagneticSensorPWM(int pin, uint32_t _min_ticks, uint32_t _max_ticks) : STM32PWMInput(pin), max_ticks(_max_ticks), min_ticks(_min_ticks) { + +}; + + + +STM32MagneticSensorPWM::~STM32MagneticSensorPWM(){}; + + + +void STM32MagneticSensorPWM::init(){ + initialized = (STM32PWMInput::initialize()==0); + if(initialized) + Sensor::init(); +}; + + + +float STM32MagneticSensorPWM::getSensorAngle(){ + uint32_t ticks = getDutyCycleTicks(); + return (ticks - min_ticks) * _2PI / (max_ticks - min_ticks); +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h new file mode 100644 index 0000000..3e5d335 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h @@ -0,0 +1,28 @@ + +#pragma once + + +#include + + +#if defined(_STM32_DEF_) + +#include "common/base_classes/Sensor.h" +#include "utilities/stm32pwm/STM32PWMInput.h" + + +class STM32MagneticSensorPWM : public Sensor, public STM32PWMInput { + public: + STM32MagneticSensorPWM(int pin, uint32_t _min_ticks = 0, uint32_t _max_ticks = 0x0FFF); + ~STM32MagneticSensorPWM(); + + void init() override; + + uint32_t max_ticks = 0x0FFF; + uint32_t min_ticks = 0; + bool initialized = false; + protected: + float getSensorAngle() override; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp new file mode 100644 index 0000000..5947209 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp @@ -0,0 +1,18 @@ + +#include "./MagneticSensorTLE5012B.h" + +#if defined(_STM32_DEF_) + +MagneticSensorTLE5012B::MagneticSensorTLE5012B(int data, int sck, int nCS) : TLE5012B(data, sck, nCS) { }; +MagneticSensorTLE5012B::~MagneticSensorTLE5012B(){ }; + +void MagneticSensorTLE5012B::init() { + this->TLE5012B::init(); + this->Sensor::init(); +}; + +float MagneticSensorTLE5012B::getSensorAngle() { + return getCurrentAngle(); +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h new file mode 100644 index 0000000..712c896 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h @@ -0,0 +1,25 @@ + +#ifndef __MAGNETIC_SENSOR_TLE5012B_H__ +#define __MAGNETIC_SENSOR_TLE5012B_H__ + +#include "./STM32TLE5012B.h" + +#if defined(_STM32_DEF_) + +#include "common/base_classes/Sensor.h" + + + + +class MagneticSensorTLE5012B : public Sensor, public TLE5012B { + public: + MagneticSensorTLE5012B(int data, int sck, int nCS); + ~MagneticSensorTLE5012B(); + virtual void init() override; + virtual float getSensorAngle() override; +}; + + + +#endif +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/README.md new file mode 100644 index 0000000..3ce22c2 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/README.md @@ -0,0 +1,91 @@ +# SimpleFOC Driver for TLE5012B encoder for STM32 MCUs + +This driver code is compatible with STM32 MCUs. Based loosely on code from [here](https://github.com/xerootg/btt-s42b-simplefoc), with many thanks to @xerootg + +## Hardware setup + +Connect the data pin of the sensor to the MOSI (COPI) line of the MCU. The SCLK and nCS lines are connected as normal. + +## Software setup + +Sample code: + +``` +#include +#include +#include +#include "encoders/tle5012b/MagneticSensorTLE5012B.h" + +MagneticSensorTLE5012B sensor(PB15,PB13,PB12); + +long ts; + +void setup() { + Serial.begin(115200); + while (!Serial) ; + delay(2000); + + Serial.println("Initializing sensor..."); + + sensor.init(); + + Serial.println("Sensor initialized."); + + ts = millis(); +} + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` + +## Other MCUs + +Infineon provides a driver library compatible with Atmel AVR (Arduino UNO, Nano, etc...) and Infineon XMC MCUs. If you have one of those MCUs, you can use this library in conjunction with the GenericSensor class in SimpleFOC. + +``` +#include +#include + +Tle5012Ino Tle5012Sensor = Tle5012Ino(); +errorTypes checkError = NO_ERROR; + +float readMySensor(){ + // read my sensor + // return the angle value in radians in between 0 and 2PI + + Tle5012Sensor.getAngleValue(d); + d = (d + 180); + if ( d + 40.9 > 360 ) { + d = (d + 40.9) - 360; + } + else + d = d + 40.9; + + d = d * 0.0174533; + return d; +} +void initMySensor(){ + // do the init + checkError = Tle5012Sensor.begin(); +} + +// empty constructor +GenericSensor sensor = GenericSensor(readMySensor, initMySensor); + +void setup(){ +... + //init sensor and use it further with foc... + sensor.init(); +... +} +``` + +Other MCUs are currently not supported. The problem is that the sensor uses SPI in half-duplex mode, which is not supported by Arduino framework. So for each MCU type a custom driver has to be written to get the half-duplex SPI communication to work. There are currently no plans to support other MCUs on our side, but we will gladly accept pull requests :-) + +If you can find other libraries for other MCUs, you can use the GenericSensor approach to integrate them. \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp new file mode 100644 index 0000000..7fd89c7 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp @@ -0,0 +1,159 @@ + +#include "STM32TLE5012B.h" + +#if defined(_STM32_DEF_) + + +#include "utility/spi_com.h" +extern "C" uint32_t spi_getClkFreqInst(SPI_TypeDef *spi_inst); + + + +TLE5012B::TLE5012B(int data, int sck, int nCS, uint32_t freq) { + _data = data; + _sck = sck; + _nCS = nCS; + _freq = freq; +}; + +TLE5012B::~TLE5012B() { + +}; + + +void TLE5012B::init() { + pinMode(_nCS, OUTPUT); + digitalWrite(_nCS, HIGH); + + // initialize pins + GPIO_InitTypeDef gpio; + gpio.Pin = digitalPinToBitMask(_data); + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Speed = GPIO_SPEED_FREQ_HIGH; + HAL_GPIO_Init(digitalPinToPort(_data), &gpio); + gpio.Pin = digitalPinToBitMask(_sck); + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Speed = GPIO_SPEED_FREQ_HIGH; + HAL_GPIO_Init(digitalPinToPort(_sck), &gpio); + + SPI_TypeDef *spi_data = (SPI_TypeDef*)pinmap_peripheral(digitalPinToPinName(_data), PinMap_SPI_MOSI); + SPI_TypeDef *spi_sclk = (SPI_TypeDef*)pinmap_peripheral(digitalPinToPinName(_sck), PinMap_SPI_SCLK); + SPI_TypeDef *spi_inst = (SPI_TypeDef*)pinmap_merge_peripheral(spi_data, spi_sclk); + + pinmap_pinout(digitalPinToPinName(_data), PinMap_SPI_MOSI); + pinmap_pinout(digitalPinToPinName(_sck), PinMap_SPI_SCLK); + + #if defined SPI1_BASE + if (spi_inst == SPI1) { + __HAL_RCC_SPI1_CLK_ENABLE(); + __HAL_RCC_SPI1_FORCE_RESET(); + __HAL_RCC_SPI1_RELEASE_RESET(); + } + #endif + + #if defined SPI2_BASE + if (spi_inst == SPI2) { + __HAL_RCC_SPI2_CLK_ENABLE(); + __HAL_RCC_SPI2_FORCE_RESET(); + __HAL_RCC_SPI2_RELEASE_RESET(); + } + #endif + + #if defined SPI3_BASE + if (spi_inst == SPI3) { + __HAL_RCC_SPI3_CLK_ENABLE(); + __HAL_RCC_SPI3_FORCE_RESET(); + __HAL_RCC_SPI3_RELEASE_RESET(); + } + #endif + + #if defined SPI4_BASE + if (spi_inst == SPI4) { + __HAL_RCC_SPI4_CLK_ENABLE(); + __HAL_RCC_SPI4_FORCE_RESET(); + __HAL_RCC_SPI4_RELEASE_RESET(); + } + #endif + + #if defined SPI5_BASE + if (spi_inst == SPI5) { + __HAL_RCC_SPI5_CLK_ENABLE(); + __HAL_RCC_SPI5_FORCE_RESET(); + __HAL_RCC_SPI5_RELEASE_RESET(); + } + #endif + + #if defined SPI6_BASE + if (spi_inst == SPI6) { + __HAL_RCC_SPI6_CLK_ENABLE(); + __HAL_RCC_SPI6_FORCE_RESET(); + __HAL_RCC_SPI6_RELEASE_RESET(); + } + #endif + + _spi.Instance = spi_inst; + _spi.Init.Direction = SPI_DIRECTION_1LINE; + _spi.Init.Mode = SPI_MODE_MASTER; + _spi.Init.DataSize = SPI_DATASIZE_8BIT; + _spi.Init.CLKPolarity = SPI_POLARITY_LOW; + _spi.Init.CLKPhase = SPI_PHASE_2EDGE; + _spi.Init.NSS = SPI_NSS_SOFT; + _spi.Init.FirstBit = SPI_FIRSTBIT_MSB; + _spi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; + _spi.Init.CRCPolynomial = 7; + _spi.Init.TIMode = SPI_TIMODE_DISABLE; + #if defined(SPI_NSS_PULSE_DISABLE) + _spi.Init.NSSPMode = SPI_NSS_PULSE_DISABLE; + #endif + + uint32_t spi_freq = spi_getClkFreqInst(spi_inst); + if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV2_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV4_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV8_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV16_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV32_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV64_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV128_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128; + } else { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; + } + + if (HAL_SPI_Init(&_spi) != HAL_OK) { + // setup error + Serial.println("TLE5012B setup error"); + } +}; + +uint16_t TLE5012B::readRawAngle() { + uint8_t data[4]; + readBytes(TLE5012B_ANGLE_REG, data, 2); + return (((uint16_t)data[0] << 8) | data[1]) & 0x7FFF; +}; + + +float TLE5012B::getCurrentAngle() { + return ((float)readRawAngle())/TLE5012B_CPR * _2PI; +}; // angle in radians, return current value + + +void TLE5012B::readBytes(uint16_t reg, uint8_t *data, uint8_t len) { + digitalWrite(_nCS, LOW); + + reg |= TLE5012B_READ_REGISTER + (len>>1); + uint8_t txbuffer[2] = { (uint8_t)(reg >> 8), (uint8_t)(reg & 0x00FF) }; + HAL_SPI_Transmit(&_spi, txbuffer, 2, 100); // TODO check return value for error, timeout + //delayMicroseconds(1); + HAL_SPI_Receive(&_spi, data, len + 2, 100); + + digitalWrite(_nCS, HIGH); +}; + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h new file mode 100644 index 0000000..6ae6c65 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h @@ -0,0 +1,45 @@ +#ifndef __TLE5012B_H__ +#define __TLE5012B_H__ + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + + +#define _2PI 6.28318530718f +#define TLE5012B_CPR 32768.0f + +#define TLE5012B_READ_REGISTER 0x8000 + +enum TLE5012B_Register : uint16_t { + TLE5012B_ANGLE_REG = 0x0020, + TLE5012B_SPEED_REG = 0x0030 +}; + + + +class TLE5012B { + public: + TLE5012B(int data, int sck, int nCS, uint32_t freq = 1000000); + ~TLE5012B(); + virtual void init(); + + uint16_t readRawAngle(); + float getCurrentAngle(); // angle in radians, return current value + private: + + void readBytes(uint16_t reg, uint8_t *data, uint8_t len); + + int _data; + int _sck; + int _nCS; + uint32_t _freq; + SPI_HandleTypeDef _spi; +}; + + + + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp new file mode 100644 index 0000000..b7fea27 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp @@ -0,0 +1,548 @@ +#include "HybridStepperMotor.h" +#include "./communication/SimpleFOCDebug.h" + +// HybridStepperMotor(int pp) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance [H] +HybridStepperMotor::HybridStepperMotor(int pp, float _R, float _KV, float _inductance) + : FOCMotor() +{ + // number od pole pairs + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/K_bemf + // usually used rms + KV_rating = _KV * _SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + // current and foc_current not supported yet + torque_controller = TorqueControlType::voltage; +} + +/** + Link the driver which controls the motor +*/ +void HybridStepperMotor::linkDriver(BLDCDriver *_driver) +{ + driver = _driver; +} + +// init hardware pins +void HybridStepperMotor::init() +{ + if (!driver || !driver->initialized) + { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if (voltage_limit > driver->voltage_limit) + voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if (voltage_sensor_align > voltage_limit) + voltage_sensor_align = voltage_limit; + + // update the controller limits + if (_isset(phase_resistance)) + { + // velocity control loop controls current + PID_velocity.limit = current_limit; + } + else + { + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + + motor_status = FOCMotorStatus::motor_uncalibrated; +} + +// disable motor driver +void HybridStepperMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0, 0); + // disable driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void HybridStepperMotor::enable() +{ + // disable enable + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0, 0); + // motor status update + enabled = 1; +} + +/** + * FOC functions + */ +int HybridStepperMotor::initFOC() +{ + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if (sensor) + { + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = sensor->getAngle(); + } + else + SIMPLEFOC_DEBUG("MOT: No sensor."); + + if (exit_flag) + { + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + } + else + { + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Encoder alignment to electrical 0 angle +int HybridStepperMotor::alignSensor() +{ + int exit_flag = 1; // success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // if unknown natural direction + if(sensor_direction==Direction::UNKNOWN) { + // check if sensor needs zero search + if (sensor->needsSearch()) + exit_flag = absoluteZeroSearch(); + // stop init if not found index + if (!exit_flag) + return exit_flag; + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <= 500; i++) + { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >= 0; i--) + { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + if (mid_angle == end_angle) + { + SIMPLEFOC_DEBUG("MOT: Failed to notice movement"); + return 0; // failed calibration + } + else if (mid_angle < end_angle) + { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CCW"); + sensor_direction = Direction::CCW; + } + else + { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CW"); + sensor_direction = Direction::CW; + } + // check pole pair number + float moved = fabs(mid_angle - end_angle); + if (fabs(moved * pole_pairs - _2PI) > 0.5f) + { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI / moved); + } + else + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + else + SIMPLEFOC_DEBUG("MOT: Skip dir calib."); + + // zero electric angle not known + if (!_isset(zero_electric_angle)) + { + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + _delay(20); + if (monitor_port) + { + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } + else + SIMPLEFOC_DEBUG("MOT: Skip offset calib."); + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int HybridStepperMotor::absoluteZeroSearch() +{ + + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while (sensor->needsSearch() && shaft_angle < _2PI) + { + angleOpenloop(1.5f * _2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if (monitor_port) + { + if (sensor->needsSearch()) + SIMPLEFOC_DEBUG("MOT: Error: Not found!"); + else + SIMPLEFOC_DEBUG("MOT: Success!"); + } + return !sensor->needsSearch(); +} + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void HybridStepperMotor::loopFOC() +{ + + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) + sensor->update(); + + // if open-loop do nothing + if (controller == MotionControlType::angle_openloop || controller == MotionControlType::velocity_openloop) + return; + // shaft angle + shaft_angle = shaftAngle(); + + // if disabled do nothing + if (!enabled) + return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or voltage loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void HybridStepperMotor::move(float new_target) +{ + + // downsampling (optional) + if (motion_cnt++ < motion_downsample) + return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if (controller != MotionControlType::angle_openloop && controller != MotionControlType::velocity_openloop) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if (!enabled) + return; + + // set internal target variable + if (_isset(new_target)) + target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) + voltage_bemf = shaft_velocity / KV_rating / _RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if (!current_sense && _isset(phase_resistance)) + current.q = (voltage.q - voltage_bemf) / phase_resistance; + + // choose control loop + switch (controller) + { + case MotionControlType::torque: + if (!_isset(phase_resistance)) + voltage.q = target; // if voltage torque control + else + voltage.q = target * phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-target * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::angle: + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = P_angle(shaft_angle_sp - shaft_angle); + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + // use voltage if phase-resistance not provided + if (!_isset(phase_resistance)) + voltage.q = current_sp; + else + voltage.q = _constrain(current_sp * phase_resistance + voltage_bemf, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-current_sp * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity: + // velocity set point + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + // use voltage if phase-resistance not provided + if (!_isset(phase_resistance)) + voltage.q = current_sp; + else + voltage.q = _constrain(current_sp * phase_resistance + voltage_bemf, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-current_sp * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + case MotionControlType::angle_openloop: + // angle control in open loop + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + } +} + +void HybridStepperMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) +{ + angle_el = _normalizeAngle(angle_el); + float _ca = _cos(angle_el); + float _sa = _sin(angle_el); + float center; + + switch (foc_modulation) { + case FOCModulationType::Trapezoid_120: + case FOCModulationType::Trapezoid_150: + // not handled + Ua = 0; + Ub = 0; + Uc = 0; + break; + case FOCModulationType::SinePWM: + // C phase is fixed at half-rail to provide bias point for A, B legs + Ua = (_ca * Ud) - (_sa * Uq); + Ub = (_sa * Ud) + (_ca * Uq); + + center = driver->voltage_limit / 2; + + Ua += center; + Ub += center; + Uc = center; + break; + + case FOCModulationType::SpaceVectorPWM: + // C phase moves in order to increase max bias on coils + uint8_t sector = floor(4.0 * angle_el / _PI) + 1; + Ua = (_ca * Ud) - (_sa * Uq); + Ub = (_sa * Ud) + (_ca * Uq); + + // Determine max/ min of [Ua, Ub, 0] based on phase angle. + switch (sector) + { + case 1: + case 8: + center = (driver->voltage_limit - Ua - Ub) / 2; + break; + + case 2: + center = (driver->voltage_limit - Ua - 0) / 2; + break; + + case 3: + center = (driver->voltage_limit - Ub - 0) / 2; + break; + + case 4: + case 5: + center = (driver->voltage_limit - Ub - Ua) / 2; + break; + + case 6: + center = (driver->voltage_limit - 0 - Ua) / 2; + break; + + case 7: + center = (driver->voltage_limit - 0 - Ub) / 2; + break; + + default: // this case does not occur, but compilers complain about uninitialized variables + center = (driver->voltage_limit - 0) / 2; + break; + } + + Ua += center; + Ub += center; + Uc = center; + break; + } + + driver->setPwm(Ua, Ub, Uc); +} + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float HybridStepperMotor::velocityOpenloop(float target_velocity) +{ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if (Ts <= 0 || Ts > 0.5f) + Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity * Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if (_isset(phase_resistance)) + { + Uq = _constrain(current_limit * phase_resistance + fabs(voltage_bemf), -voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf)) / phase_resistance; + } + + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float HybridStepperMotor::angleOpenloop(float target_angle) +{ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if (Ts <= 0 || Ts > 0.5f) + Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + if (abs(target_angle - shaft_angle) > abs(velocity_limit * Ts)) + { + shaft_angle += _sign(target_angle - shaft_angle) * abs(velocity_limit) * Ts; + shaft_velocity = velocity_limit; + } + else + { + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if (_isset(phase_resistance)) + { + Uq = _constrain(current_limit * phase_resistance + fabs(voltage_bemf), -voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf)) / phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle((shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h new file mode 100644 index 0000000..71e8792 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h @@ -0,0 +1,112 @@ +/** + * @file HybridStepperMotor.h + * + */ + +#ifndef HybridStepperMotor_h +#define HybridStepperMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/StepperDriver.h" +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + Stepper Motor class +*/ +class HybridStepperMotor : public FOCMotor +{ +public: + /** + HybridStepperMotor class constructor + @param pp pole pair number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + HybridStepperMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver BLDCDriver handle for hardware peripheral control + */ + void linkDriver(BLDCDriver *driver); + + /** + * BLDCDriver link: + */ + BLDCDriver *driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + */ + int initFOC() override; + + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + + /** + * Function executing the control loops set by the controller parameter of the HybridStepperMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ua, Ub, Uc; //!< Phase voltages used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + +private: + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md new file mode 100644 index 0000000..4176539 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md @@ -0,0 +1,45 @@ +# Three Phase / Hybrid Stepper Motor Class + +By tying two phases together, you can use a standard bipolar stepper motor with three-phase drivers! +Of course, it requires some different math for commutation. Another limitation is that with SinePWM the motor voltage is limited to 50% of the supply voltage. Using some tricks in SpaceVectorPWM, this max bias can be increased to about 70%. + +## Warning + +This code has not been tested much! +It has killed one B-G431B-ESC1 board before. +However, it has been succesfully used on a few different hardware platforms, so it does work. +BEMF may be the danger here, so take caution with fast decelerations. +Keep in mind that potentially twice the current can be sunk in the shorted phase as the two other legs. + +## Hardware setup + +Tie two of the phases of the motor together. This is phase "C". The other two phases are A & B. Phase C must go last in the driver constructor. + + +## Software setup + +Usage is quite simple: + +```c++ +#include "Arduino.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "motors/HybridStepperMotor/HybridStepperMotor.h" + +HybridStepperMotor motor = HybridStepperMotor(pole_pairs, phase_resistance, kv, winding_inductance); +BLDCDriver3PWM driver = BLDCDriver3PWM(phase_A, phase_B, phase_C, enable); + +void setup() { + driver.init(); + motor.linkDriver(&driver); + + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; //or SinePWM + motor.init(); + motor.initFOC(); +} + +void loop(){ + motor.loopFOC(); + motor.move(); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/README.md new file mode 100644 index 0000000..9bd70fb --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/README.md @@ -0,0 +1,143 @@ +# SimpleFOC Settings Storage + +Code to persist calibration data and motor settings to and from storage. The SettingsStorage is abstract, and different implementations exist for different concete storage media. + +The SettingsStorage is based on the SimpleFOCRegisters abstraction, so this means you can load or store any of the registers (representing SimpleFOC settings) that is both readable and writeable (e.g. while the current shaft velocity is available as a register, you can't store it as a setting because its read-only). + +The SettingsStorage can save the settings for one or more motors, and you can customize which settings are saved. If you don't customize, only the motor +calibration will be stored (REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION). + +## Hardware requirements + +Obviously, to save settings you need a place to store them where they won't be lost when you power down. There can be many solutions to this question, and we attempt to provide some simple drivers for the common storage solutions people might want. + +But its also quite easy to extend the system to support your specific needs. + +Supported hardware: + +- [SAMD NVM](samd/) \ +Some SAMD chips have a built-in NVRAM flash, which can be written from user code. +- [CAT24 I2C Flash EEPROMs](i2c/) \ :warning: UNFINISHED +Simple programmable memories with I2C interface. Memory space is quite limited, but enough for storing settings. + +See Roadmap, below, for systems we may support in the future. + + +## Basic Usage + +Using SettingsStorage is simple. Just add a `settings.loadSettings()` after setting up your defaults, but before calling init() on the objects. And call `saveSettings()` after initialization of the motor has (successfully) completed. + +This example saves just the motor calibration, but you might want to add additional code to save other registers, or make your solution more user-friendly. See the other examples below. + +```c++ + +#include "settings/samd/SAMDNVMSettingsStorage.h" + +SAMDNVMSettingsStorage settings = SAMDNVMSettingsStorage(); +BLDCMotor motor = BLDCMotor(...); + +void setup() { + SimpleFOCDebug::enable(); // show debug messages from settings + + // initialize the settings early in the setup + settings.addMotor(&motor); + settings.init(); + + // first set your defaults + driver.voltage_power_supply = 12.0f; + driver.voltage_limit = 12.0f; // 12V driver limit, like PSU + motor.voltage_limit = 6.0f; // 6V motor limit + + // then try to load the settings + SettingsStatus loadStatus = settings.loadSettings(); + + // driver init code, etc... + ... + + // then init the motor + motor.init(); + motor.initFOC(); + + // and if the settings were not loaded earlier, then save them + if (motor.motor_status == FOCMotorStatus::motor_ready && loadStatus != SFOC_SETTINGS_SUCCESS) { + settings.saveSettings(); + } + + // any other initialization + ... +} + +``` + +### Choosing registers + +By default, only the calibration data (motor native direction, electrical zero position) are saved. To save additional registers, you can choose them before calling `SettingsStorage.init()`, as in the example below: + +```c++ + +SimpleFOCRegister settingsRegisters[] = { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION, REG_VEL_PID_P, REG_VEL_PID_I, REG_VEL_PID_D, REG_VEL_LPF_T, REG_VEL_LIMIT, REG_VEL_MAX_RAMP, REG_VOLTAGE_LIMIT, REG_MOTION_DOWNSAMPLE, REG_CONTROL_MODE, REG_TORQUE_MODE, REG_PHASE_RESISTANCE, REG_KV, REG_INDUCTANCE }; +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); +BLDCMotor motor = BLDCMotor(...); + +void setup() { + SimpleFOCDebug::enable(); // show debug messages from settings + + // initialize the settings early in the setup + settings.setRegisters(mySettingsRegisters, sizeof(mySettingsRegisters)); + settings.addMotor(&motor); + settings.init(); + ... +} +``` + + + +### Using settings with commander + +You can easily interact with the settings via the commander, for example to save your PID tuning values: + +```c++ + +SimpleFOCRegister settingsRegisters[] = { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION, REG_VEL_PID_P, REG_VEL_PID_I, REG_VEL_PID_D, REG_VEL_LPF_T }; + +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); + +Commander commander; + +void doSaveSettings(char *cmd) { settings.saveSettings(); } +void doLoadSettings(char *cmd) { settings.loadSettings(); } + + +void setup() { + ... + + settings.setRegisters(settingsRegisters, sizeof(settingsRegisters)); + settings.addMotor(&motor); + settings.init(); + + ... + + commander.addCommand('s', doSaveSettings, "Save settings"); + commander.addCommand('l', doLoadSettings, "Load settings"); + + ... + +} + + +void run() { + motor.move(); + motor.loopFOC(); + commander.run(); +} +``` + +## Roadmap + +The following ideas for storage backends are on the roadmap, with no definate schedule for implementation: + +- ESP32 SPIFFs Filesystem +- ESP32 Preferences +- Arduino EEPROM library +- In-Memory Buffer (for use with other libraries or abstractions) +- JSON String (for printing to console) diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.cpp new file mode 100644 index 0000000..5df2bcf --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.cpp @@ -0,0 +1,120 @@ + +#include "./SettingsStorage.h" +#include "communication/SimpleFOCDebug.h" + + +SettingsStorage::SettingsStorage() { + +}; + + +SettingsStorage::~SettingsStorage() { + +}; + + + +void SettingsStorage::addMotor(BLDCMotor* motor) { + if (numMotors < SIMPLEFOC_SETTINGS_MAX_MOTORS) { + motors[numMotors] = motor; + numMotors++; + } + else + SimpleFOCDebug::println("SS: too many motors"); +}; + + +void SettingsStorage::setRegisters(SimpleFOCRegister* registers, int numRegisters){ + this->registers = registers; + this->numRegisters = numRegisters; +}; + + +void SettingsStorage::init() { + // make sure we have motors and registers + if (numMotors < 1) { + SimpleFOCDebug::println("SS: no motors"); + return; + } + if (registers==NULL || numRegisters < 1) { + SimpleFOCDebug::println("SS: no registers"); + return; + } +}; + + + +SettingsStatus SettingsStorage::loadSettings() { + SimpleFOCDebug::println("Loading settings..."); + beforeLoad(); + uint8_t magic; readByte(&magic); + if (magic != SIMPLEFOC_SETTINGS_MAGIC_BYTE) { + SimpleFOCDebug::println("No settings found "); + return SFOC_SETTINGS_NONE; + } + uint8_t rversion; readByte(&rversion); + if (rversion != SIMPLEFOC_REGISTERS_VERSION) { + SimpleFOCDebug::println("Registers version mismatch"); + return SFOC_SETTINGS_OLD; + } + uint8_t version; readByte(&version); + if (version != settings_version) { + SimpleFOCDebug::println("Settings version mismatch"); + return SFOC_SETTINGS_OLD; + } + for (int m = 0; m < numMotors; m++) { + if (numMotors>1) + SimpleFOCDebug::println("Loading settings for motor ", m); + startLoadMotor(m); + for (int i = 0; i < numRegisters; i++) { + SimpleFOCRegister reg = registers[i]; + startLoadRegister(reg); + setRegister(reg, motors[m]); + endLoadRegister(); + } + endLoadMotor(); + } + afterLoad(); + SimpleFOCDebug::println("Settings loaded"); + return SFOC_SETTINGS_SUCCESS; +}; + + +SettingsStatus SettingsStorage::saveSettings() { + SimpleFOCDebug::println("Saving settings..."); + beforeSave(); + writeByte(SIMPLEFOC_SETTINGS_MAGIC_BYTE); + writeByte(SIMPLEFOC_REGISTERS_VERSION); + writeByte(settings_version); + for (int m = 0; m < numMotors; m++) { + if (numMotors>1) + SimpleFOCDebug::println("Saving settings for motor ", m); + startSaveMotor(m); + for (int i = 0; i < numRegisters; i++) { + SimpleFOCRegister reg = registers[i]; + startSaveRegister(reg); + sendRegister(reg, motors[m]); + endSaveRegister(); + } + endSaveMotor(); + } + afterSave(); + SimpleFOCDebug::println("Settings saved"); + return SFOC_SETTINGS_SUCCESS; +}; + +// empty implementation for these + +void SettingsStorage::startSaveMotor(uint8_t num) {}; +void SettingsStorage::endSaveMotor() {}; +void SettingsStorage::startSaveRegister(SimpleFOCRegister reg) {}; +void SettingsStorage::endSaveRegister() {}; +void SettingsStorage::startLoadMotor(uint8_t num) {}; +void SettingsStorage::endLoadMotor() {}; +void SettingsStorage::startLoadRegister(SimpleFOCRegister reg) {}; +void SettingsStorage::endLoadRegister() {}; + +void SettingsStorage::beforeLoad() {}; +void SettingsStorage::afterLoad() {}; +void SettingsStorage::beforeSave() {}; +void SettingsStorage::afterSave() {}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.h new file mode 100644 index 0000000..8506c3b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/SettingsStorage.h @@ -0,0 +1,70 @@ + +#pragma once + + +#include "../comms/SimpleFOCRegisters.h" +#include "../comms/RegisterReceiver.h" +#include "../comms/RegisterSender.h" +#include "BLDCMotor.h" + +#define SIMPLEFOC_SETTINGS_MAGIC_BYTE 0x42 +#define SIMPLEFOC_SETTINGS_VERSION 0x01 + +#define SIMPLEFOC_SETTINGS_REGISTERS_MINIMAL { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION } + + +#if !defined(SIMPLEFOC_SETTINGS_MAX_MOTORS) +#define SIMPLEFOC_SETTINGS_MAX_MOTORS 4 +#endif + + +typedef enum : uint8_t { + SFOC_SETTINGS_SUCCESS = 0, // settings loaded/saved successfully + SFOC_SETTINGS_NONE = 1, // no settings found to load + SFOC_SETTINGS_OLD = 2, // settings found, but version is old + SFOC_SETTINGS_ERROR = 3 // other error +} SettingsStatus; + + + + +class SettingsStorage : public RegisterReceiver, public RegisterSender { +public: + SettingsStorage(); + ~SettingsStorage(); + + void addMotor(BLDCMotor* motor); + void setRegisters(SimpleFOCRegister* registers, int numRegisters); + + virtual void init(); + + SettingsStatus loadSettings(); + SettingsStatus saveSettings(); + + /* + Change this value if you make changes to the registers saved in code, and have saved settings on existing devices. + This will cause the device to reset to defaults on the next boot. + */ + uint8_t settings_version = SIMPLEFOC_SETTINGS_VERSION; + +protected: + virtual void startSaveMotor(uint8_t num); + virtual void endSaveMotor(); + virtual void startSaveRegister(SimpleFOCRegister reg); + virtual void endSaveRegister(); + virtual void startLoadMotor(uint8_t num); + virtual void endLoadMotor(); + virtual void startLoadRegister(SimpleFOCRegister reg); + virtual void endLoadRegister(); + + + virtual void beforeLoad(); + virtual void afterLoad(); + virtual void beforeSave(); + virtual void afterSave(); + + FOCMotor* motors[SIMPLEFOC_SETTINGS_MAX_MOTORS]; + int numMotors = 0; + SimpleFOCRegister* registers = NULL; + int numRegisters = 0; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp new file mode 100644 index 0000000..5d4b336 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp @@ -0,0 +1,98 @@ + +#include "./CAT24I2CFlashSettingsStorage.h" + + +CAT24I2CFlashSettingsStorage::CAT24I2CFlashSettingsStorage(uint8_t address, uint16_t offset) { + _address = address; + _offset = offset; +}; + + + +CAT24I2CFlashSettingsStorage::~CAT24I2CFlashSettingsStorage() {}; + + + +void CAT24I2CFlashSettingsStorage::init(TwoWire* wire) { + SettingsStorage::init(); + _wire = wire; + reset(); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readByte(uint8_t* valueToSet) { + return readBytes(valueToSet, 1); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readFloat(float* valueToSet) { + return readBytes(valueToSet, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readInt(uint32_t* valueToSet) { + return readBytes(valueToSet, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeByte(uint8_t value) { + return writeBytes(&value, 1); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeFloat(float value) { + return writeBytes(&value, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeInt(uint32_t value) { + return writeBytes(&value, 4); +}; + + + +void CAT24I2CFlashSettingsStorage::beforeSave() { + reset(); +}; + + +void CAT24I2CFlashSettingsStorage::beforeLoad() { + reset(); + _wire->beginTransmission(_address); + _wire->write(_currptr >> 8); + _wire->write(_currptr & 0xFF); + _wire->endTransmission(false); +}; + + + +void CAT24I2CFlashSettingsStorage::reset() { + _currptr = _offset; +}; + + +int CAT24I2CFlashSettingsStorage::readBytes(void* valueToSet, int numBytes) { + int read = _wire->requestFrom((uint8_t)_address, (uint8_t)numBytes, (uint8_t)true); + _currptr += read; + if (read==numBytes) + _wire->readBytes((uint8_t*)valueToSet, numBytes); + return read; +}; + + +int CAT24I2CFlashSettingsStorage::writeBytes(void* value, int numBytes) { + _wire->beginTransmission(_address); + _wire->write(_currptr >> 8); + _wire->write(_currptr & 0xFF); + size_t written = _wire->write((uint8_t*)value, numBytes); + _wire->endTransmission(true); + _currptr += written; + delay(5); // write-cycle time + return written; +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h new file mode 100644 index 0000000..4f6c511 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h @@ -0,0 +1,35 @@ + +#pragma once + +#include "../SettingsStorage.h" +#include "Wire.h" + +class CAT24I2CFlashSettingsStorage : public SettingsStorage { +public: + CAT24I2CFlashSettingsStorage(uint8_t address = 0xA0, uint16_t offset = 0x0); + ~CAT24I2CFlashSettingsStorage(); + + void init(TwoWire* wire = &Wire); + +protected: + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + void beforeSave() override; + void beforeLoad() override; + + void reset(); + int readBytes(void* valueToSet, int numBytes); + int writeBytes(void* value, int numBytes); + + TwoWire* _wire; + uint8_t _address; // i2c address + uint16_t _offset; // offset into NVRAM to store settings + uint16_t _currptr = 0; // pointer to current location in NVRAM + +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/README.md new file mode 100644 index 0000000..9cb33bd --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/i2c/README.md @@ -0,0 +1,26 @@ + +# I2C memories settings drivers + +Store settings to I2C EEPROMs. + +## CAT24C64 I2C EEPROMs + +:warning: not yet finished or tested! + +Datasheet describes the CAT24C64 as: _The CAT24C64 is a 64 Kb CMOS Serial EEPROM device, internally organized as 8192 words of 8 bits each._ + +Provide I2C address in constructor: + +`CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0);` + +If you want to use a different I2C bus than the default, you can pass it to the constructor: + +`settings.init(&Wire2);` + +You can use multiple settings objects in the same EEPROM, by providing an offset to the constructor: + +```c++ +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0, 80); +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0, 160); +``` diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/README.md new file mode 100644 index 0000000..7382751 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/README.md @@ -0,0 +1,49 @@ +# SAMD21 settings driver + +Driver for storing settings on SAMD21 chips. + +There are two options: + +1. **RWW Flash** \ +Some SAMD21 chips have a seperate RWW flash area, of 2kB-8kB size. This area is seperate from the main flash array. \ +Note: this method is implemented and tested. + +2. **EEPROM emulation** \ +All SAMD21s support EEPROM emulation, where an area of the main flash is reserved for the application storage. \ +:warning: This method is not yet implemented fully, and untested. + +## Using RWW flash + +To use the RWW flash to store settings, you have to make sure the flash region is not locked (you can set the lock fuses using MCP Studio or other SAM development tools). + +Otherwise there is nothing special to do. When reprogramming the chip, be careful not to erase the RWW area. + +You can use multiple settings objects to store to different areas of the RWW. +To do so, specify an offset to the constructor: + +```c++ +SAMDNVMSettingsStorage settings0 = SAMDNVMSettingsStorage(); +SAMDNVMSettingsStorage settings1 = SAMDNVMSettingsStorage(NVMCTRL_ROW_SIZE); +SAMDNVMSettingsStorage settings2 = SAMDNVMSettingsStorage(NVMCTRL_ROW_SIZE*2); +``` + +Note: the offset must be a multiple of the flash memory row size, typically 256 bytes. + + +## Using EEPROM emulation + +TODO implement and test this method + +To use the EEPROM emulation, use a tool like MCP Studio to set the chip fuses to reserve an area of flash for EEPROM emulation: + +![](eeprom_emulation_screenshot.jpg) + +Normally 256 bytes (one flash page) should be plenty for SimpleFOC settings... Obviously, the area you reserve for EEPROM can't be used for your main program. + +Add a build-flag to your build to set the base address of your EEPROM memory: + +`-DSIMPLEFOC_SAMDNVMSETTINGS_BASE=0x003FF800` + +Then use the settings as normal. + +You'll have to adjust your board files to exclude the EEPROM area in the ldscript to prevent it being erased when you re-program, if you care to keep your settings when reflashing. diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp new file mode 100644 index 0000000..5569600 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp @@ -0,0 +1,164 @@ + + + +#include "./SAMDNVMSettingsStorage.h" + + +#if defined(_SAMD21_) + +#include "communication/SimpleFOCDebug.h" + + +SAMDNVMSettingsStorage::SAMDNVMSettingsStorage(uint32_t offset) { + _offset = offset; +}; + +SAMDNVMSettingsStorage::~SAMDNVMSettingsStorage(){}; + +void SAMDNVMSettingsStorage::init(){ + SettingsStorage::init(); + reset(); +}; + + + +void SAMDNVMSettingsStorage::reset(){ + _currptr = (uint8_t*) SIMPLEFOC_SAMDNVMSETTINGS_BASE + _offset; // add offset to NVM base address +}; + + + +void SAMDNVMSettingsStorage::beforeLoad(){ + reset(); +}; + + +void SAMDNVMSettingsStorage::beforeSave(){ + reset(); + _writeIndex = 0; + + // set manual write mode + _ctlB = NVMCTRL->CTRLB.reg; + NVMCTRL->CTRLB.reg |= NVMCTRL_CTRLB_MANW; + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + // unlock region - user can do it with fuses + // erase rows + // TODO handle case that it is more than one row + NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + NVMCTRL->STATUS.reg |= NVMCTRL_STATUS_MASK; + NVMCTRL->INTFLAG.bit.ERROR = 0; + NVMCTRL->CTRLA.reg = 0x1A | NVMCTRL_CTRLA_CMDEX_KEY; + delay(1); + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + if (NVMCTRL->INTFLAG.bit.ERROR == 1) + SimpleFOCDebug::println("NVM erase error ", NVMCTRL->STATUS.reg); + else + SimpleFOCDebug::println("NVM erased row @", (int)_currptr); +}; + + +void SAMDNVMSettingsStorage::afterSave() { + if (_writeIndex>0) + flushPage(); + // restore ctlb + delay(1); + NVMCTRL->CTRLB.reg =_ctlB; + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; +} + + +void SAMDNVMSettingsStorage::flushPage(){ + // if we have an odd number of bytes, pad with 0xFF + if (_writeIndex%2==1) { + _writeBuffer[_writeIndex++] = 0xFF; + } + // erase page buffer + // NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + // NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMD_PBC | NVMCTRL_CTRLA_CMDEX_KEY; + // while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + // copy writeBuffer to NVM, using 16-bit writes + uint16_t* src = (uint16_t*)_writeBuffer; + uint16_t* dst = (uint16_t*)_currptr; + for (int i=0;i<_writeIndex/2;i++) { + *dst++ = *src++; + } + // write page + NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + NVMCTRL->STATUS.reg |= NVMCTRL_STATUS_MASK; + NVMCTRL->INTFLAG.bit.ERROR = 0; + NVMCTRL->CTRLA.reg = 0x1C | NVMCTRL_CTRLA_CMDEX_KEY; + delay(1); + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + if (NVMCTRL->INTFLAG.bit.ERROR == 1) + SimpleFOCDebug::println("NVM write error ", NVMCTRL->STATUS.reg); + else + SimpleFOCDebug::println("NVM wrote page @", (int)_currptr); + // reset writeBuffer pointer and advance currptr to next page + _writeIndex = 0; + _currptr+=NVMCTRL_PAGE_SIZE; +} + + +int SAMDNVMSettingsStorage::readBytes(void* valueToSet, int numBytes) { + uint8_t* bytes = (uint8_t*)valueToSet; + for (int i=0;i + +#if defined(_SAMD21_) + +#include "../SettingsStorage.h" + +#define RWWEE_BASE 0x00400000 + +#if !defined(SIMPLEFOC_SAMDNVMSETTINGS_BASE) +#define SIMPLEFOC_SAMDNVMSETTINGS_BASE RWWEE_BASE +#endif + + +class SAMDNVMSettingsStorage : public SettingsStorage { +public: + SAMDNVMSettingsStorage(uint32_t offset = 0x0); + ~SAMDNVMSettingsStorage(); + + void init() override; + +protected: + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + void beforeSave() override; + void afterSave() override; + void beforeLoad() override; + + + void reset(); + int readBytes(void* valueToSet, int numBytes); + int writeBytes(void* value, int numBytes); + void flushPage(); + + uint32_t _offset; // offset into NVRAM to store settings + + uint32_t _ctlB; + uint8_t* _currptr; // pointer to current location in NVM + int _writeIndex; // index into current page being written + uint8_t _writeBuffer[NVMCTRL_PAGE_SIZE]; // buffer for writing to NVM +}; + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp new file mode 100644 index 0000000..36271da --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp @@ -0,0 +1,113 @@ +/* + * PreciseAngle.cpp + * + * Created on: 1 May 2021 + * Author: runger + */ + +#include +#include + +PreciseAngle::PreciseAngle() : angle(0), rotations(0) {} + +PreciseAngle::~PreciseAngle() {} + +PreciseAngle::PreciseAngle(const PreciseAngle &other) : angle(other.angle), rotations(other.rotations) {} + + +PreciseAngle::PreciseAngle(uint16_t count, int32_t rotations) : angle(count), rotations(rotations) {} + + +PreciseAngle::PreciseAngle(float radians) { + rotations = radians/_2PI; + angle = (radians - (rotations * _2PI)) * cpr / _2PI; +} + +PreciseAngle::PreciseAngle(double radians) { + rotations = radians/_2PI; + angle = (radians - (rotations * _2PI)) * cpr / _2PI; +} + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +float PreciseAngle::getShaftAngle() { + return angle * _2PI / cpr; +} + + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +int32_t PreciseAngle::getRotations() { + return rotations; +} + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +uint16_t PreciseAngle::getShaftTicks() { + return angle; +} + + +// returns the total angle, including rotations, in radians +float PreciseAngle::asFloat() { + return (angle * _2PI / (float)cpr) + ((float)rotations * _2PI); +} + + +// returns the total angle, including rotations, in radians, as double precision +double PreciseAngle::asDouble() { + return (double)angle * _2PI / cpr + (double)rotations * _2PI; +} + + +// returns the total angle, including rotations, in ticks, as 64 bit integer +int64_t PreciseAngle::asTicks() { + return (int64_t)angle + (int64_t)rotations * cpr; +} + + + + +PreciseAngle PreciseAngle::operator+(const PreciseAngle &other) { + PreciseAngle result; + result.rotations = rotations + other.rotations; + uint32_t temp = angle + other.angle; + if (temp>=cpr) { + result.angle = temp-cpr; + result.rotations+=1; + } + else + result.angle = temp; + return result; +} + + +PreciseAngle PreciseAngle::operator-(const PreciseAngle &other) { + PreciseAngle result; + result.rotations = rotations - other.rotations; + int32_t temp = angle - other.angle; + if (temp<0) { + result.angle = temp+cpr; + result.rotations-=1; + } + else + result.angle = temp; + return result; +} + + +// velocity in rad/s +float PreciseAngle::velocity(const PreciseAngle &previous, uint32_t microseconds) { + PreciseAngle diff = (*this - previous); + return diff.asFloat() * (1e6/(float)microseconds); +} + + + +void PreciseAngle::update(uint16_t current_angle){ + int32_t diff = current_angle - angle; + if (abs(diff)>cpr_overflow_check){ + rotations += diff>0?-1:1; + } + angle = current_angle; +} + + + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.h new file mode 100644 index 0000000..9b05d5e --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/PreciseAngle.h @@ -0,0 +1,57 @@ +/* + * PreciseAngle.h + * + * Created on: 1 May 2021 + * Author: runger + */ + +#ifndef LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ +#define LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ + +#include + +class PreciseAngle { +public: + PreciseAngle(); + virtual ~PreciseAngle(); + PreciseAngle(const PreciseAngle &other); + PreciseAngle(uint16_t count, int32_t rotations); + PreciseAngle(float radians); + PreciseAngle(double radians); + //PreciseAngle& operator=(const PreciseAngle &other); + + // returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians + float getShaftAngle(); + // number of complete motor rotations + int32_t getRotations(); + uint16_t getShaftTicks(); + + // returns the total angle, including rotations, in radians + float asFloat(); + // returns the total angle, including rotations, in radians, as double precision + double asDouble(); + // returns the total angle, including rotations, in ticks, as 64 bit integer + int64_t asTicks(); + + + PreciseAngle operator+(const PreciseAngle &other); + PreciseAngle operator-(const PreciseAngle &other); + + // velocity in rad/s + float velocity(const PreciseAngle &previous, uint32_t microseconds); + + // update the angle with a new ticks value. this will increment the rotations + // if necessary, using a simple algorithm to detect overflows. + // if the motor turns too much between calls to update, the overflow could be missed + // and the rotations counted incorrectly. + void update(uint16_t current_angle); + + + const static uint16_t cpr = 16384; + const static uint16_t cpr_overflow_check = 13107; //cpr*0.8; +protected: + uint16_t angle; + int32_t rotations; +}; + +#endif /* LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ */ diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/README.md new file mode 100644 index 0000000..7eb1796 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/README.md @@ -0,0 +1,25 @@ +# Utilities + +Some classes and functions you may find useful when working with SimpleFOC. + +### PreciseAngle + +:warning: work in progress! only velocity mode has been tested. + +SimpleFOC generally uses floats for calculations. This preserves compatibility with older MCUs which only support 32bit FP, and +can't do double precision, and ensures the maximum compatibilty for the library. + +When older MCUs become irrelevant, this may change, but in the meantime there is a problem representing the angles as floats. +On the one hand they have to be precise enough to accurately capture the electric position of the motor's rotor with respect to +the stator, so meybe even less than 0.1deg. Without this precision you can't do FOC. On the other hand, we need to support many +rotations of the motor shaft - a motor turning at 1000RPM for 1h will be at 60000 rotations or approx. 378,991.118radians. +Expressing these large numbers while also maintaining the required precision is too much for the humble float. + +PreciseAngle works in conjunction with PreciseXXXSensor classes [like this one](../encoders/as5048a/PreciseMagneticSensorAS5048A.h). +It solves the float problem by representing the angle as seperate components: angle within rotation and number of rotations, both as +integers. It maintains the complete precision of the sensor over a rotation range of +/- 2^31, i.e. about 2 billion rotations, before +overflowing. + +Even at 50000RPM (which would be a lot for SimpleFOC!) that's enough for about 30 days of continuous rotation. At 1000RPM it's enough +for 4 years. + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/README.md b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/README.md new file mode 100644 index 0000000..20a641b --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/README.md @@ -0,0 +1,73 @@ + +# STM32 Utilities - PWM Input + +Using the `STM32PWMInput` class you can precisely read a PWM input signal on STM32 MCUs by using their timer's PWM-Input mode. This happens entirely in the timer hardware with zero MCU overhead, and is very precise. + +## Setup + +Connect the PWM input to either channel 1 or channel 2 of a general purpose or advanced control timer supporting PWM-Input mode. + +This should include the following timers: + +- Advanced control timers: TIM1, TIM8 +- General purpose timers (32 bit): TIM2, TIM5 +- General purpose timers (16 bit): TIM3, TIM4, TIM9, TIM12 + +The best to use are the 32 bit general purpose timers, although TIM1 may also be interesting because on some MCUs it can be clocked at a higher rate than the other timers. + +## Usage + +``` + STM32PWMInput pwmInput = STM32PWMInput(PA15); + pwmInput.init(); + + uint32_t periodTicks = pwmInput.getPeriodTicks(); + uint32_t dutyTicks = pwmInput.getDutyCycleTicks(); + float dutyPercent = pwmInput.getDutyCyclePercent(); +``` + +## Input signal + +The input signal should be a PWM signal (square wave) with a duty cycle that is >0% and <100%. The + +The behaviour if the input is not a square wave is not defined, although the MCU will continue to sample the input, and resumes correct measurement when the square wave input is restored. + +## Performance + +The range of PWM frequencies that can be measured and the resolution with which they can be sampled depends on the following: + +- timer clock speed - normally this is your MCU speed, but it can be lower, or in some cases even higher. +- timer prescaler - can divide the timer clock. + +The sample resolution for a PWM input signal of frequency _Fp_ is given by + +_R = Fc / Fp_ + +Where _R_ is the resolution in ticks, and _Fc_ is the timer tick frequency (timer clock with prescaler). + +This sample resolution is equal to the length of the PWM period in ticks. The timer needs to sample both the duty cycle and the full period, so the timer can't sample signals whose period would cause it to overflow. + +On a 16 bit timer this means the period (and sample resolution) must be less than 65536 ticks, while on a 32 bit timer it must be less than 4294967296 ticks. This places a lower limit on the PWM frequencies that can be sampled. + +On the other side, the minimum duty cycle that can be reliably captured is limited by the duration of one timer tick, e.g. the minimum on-time that can be measured reliably is equal to two timer ticks in duration. + +So as the frequency increases, the resolution decreases, and the minimum duty cycle increases. + +For example, on a 64MHz STM32 MCU, using TIM3 (16 bit) you could capture a PWM signal at 10kHz with a resolution of 6400 ticks. This fits in the 16 bit timer, so no problem. Assuming the signal is perfectly stable (it usually isn't) that would be 12 bits of accuracy on your input. + +If you lowered the PWM input frequency to 1kHz, you'd get a 64000 range - just fitting in the timer. But now the accuracy is greatly increased - almost 16 bits! + +If you increased the PWM frequency to 10Mhz, the resolution would be just 6 ticks, and the resulting accuracy very low - just 2 bits, with a minimum duty cycle of 33% and a maximum of 66%. + +Of course you could vary the 10MHz signal faster than the 1kHz one - so choosing the PWM input frequency is a tradeoff between accuracy and control bandwidth... + +Note: clock configuration is out of scope for this class. Set your clocks up in advance. + + + +## Roadmap + +- Support setting of the filtering function, this could help a lot against noise on the input +- Support setting of the timer prescaler (not the clock prescaler!) +- Support setting of the downsample function, this could help increase accuracy +- Support choosing of the alternate timers on pins with more than one diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp new file mode 100644 index 0000000..c530c04 --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp @@ -0,0 +1,108 @@ + +#include "./STM32PWMInput.h" + +#if defined(_STM32_DEF_) + + + +STM32PWMInput::STM32PWMInput(int pin){ + _pin = digitalPinToPinName(pin); +}; + + +STM32PWMInput::~STM32PWMInput(){}; + + + + + +int STM32PWMInput::initialize(){ + pinmap_pinout(_pin, PinMap_TIM); + uint32_t channel = STM_PIN_CHANNEL(pinmap_function(_pin, PinMap_TIM)); + timer.Instance = (TIM_TypeDef *)pinmap_peripheral(_pin, PinMap_TIM); + timer.Init.Prescaler = 0; + timer.Init.CounterMode = TIM_COUNTERMODE_UP; + timer.Init.Period = 4.294967295E9; // TODO max period, depends on which timer is used - 32 bits or 16 bits + timer.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + timer.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (channel!=1 && channel!=2) // only channels 1 & 2 supported + return -10; + useChannel2 = (channel==2);// remember the channel + if (HAL_TIM_Base_Init(&timer) != HAL_OK) { + return -1; + } + TIM_ClockConfigTypeDef clkCfg = { .ClockSource=TIM_CLOCKSOURCE_INTERNAL }; + if (HAL_TIM_ConfigClockSource(&timer, &clkCfg) != HAL_OK) { + return -2; + } + if (HAL_TIM_IC_Init(&timer) != HAL_OK) { + return -3; + } + + TIM_SlaveConfigTypeDef slCfg = { + .SlaveMode = TIM_SLAVEMODE_RESET, + .InputTrigger = (channel==1)?TIM_TS_TI1FP1:TIM_TS_TI2FP2, + .TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING, + .TriggerPrescaler = TIM_ICPSC_DIV1, + .TriggerFilter = 0 + }; + if (HAL_TIM_SlaveConfigSynchro(&timer, &slCfg) != HAL_OK) { + return -4; + } + TIM_IC_InitTypeDef icCfg = { + .ICPolarity = (channel==1)?TIM_INPUTCHANNELPOLARITY_RISING:TIM_INPUTCHANNELPOLARITY_FALLING, + .ICSelection = (channel==1)?TIM_ICSELECTION_DIRECTTI:TIM_ICSELECTION_INDIRECTTI, + .ICPrescaler = TIM_ICPSC_DIV1, + .ICFilter = 0 + }; + if (HAL_TIM_IC_ConfigChannel(&timer, &icCfg, TIM_CHANNEL_1) != HAL_OK) { + return -5; + } + icCfg.ICPolarity = (channel==1)?TIM_INPUTCHANNELPOLARITY_FALLING:TIM_INPUTCHANNELPOLARITY_RISING; + icCfg.ICSelection = (channel==1)?TIM_ICSELECTION_INDIRECTTI:TIM_ICSELECTION_DIRECTTI; + if (HAL_TIM_IC_ConfigChannel(&timer, &icCfg, TIM_CHANNEL_2) != HAL_OK) { + return -6; + } + TIM_MasterConfigTypeDef mCfg = { + .MasterOutputTrigger = TIM_TRGO_RESET, + .MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE + }; + if (HAL_TIMEx_MasterConfigSynchronization(&timer, &mCfg) != HAL_OK) { + return -7; + } + if (HAL_TIM_IC_Start(&timer, TIM_CHANNEL_1)!=HAL_OK) { + return -8; + } + if (HAL_TIM_IC_Start(&timer, TIM_CHANNEL_2)!=HAL_OK) { + return -9; + } + timer.Instance->CR1 |= TIM_CR1_CEN; + return 0; +}; + + +float STM32PWMInput::getDutyCyclePercent(){ + uint32_t period = getPeriodTicks(); + if (period<1) return 0.0f; + return getDutyCycleTicks() / (float)period * 100.0f; +}; + + +uint32_t STM32PWMInput::getDutyCycleTicks(){ + if (useChannel2) + return timer.Instance->CCR1; + else + return timer.Instance->CCR2; +}; + + +uint32_t STM32PWMInput::getPeriodTicks(){ + if (useChannel2) + return timer.Instance->CCR2; + else + return timer.Instance->CCR1; +}; + + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h new file mode 100644 index 0000000..fcf3acd --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + + +class STM32PWMInput { + public: + STM32PWMInput(int pin); + ~STM32PWMInput(); + + int initialize(); + + float getDutyCyclePercent(); + uint32_t getDutyCycleTicks(); + uint32_t getPeriodTicks(); + + PinName _pin; + protected: + TIM_HandleTypeDef timer; + bool useChannel2 = false; +}; + + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp new file mode 100644 index 0000000..714a0cb --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp @@ -0,0 +1,44 @@ + +#include "GenericVoltageSense.h" +#include "Arduino.h" + + + +GenericVoltageSense::GenericVoltageSense(int pin, float gain, float offset, float Tf, float fullScaleVoltage) : VoltageSense(gain, offset, Tf), fullScaleVoltage(fullScaleVoltage), pin(pin) { +}; + +#ifndef INPUT_ANALOG +#define INPUT_ANALOG INPUT +#endif + +#ifndef ADC_RESOLUTION +#define ADC_RESOLUTION 10 +#endif + + +bool GenericVoltageSense::init(int resolution){ + pinMode(pin, INPUT_ANALOG); +#if !defined(ARDUINO_ARCH_AVR) && !defined(ARDUINO_ARCH_MEGAAVR) + if (resolution>0) { + analogReadResolution(resolution); + maxValue = _powtwo(resolution); + } + else { + maxValue = _powtwo(ADC_RESOLUTION); + } +#else + maxValue = _powtwo(ADC_RESOLUTION); +#endif + return true; +}; + + + + + +float GenericVoltageSense::readRawVoltage(){ + float val = analogRead(pin); + val = (val / (float)maxValue) * fullScaleVoltage; + return val; +}; + diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h new file mode 100644 index 0000000..673162a --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h @@ -0,0 +1,23 @@ + +#ifndef __GENERICVOLTAGESENSE_H__ +#define __GENERICVOLTAGESENSE_H__ + +#include "./VoltageSense.h" +#include "common/foc_utils.h" + +class GenericVoltageSense : public VoltageSense { + public: + GenericVoltageSense(int pin, float gain = 1.0f, float offset = 0.0f, float Tf = 0.1f, float fullScaleVoltage = 3.3f); + bool init(int resolution = -1); + float fullScaleVoltage; + protected: + float readRawVoltage() override; + int maxValue; + int pin; +}; + + + + + +#endif diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.cpp b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.cpp new file mode 100644 index 0000000..725d16f --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.cpp @@ -0,0 +1,23 @@ + + +#include "./VoltageSense.h" + + + +VoltageSense::VoltageSense(float gain, float offset, float Tf) : filter(Tf), gain(gain), offset(offset) { + voltage = 0.0f; +}; + + + +float VoltageSense::getVoltage(){ + return voltage; +}; + + + +void VoltageSense::update(){ + float v = readRawVoltage(); + v = (v - offset) * gain; + voltage = filter(v); +}; diff --git a/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.h b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.h new file mode 100644 index 0000000..08ebfec --- /dev/null +++ b/.pio/libdeps/aioli-foc/SimpleFOCDrivers/src/voltage/VoltageSense.h @@ -0,0 +1,24 @@ +#ifndef __VOLTAGESENSE_H__ +#define __VOLTAGESENSE_H__ + +#include "common/lowpass_filter.h" + +class VoltageSense { + public: + VoltageSense(float gain = 1.0f, float offset = 0.0f, float Tf = 0.1f); + float getVoltage(); + virtual void update(); + + LowPassFilter filter; + float gain; + float offset; + + protected: + virtual float readRawVoltage() = 0; + + float voltage; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/aioli-foc/integrity.dat b/.pio/libdeps/aioli-foc/integrity.dat new file mode 100644 index 0000000..03a2f3d --- /dev/null +++ b/.pio/libdeps/aioli-foc/integrity.dat @@ -0,0 +1,2 @@ +simplefoc/SimpleFOCDrivers@^1.0.2 +askuric/Simple FOC@^2.2.3 \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md b/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3d352a1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: possible bug +assignees: '' + +--- + +This is a simplified template, feel free to change it if it does not fit your case. + +**Describe the bug** +A clear and concise description of what the bug is. + +**Describe the hardware setup** +For us it is very important to know what is the hardware setup you're using in order to be able to help more directly +- Which motor +- Which driver +- Which microcontroller +- Which position sensor +- Current sensing used? + +**IDE you are using** +- Arduino IDE +- Platformio +- Something else + +**Tried the Getting started guide? - if applicable** +Have you tried the getting started guide and at which step are you blocked in diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md b/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..df55847 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +Description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +Description of what you want to happen. + +**Describe alternatives you've considered** +Description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/.github/workflows/ccpp.yml b/.pio/libdeps/lemon-pepper/Simple FOC/.github/workflows/ccpp.yml new file mode 100644 index 0000000..5c50bb1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/.github/workflows/ccpp.yml @@ -0,0 +1,109 @@ +name: Library Compile +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update + project-type: library + build: + name: Test compiling + runs-on: ubuntu-latest + + strategy: + matrix: + arduino-boards-fqbn: + - arduino:avr:uno # arudino uno + - arduino:sam:arduino_due_x # arduino due + - arduino:avr:mega # arduino mega2650 + - arduino:avr:leonardo # arduino leonardo + - arduino:samd:nano_33_iot # samd21 + - adafruit:samd:adafruit_metro_m4 # samd51 + - esp32:esp32:esp32doit-devkit-v1 # esp32 + - esp32:esp32:esp32s2 # esp32s2 + - esp32:esp32:esp32s3 # esp32s3 + - STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # stm32 bluepill + - STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # stm32 nucleo + - STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX # stm32f405 - odrive + - STMicroelectronics:stm32:GenL4:pnum=GENERIC_L475RGTX # stm32l475 + - STMicroelectronics:stm32:Disco:pnum=B_G431B_ESC1 # B-G431-ESC1 + - arduino:mbed_rp2040:pico # rpi pico + + include: + - arduino-boards-fqbn: arduino:avr:uno # arudino uno - compiling almost all examples + sketch-names: '**.ino' + required-libraries: PciManager + sketches-exclude: full_control_serial, angle_control, bluepill_position_control, esp32_position_control, esp32_i2c_dual_bus_example, stm32_i2c_dual_bus_example, magnetic_sensor_spi_alt_example, osc_esp32_3pwm, osc_esp32_fullcontrol, nano33IoT_velocity_control, smartstepper_control,esp32_current_control_low_side, stm32_spi_alt_example, esp32_spi_alt_example, B_G431B_ESC1, odrive_example_spi, odrive_example_encoder, single_full_control_example, double_full_control_example, stm32_current_control_low_side, open_loop_velocity_6pwm + + - arduino-boards-fqbn: arduino:sam:arduino_due_x # arduino due - one full example + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: arduino:avr:leonardo # arduino leonardo - one full example + sketch-names: open_loop_position_example.ino + + - arduino-boards-fqbn: arduino:avr:mega # arduino mega2660 - one full example + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: arduino:samd:nano_33_iot # samd21 + sketch-names: nano33IoT_velocity_control.ino, smartstepper_control.ino + + - arduino-boards-fqbn: arduino:mbed_rp2040:pico # raspberry pi pico - one example + sketch-names: open_loop_position_example.ino + + - arduino-boards-fqbn: adafruit:samd:adafruit_metro_m4 # samd51 - one full example + platform-url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + sketch-names: single_full_control_example.ino + + - arduino-boards-fqbn: esp32:esp32:esp32s2 # esp32s2 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: bldc_driver_3pwm_standalone.ino, stepper_driver_2pwm_standalone.ino, stepper_driver_4pwm_standalone + + - arduino-boards-fqbn: esp32:esp32:esp32s3 # esp32s3 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: esp32_position_control.ino, esp32_i2c_dual_bus_example.ino + + - arduino-boards-fqbn: esp32:esp32:esp32doit-devkit-v1 # esp32 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + sketch-names: esp32_position_control.ino, esp32_i2c_dual_bus_example.ino, esp32_current_control_low_side.ino, esp32_spi_alt_example.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # bluepill - hs examples + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: bluepill_position_control.ino, stm32_i2c_dual_bus_example.ino, stm32_spi_alt_example.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:Disco:pnum=B_G431B_ESC1 # B-G431-ESC1 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: B_G431B_ESC1.ino + build-properties: + B_G431B_ESC1: + -DHAL_OPAMP_MODULE_ENABLED + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF4:pnum=GENERIC_F405RGTX # stm32f405 - odrive + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: odrive_example_encoder.ino, odrive_example_spi.ino, stm32_current_control_low_side.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:GenL4:pnum=GENERIC_L475RGTX # stm32l475 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: single_full_control_example.ino, stm32_spi_alt_example.ino, double_full_control_example.ino, stm32_current_control_low_side.ino + + - arduino-boards-fqbn: STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # nucleo one full example + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + sketch-names: single_full_control_example.ino, stm32_spi_alt_example.ino, double_full_control_example.ino, stm32_current_control_low_side.ino + + + # Do not cancel all jobs / architectures if one job fails + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@master + - name: Compile all examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} + required-libraries: ${{ matrix.required-libraries }} + platform-url: ${{ matrix.platform-url }} + sketch-names: ${{ matrix.sketch-names }} + sketches-exclude: ${{ matrix.sketches-exclude }} + build-properties: ${{ toJson(matrix.build-properties) }} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/.piopm b/.pio/libdeps/lemon-pepper/Simple FOC/.piopm new file mode 100644 index 0000000..d89173e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "Simple FOC", "version": "2.3.1", "spec": {"owner": "askuric", "id": 7229, "name": "Simple FOC", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/CITATION.cff b/.pio/libdeps/lemon-pepper/Simple FOC/CITATION.cff new file mode 100644 index 0000000..ec37686 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/CITATION.cff @@ -0,0 +1,48 @@ +cff-version: 1.0.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Skuric" + given-names: "Antun" + orcid: "https://orcid.org/0000-0002-3323-4482" +- family-names: "Bank" + given-names: "Hasan Sinan" + orcid: "https://orcid.org/0000-0002-0626-2664" +- family-names: "Unger" + given-names: "Richard" +- family-names: "Williams" + given-names: "Owen" +- family-names: "González-Reyes" + given-names: "David" + orcid: "https://orcid.org/0000-0002-1535-3007" +title: "SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors" +version: 2.2.2 +doi: 10.21105/joss.04232 +date-released: 2022-06-26 +url: "https://github.com/simplefoc/Arduino-FOC" + +preferred-citation: + type: article + authors: + - family-names: "Skuric" + given-names: "Antun" + orcid: "https://orcid.org/0000-0002-3323-4482" + - family-names: "Bank" + given-names: "Hasan Sinan" + orcid: "https://orcid.org/0000-0002-0626-2664" + - family-names: "Unger" + given-names: "Richard" + - family-names: "Williams" + given-names: "Owen" + - family-names: "González-Reyes" + given-names: "David" + orcid: "https://orcid.org/0000-0002-1535-3007" + doi: "10.21105/joss.04232" + journal: "Journal of Open Source Software" + url: "https://doi.org/10.21105/joss.04232" + month: 6 + start: 4232 # First page number + end: 4232 # Last page number + title: "SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors" + volume: 7 + issue: 74 + year: 2022 diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/CODE_OF_CONDUCT.md b/.pio/libdeps/lemon-pepper/Simple FOC/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..88763e9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@simplefoc.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/LICENSE b/.pio/libdeps/lemon-pepper/Simple FOC/LICENSE new file mode 100644 index 0000000..4a32ba6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Antun Skuric + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/README.md b/.pio/libdeps/lemon-pepper/Simple FOC/README.md new file mode 100644 index 0000000..747b0f3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/README.md @@ -0,0 +1,223 @@ +# SimpleFOClibrary - **Simple** Field Oriented Control (FOC) **library**
+### A Cross-Platform FOC implementation for BLDC and Stepper motors
based on the Arduino IDE and PlatformIO + +![Library Compile](https://github.com/simplefoc/Arduino-FOC/workflows/Library%20Compile/badge.svg) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/simplefoc/arduino-foc) +![GitHub Release Date](https://img.shields.io/github/release-date/simplefoc/arduino-foc?color=blue) +![GitHub commits since tagged version](https://img.shields.io/github/commits-since/simplefoc/arduino-foc/latest/dev) +![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/simplefoc/arduino-foc/dev) + +[![arduino-library-badge](https://www.ardu-badge.com/badge/Simple%20FOC.svg?)](https://www.ardu-badge.com/badge/Simple%20FOC.svg) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![status](https://joss.theoj.org/papers/4382445f249e064e9f0a7f6c1bb06b1d/status.svg)](https://joss.theoj.org/papers/4382445f249e064e9f0a7f6c1bb06b1d) + + +We live in very exciting times 😃! BLDC motors are entering the hobby community more and more and many great projects have already emerged leveraging their far superior dynamics and power capabilities. BLDC motors have numerous advantages over regular DC motors but they have one big disadvantage, the complexity of control. Even though it has become relatively easy to design and manufacture PCBs and create our own hardware solutions for driving BLDC motors the proper low-cost solutions are yet to come. One of the reasons for this is the apparent complexity of writing the BLDC driving algorithms, Field oriented control (FOC) being an example of one of the most efficient ones. +The solutions that can be found on-line are almost exclusively very specific for certain hardware configuration and the microcontroller architecture used. +Additionally, most of the efforts at this moment are still channeled towards the high-power applications of the BLDC motors and proper low-cost and low-power FOC supporting boards are very hard to find today and even may not exist.
+Therefore this is an attempt to: +- 🎯 Demystify FOC algorithm and make a robust but simple Arduino library: [Arduino *SimpleFOClibrary*](https://docs.simplefoc.com/arduino_simplefoc_library_showcase) + - Support as many motor + sensor + driver + mcu combinations out there +- 🎯 Develop a modular FOC supporting BLDC driver boards: + - ***NEW*** 📢: *Minimalistic* BLDC driver (<3Amps) : [SimpleFOCMini ](https://github.com/simplefoc/SimpleFOCMini). + - *Low-power* gimbal driver (<5Amps) : [*Arduino Simple**FOC**Shield*](https://docs.simplefoc.com/arduino_simplefoc_shield_showcase). + - *Medium-power* BLDC driver (<30Amps): [Arduino SimpleFOCPowerShield ](https://github.com/simplefoc/Arduino-SimpleFOC-PowerShield). + - See also [@byDagor](https://github.com/byDagor)'s *fully-integrated* ESP32 based board: [Dagor Brushless Controller](https://github.com/byDagor/Dagor-Brushless-Controller) + +> NEW RELEASE 📢 : SimpleFOClibrary v2.3.1 +> - Support for Arduino UNO R4 Minima (Renesas R7FA4M1 MCU - note UNO R4 WiFi is not yet supported) +> - Support setting PWM polarity on ESP32 (thanks to [@mcells](https://github.com/mcells)) +> - Expose I2C errors in MagneticSensorI2C (thanks to [@padok](https://github.com/padok)) +> - Improved default trig functions (sine, cosine) - faster, smaller +> - Overridable trig functions - plug in your own optimized versions +> - Bugfix: microseconds overflow in velocity mode [#287](https://github.com/simplefoc/Arduino-FOC/issues/287) +> - Bugfix: KV initialization ([5fc3128](https://github.com/simplefoc/Arduino-FOC/commit/5fc3128d282b65c141ca486327c6235089999627)) +> - And more bugfixes - see the [complete list of 2.3.1 fixes here](https://github.com/simplefoc/Arduino-FOC/issues?q=is%3Aissue+milestone%3A2.3.1_Release) +> - Change: simplify initFOC() API ([d57d32d](https://github.com/simplefoc/Arduino-FOC/commit/d57d32dd8715dbed4e476469bc3de0c052f1d531). [5231e5e](https://github.com/simplefoc/Arduino-FOC/commit/5231e5e1d044b0cc33ede67664b6ef2f9d0a8cdf), [10c5b87](https://github.com/simplefoc/Arduino-FOC/commit/10c5b872672cab72df16ddd738bbf09bcce95d28)) +> - Change: check for linked driver in currentsense and exit gracefully ([5ef4d9d](https://github.com/simplefoc/Arduino-FOC/commit/5ef4d9d5a92e03da0dd5af7f624243ab30f1b688)) +> - Compatibility with newest versions of Arduino framework for STM32, Renesas, ESP32, Atmel SAM, Atmel AVR, nRF52 and RP2040 + +## Arduino *SimpleFOClibrary* v2.3.1 + +

+ + + +

+ +This video demonstrates the *Simple**FOC**library* basic usage, electronic connections and shows its capabilities. + +### Features +- **Easy install**: + - Arduino IDE: Arduino Library Manager integration + - PlatformIO +- **Open-Source**: Full code and documentation available on github +- **Goal**: + - Support as many [sensor](https://docs.simplefoc.com/position_sensors) + [motor](https://docs.simplefoc.com/motors) + [driver](https://docs.simplefoc.com/drivers) + [current sense](https://docs.simplefoc.com/current_sense) combination as possible. + - Provide the up-to-date and in-depth documentation with API references and the examples +- **Easy to setup and configure**: + - Easy hardware configuration + - Each hardware component is a C++ object (easy to understand) + - Easy [tuning the control loops](https://docs.simplefoc.com/motion_control) + - [*Simple**FOC**Studio*](https://docs.simplefoc.com/studio) configuration GUI tool + - Built-in communication and monitoring +- **Cross-platform**: + - Seamless code transfer from one microcontroller family to another + - Supports multiple [MCU architectures](https://docs.simplefoc.com/microcontrollers): + - Arduino: UNO R4, UNO, MEGA, DUE, Leonardo, Nano, Nano33 .... + - STM32 + - ESP32 + - Teensy + - many more ... + +

+ + +## Documentation +Full API code documentation as well as example projects and step by step guides can be found on our [docs website](https://docs.simplefoc.com/). + +![image](https://user-images.githubusercontent.com/36178713/168475410-105e4e3d-082a-4015-98ff-d380c7992dfd.png) + + +## Getting Started +Depending on if you want to use this library as the plug and play Arduino library or you want to get insight in the algorithm and make changes there are two ways to install this code. + +- Full library installation [Docs](https://docs.simplefoc.com/library_download) +- PlatformIO [Docs](https://docs.simplefoc.com/library_platformio) + +### Arduino *SimpleFOClibrary* installation to Arduino IDE +#### Arduino Library Manager +The simplest way to get hold of the library is directly by using Arduino IDE and its integrated Library Manager. +- Open Arduino IDE and start Arduino Library Manager by clicking: `Tools > Manage Libraries...`. +- Search for `Simple FOC` library and install the latest version. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +#### Using Github website +- Go to the [github repository](https://github.com/simplefoc/Arduino-FOC) +- Click first on `Clone or Download > Download ZIP`. +- Unzip it and place it in `Arduino Libraries` folder. Windows: `Documents > Arduino > libraries`. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +#### Using terminal +- Open terminal and run +```sh +cd #Arduino libraries folder +git clone https://github.com/simplefoc/Arduino-FOC.git +``` +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC`. + +## Community and contributing + +For all the questions regarding the potential implementation, applications, supported hardware and similar please visit our [community forum](https://community.simplefoc.com) or our [discord server](https://discord.gg/kWBwuzY32n). + +It is always helpful to hear the stories/problems/suggestions of people implementing the code and you might find a lot of answered questions there already! + +### Github Issues & Pull requests + +Please do not hesitate to leave an issue if you have problems/advices/suggestions regarding the code! + +Pull requests are welcome, but let's first discuss them in [community forum](https://community.simplefoc.com)! + +If you'd like to contribute to this project but you are not very familiar with github, don't worry, let us know either by posting at the community forum , by posting a github issue or at our discord server. + +If you are familiar, we accept pull requests to the dev branch! + +## Arduino code example +This is a simple Arduino code example implementing the velocity control program of a BLDC motor with encoder. + +NOTE: This program uses all the default control parameters. + +```cpp +#include + +// BLDCMotor( pole_pairs ) +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver( pin_pwmA, pin_pwmB, pin_pwmC, enable (optional) ) +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +// Encoder(pin_A, pin_B, CPR) +Encoder encoder = Encoder(2, 3, 2048); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +void setup() { + // initialize encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage [V] + driver.voltage_power_supply = 12; + // initialise driver hardware + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::velocity; + // initialize motor + motor.init(); + + // align encoder and start FOC + motor.initFOC(); +} + +void loop() { + // FOC algorithm function + motor.loopFOC(); + + // velocity control loop function + // setting the target velocity or 2rad/s + motor.move(2); +} +``` +You can find more details in the [SimpleFOC documentation](https://docs.simplefoc.com/). + +## Example projects +Here are some of the *Simple**FOC**library* and *Simple**FOC**Shield* application examples. +

+ + + + + + + + + + + + +

+ + +## Citing the *SimpleFOC* + +We are very happy that *Simple**FOC**library* has been used as a component of several research project and has made its way to several scientific papers. We are hoping that this trend is going to continue as the project matures and becomes more robust! +A short resume paper about *Simple**FOC*** has been published in the Journal of Open Source Software: +

+ SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors.
+ A. Skuric, HS. Bank, R. Unger, O. Williams, D. González-Reyes
+Journal of Open Source Software, 7(74), 4232, https://doi.org/10.21105/joss.04232 +

+ +If you are interested in citing *Simple**FOC**library* or some other component of *Simple**FOC**project* in your research, we suggest you to cite our paper: + +```bib +@article{simplefoc2022, + doi = {10.21105/joss.04232}, + url = {https://doi.org/10.21105/joss.04232}, + year = {2022}, + publisher = {The Open Journal}, + volume = {7}, + number = {74}, + pages = {4232}, + author = {Antun Skuric and Hasan Sinan Bank and Richard Unger and Owen Williams and David González-Reyes}, + title = {SimpleFOC: A Field Oriented Control (FOC) Library for Controlling Brushless Direct Current (BLDC) and Stepper Motors}, + journal = {Journal of Open Source Software} +} + +``` diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino new file mode 100644 index 0000000..51ac990 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/B_G431B_ESC1.ino @@ -0,0 +1,109 @@ +/** + * B-G431B-ESC1 position motion control example with encoder + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(A_PHASE_UH, A_PHASE_UL, A_PHASE_VH, A_PHASE_VL, A_PHASE_WH, A_PHASE_WL); +LowsideCurrentSense currentSense = LowsideCurrentSense(0.003f, -64.0f/7.0f, A_OP1_OUT, A_OP2_OUT, A_OP3_OUT); + + +// encoder instance +Encoder encoder = Encoder(A_HALL2, A_HALL3, 2048, A_HALL1); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doIndex(){encoder.handleIndex();} + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.motion(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + currentSense.linkDriver(&driver); + + // current sensing + currentSense.init(); + // no need for aligning + currentSense.skip_align = true; + motor.linkCurrentSense(¤tSense); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + + // Motion control function + motor.move(); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h new file mode 100644 index 0000000..6f547ec --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/B_G431B_ESC1/build_opt.h @@ -0,0 +1 @@ +-DHAL_OPAMP_MODULE_ENABLED \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino new file mode 100644 index 0000000..be853f0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/encoder/bluepill_position_control/bluepill_position_control.ino @@ -0,0 +1,115 @@ +/** + * + * STM32 Bluepill position motion control example with encoder + * + * The same example can be ran with any STM32 board - just make sure that put right pin numbers. + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM(IN1, IN2, IN3, enable(optional)) +BLDCDriver3PWM driver = BLDCDriver3PWM(PB6, PB7, PB8, PB5); +// BLDCDriver6PWM(IN1_H, IN1_L, IN2_H, IN2_L, IN3_H, IN3_L, enable(optional)) +//BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, PB12); + +// encoder instance +Encoder encoder = Encoder(PA8, PA9, 8192, PA10); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doI(){encoder.handleIndex();} + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB, doI); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino new file mode 100644 index 0000000..e27f1e7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Bluepill_examples/magnetic_sensor/bluepill_position_control/bluepill_position_control.ino @@ -0,0 +1,115 @@ +/** + * + * STM32 Bluepill position motion control example with magnetic sensor + * + * The same example can be ran with any STM32 board - just make sure that put right pin numbers. + * + */ +#include + +// SPI Magnetic sensor instance (AS5047U example) +// MISO PA7 +// MOSI PA6 +// SCK PA5 +MagneticSensorSPI sensor = MagneticSensorSPI(PA4, 14, 0x3FFF); + +// I2C Magnetic sensor instance (AS5600 example) +// make sure to use the pull-ups!! +// SDA PB7 +// SCL PB6 +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM(IN1, IN2, IN3, enable(optional)) +BLDCDriver3PWM driver = BLDCDriver3PWM(PB6, PB7, PB8, PB5); +// BLDCDriver6PWM(IN1_H, IN1_L, IN2_H, IN2_L, IN3_H, IN3_L, enable(optional)) +//BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, PB12); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 40; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..842f383 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/3pwm_example/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,130 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 9 +#define INH_B 10 +#define INH_C 11 + +#define EN_GATE 7 +#define M_PWM A1 +#define M_OC A2 +#define OC_ADJ A3 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..a5b72f8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/6pwm_example/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,129 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 3 +#define INH_B 5 +#define INH_C 9 +#define INL_A 11 +#define INL_B 6 +#define INL_C 10 + +#define EN_GATE 7 +#define M_PWM A1 +#define M_OC A2 +#define OC_ADJ A3 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(INH_A,INH_A, INH_B,INH_B, INH_C,INL_C, EN_GATE); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - disable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM, LOW); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino new file mode 100644 index 0000000..84033b4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/esp32_current_control_low_side/esp32_current_control_low_side.ino @@ -0,0 +1,167 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A 21 +#define INH_B 19 +#define INH_C 18 + +#define EN_GATE 5 +#define M_PWM 25 +#define M_OC 26 +#define OC_ADJ 12 +#define OC_GAIN 14 + +#define IOUTA 34 +#define IOUTB 35 +#define IOUTC 32 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// DRV8302 board has 0.005Ohm shunt resistors and the gain of 12.22 V/V +LowsideCurrentSense cs = LowsideCurrentSense(0.005f, 12.22f, IOUTA, IOUTB, IOUTC); + +// encoder instance +Encoder encoder = Encoder(22, 23, 500); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + pinMode(OC_GAIN,OUTPUT); + digitalWrite(OC_GAIN,LOW); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.pwm_frequency = 15000; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + cs.linkDriver(&driver); + + // align voltage + motor.voltage_sensor_align = 0.5; + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + motor.motion_downsample = 0.0; + + // velocity loop PID + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 5.0; + // Low pass filtering time constant + motor.LPF_velocity.Tf = 0.02; + // angle loop PID + motor.P_angle.P = 20.0; + // Low pass filtering time constant + motor.LPF_angle.Tf = 0.0; + // current q loop PID + motor.PID_current_q.P = 3.0; + motor.PID_current_q.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_q.Tf = 0.02; + // current d loop PID + motor.PID_current_d.P = 3.0; + motor.PID_current_d.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_d.Tf = 0.02; + + // Limits + motor.velocity_limit = 100.0; // 100 rad/s velocity limit + motor.voltage_limit = 12.0; // 12 Volt limit + motor.current_limit = 2.0; // 2 Amp current limit + + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; // monitor the two currents d and q + motor.monitor_downsample = 1000; + + // initialise motor + motor.init(); + + cs.init(); + // driver 8302 has inverted gains on all channels + cs.gain_a *=-1; + cs.gain_b *=-1; + cs.gain_c *=-1; + motor.linkCurrentSense(&cs); + + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // monitoring the state variables + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino new file mode 100644 index 0000000..3e3f04a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/DRV8302_driver/stm32_current_control_low_side/stm32_current_control_low_side.ino @@ -0,0 +1,167 @@ +/** + * Comprehensive BLDC motor control example using encoder and the DRV8302 board + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * check the https://docs.simplefoc.com for full list of motor commands + * + */ +#include + +// DRV8302 pins connections +// don't forget to connect the common ground pin +#define INH_A PA8 +#define INH_B PA9 +#define INH_C PA10 + +#define EN_GATE PB7 +#define M_PWM PB4 +#define M_OC PB3 +#define OC_ADJ PB6 +#define OC_GAIN PB5 + +#define IOUTA PA0 +#define IOUTB PA1 +#define IOUTC PA2 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(INH_A, INH_B, INH_C, EN_GATE); + +// DRV8302 board has 0.005Ohm shunt resistors and the gain of 12.22 V/V +LowsideCurrentSense cs = LowsideCurrentSense(0.005f, 12.22f, IOUTA, IOUTB, IOUTC); + +// encoder instance +Encoder encoder = Encoder(PB14, PB15, 2048); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // DRV8302 specific code + // M_OC - enable overcurrent protection + pinMode(M_OC,OUTPUT); + digitalWrite(M_OC,LOW); + // M_PWM - enable 3pwm mode + pinMode(M_PWM,OUTPUT); + digitalWrite(M_PWM,HIGH); + // OD_ADJ - set the maximum overcurrent limit possible + // Better option would be to use voltage divisor to set exact value + pinMode(OC_ADJ,OUTPUT); + digitalWrite(OC_ADJ,HIGH); + pinMode(OC_GAIN,OUTPUT); + digitalWrite(OC_GAIN,LOW); + + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 19; + driver.pwm_frequency = 15000; // suggested under 18khz + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + // link current sense and the driver + cs.linkDriver(&driver); + + // align voltage + motor.voltage_sensor_align = 0.5; + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + motor.motion_downsample = 0.0; + + // velocity loop PID + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 5.0; + // Low pass filtering time constant + motor.LPF_velocity.Tf = 0.02; + // angle loop PID + motor.P_angle.P = 20.0; + // Low pass filtering time constant + motor.LPF_angle.Tf = 0.0; + // current q loop PID + motor.PID_current_q.P = 3.0; + motor.PID_current_q.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_q.Tf = 0.02; + // current d loop PID + motor.PID_current_d.P = 3.0; + motor.PID_current_d.I = 100.0; + // Low pass filtering time constant + motor.LPF_current_d.Tf = 0.02; + + // Limits + motor.velocity_limit = 100.0; // 100 rad/s velocity limit + motor.voltage_limit = 12.0; // 12 Volt limit + motor.current_limit = 2.0; // 2 Amp current limit + + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; // monitor the two currents d and q + motor.monitor_downsample = 0; + + // initialise motor + motor.init(); + + cs.init(); + // driver 8302 has inverted gains on all channels + cs.gain_a *=-1; + cs.gain_b *=-1; + cs.gain_c *=-1; + motor.linkCurrentSense(&cs); + + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // define the motor id + command.add('M', onMotor, "motor"); + + Serial.println(F("Full control example: ")); + Serial.println(F("Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) \n ")); + Serial.println(F("Initial motion control loop is voltage loop.")); + Serial.println(F("Initial target voltage 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // monitoring the state variables + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino new file mode 100644 index 0000000..f6dac94 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/encoder/esp32_position_control/esp32_position_control.ino @@ -0,0 +1,107 @@ +/** + * ESP32 position motion control example with encoder + * + */ +#include + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27, 7); + +// encoder instance +Encoder encoder = Encoder(4, 2, 1024); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino new file mode 100644 index 0000000..f025895 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/ESP32/magnetic_sensor/esp32_position_control/esp32_position_control.ino @@ -0,0 +1,112 @@ +/** + * ESP32 position motion control example with magnetic sensor + */ +#include + +// SPI Magnetic sensor instance (AS5047U example) +// MISO 12 +// MOSI 9 +// SCK 14 +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 15); + +// I2C Magnetic sensor instance (AS5600 example) +// make sure to use the pull-ups!! +// SDA 21 +// SCL 22 +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); + +// Analog output Magnetic sensor instance (AS5600) +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27, 7); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 40; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino new file mode 100644 index 0000000..5b5c8e4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/position_control/position_control.ino @@ -0,0 +1,127 @@ +/** + * + * HMBGC position motion control example with encoder + * + * - Motor is connected the MOT1 connector (MOT1 9,10,11; MOT2 3,5,6) + * - Encoder is connected to A0 and A1 + * + * This board doesn't have any interrupt pins so we need to run all the encoder channels with the software interrupt library + * - For this example we use: PciManager library : https://github.com/prampec/arduino-pcimanager + * + * See docs.simplefoc.com for more info. + * + */ +#include +// software interrupt library +#include +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11); + +// encoder instance +Encoder encoder = Encoder(A0, A1, 2048); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise encoder hardware + encoder.init(); + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino new file mode 100644 index 0000000..9c9655f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/HMBGC_example/voltage_control/voltage_control.ino @@ -0,0 +1,110 @@ +/** + * + * HMBGC torque control example using voltage control loop. + * + * - Motor is connected the MOT1 connector (MOT1 9,10,11; MOT2 3,5,6) + * - Encoder is connected to A0 and A1 + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. position motion control example with encoder + * + * NOTE: + * > HMBGC doesn't have any interrupt pins so we need to run all the encoder channels with the software interrupt library + * > - For this example we use: PciManager library : https://github.com/prampec/arduino-pcimanager + * + * See docs.simplefoc.com for more info. + * + */ +#include +// software interrupt library +#include +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11); + +// encoder instance +Encoder encoder = Encoder(A0, A1, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial for motor init + // comment out if not needed + motor.useMonitoring(Serial); + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino new file mode 100644 index 0000000..b89c215 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_encoder/odrive_example_encoder.ino @@ -0,0 +1,134 @@ +/* + Odrive robotics' hardware is one of the best BLDC motor foc supporting hardware out there. + + This is an example code that can be directly uploaded to the Odrive using the SWD programmer. + This code uses an encoder with 500 cpr and a BLDC motor with 7 pole pairs connected to the M0 interface of the Odrive. + + This is a short template code and the idea is that you are able to adapt to your needs not to be a complete solution. :D +*/ +#include + +// Odrive M0 motor pinout +#define M0_INH_A PA8 +#define M0_INH_B PA9 +#define M0_INH_C PA10 +#define M0_INL_A PB13 +#define M0_INL_B PB14 +#define M0_INL_C PB15 +// M0 currnets +#define M0_IB PC0 +#define M0_IC PC1 +// Odrive M0 encoder pinout +#define M0_ENC_A PB4 +#define M0_ENC_B PB5 +#define M0_ENC_Z PC9 + + +// Odrive M1 motor pinout +#define M1_INH_A PC6 +#define M1_INH_B PC7 +#define M1_INH_C PC8 +#define M1_INL_A PA7 +#define M1_INL_B PB0 +#define M1_INL_C PB1 +// M0 currnets +#define M1_IB PC2 +#define M1_IC PC3 +// Odrive M1 encoder pinout +#define M1_ENC_A PB6 +#define M1_ENC_B PB7 +#define M1_ENC_Z PC15 + +// M1 & M2 common enable pin +#define EN_GATE PB12 + +// SPI pinout +#define SPI3_SCL PC10 +#define SPI3_MISO PC11 +#define SPI3_MOSO PC12 + +// Motor instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver6PWM driver = BLDCDriver6PWM(M0_INH_A,M0_INL_A, M0_INH_B,M0_INL_B, M0_INH_C,M0_INL_C, EN_GATE); + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +// low side current sensing define +// 0.0005 Ohm resistor +// gain of 10x +// current sensing on B and C phases, phase A not connected +LowsideCurrentSense current_sense = LowsideCurrentSense(0.0005f, 10.0f, _NC, M0_IB, M0_IC); + +Encoder encoder = Encoder(M0_ENC_A, M0_ENC_B, 500,M0_ENC_Z); +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doI(){encoder.handleIndex();} + +void setup(){ + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 20000; + // power supply voltage [V] + driver.voltage_power_supply = 20; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 20; + // driver init + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB, doI); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // max voltage allowed for motion control + motor.voltage_limit = 8.0; + // alignment voltage limit + motor.voltage_sensor_align = 0.5; + + + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; + motor.monitor_downsample = 1000; + + // add target command T + command.add('M', doMotor, "motor M0"); + + // initialise motor + motor.init(); + + // link the driver + current_sense.linkDriver(&driver); + // init the current sense + current_sense.init(); + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // init FOC + motor.initFOC(); + delay(1000); +} + +void loop(){ + + // foc loop + motor.loopFOC(); + // motion control + motor.move(); + // monitoring + motor.monitor(); + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino new file mode 100644 index 0000000..2aed6bf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Odrive_examples/odrive_example_spi/odrive_example_spi.ino @@ -0,0 +1,132 @@ +/* + Odrive robotics' hardware is one of the best BLDC motor foc supporting hardware out there. + + This is an example code that can be directly uploaded to the Odrive using the SWD programmer. + This code uses an magnetic spi sensor AS5047 and a BLDC motor with 11 pole pairs connected to the M0 interface of the Odrive. + + This is a short template code and the idea is that you are able to adapt to your needs not to be a complete solution. :D +*/ +#include + +// Odrive M0 motor pinout +#define M0_INH_A PA8 +#define M0_INH_B PA9 +#define M0_INH_C PA10 +#define M0_INL_A PB13 +#define M0_INL_B PB14 +#define M0_INL_C PB15 +// M0 currnets +#define M0_IB PC0 +#define M0_IC PC1 +// Odrive M0 encoder pinout +#define M0_ENC_A PB4 +#define M0_ENC_B PB5 +#define M0_ENC_Z PC9 + + +// Odrive M1 motor pinout +#define M1_INH_A PC6 +#define M1_INH_B PC7 +#define M1_INH_C PC8 +#define M1_INL_A PA7 +#define M1_INL_B PB0 +#define M1_INL_C PB1 +// M0 currnets +#define M1_IB PC2 +#define M1_IC PC3 +// Odrive M1 encoder pinout +#define M1_ENC_A PB6 +#define M1_ENC_B PB7 +#define M1_ENC_Z PC15 + +// M1 & M2 common enable pin +#define EN_GATE PB12 + +// SPI pinout +#define SPI3_SCL PC10 +#define SPI3_MISO PC11 +#define SPI3_MOSO PC12 + +// Motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver6PWM driver = BLDCDriver6PWM(M0_INH_A,M0_INL_A, M0_INH_B,M0_INL_B, M0_INH_C,M0_INL_C, EN_GATE); + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +// low side current sensing define +// 0.0005 Ohm resistor +// gain of 10x +// current sensing on B and C phases, phase A not connected +LowsideCurrentSense current_sense = LowsideCurrentSense(0.0005f, 10.0f, _NC, M0_IB, M0_IC); + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, M0_ENC_A); +SPIClass SPI_3(SPI3_MOSO, SPI3_MISO, SPI3_SCL); + +void setup(){ + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 20000; + // power supply voltage [V] + driver.voltage_power_supply = 20; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 20; + // driver init + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // initialise magnetic sensor hardware + sensor.init(&SPI_3); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // control loop type and torque mode + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // max voltage allowed for motion control + motor.voltage_limit = 8.0; + // alignment voltage limit + motor.voltage_sensor_align = 0.5; + + + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_CURR_Q | _MON_CURR_D; + motor.monitor_downsample = 1000; + + // add target command T + command.add('M', doMotor, "motor M0"); + + // initialise motor + motor.init(); + + // link the driver + current_sense.linkDriver(&driver); + // init the current sense + current_sense.init(); + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // init FOC + motor.initFOC(); + delay(1000); +} + +void loop(){ + + // foc loop + motor.loopFOC(); + // motion control + motor.move(); + // monitoring + motor.monitor(); + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md new file mode 100644 index 0000000..d1a5258 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/README.md @@ -0,0 +1,62 @@ + +# SAMD Support + +SimpleFOC supports many SAMD21 MCUs, really any SAMD21 supported by Arduino core should work. + +## Pin assignments + +The SAMD chips have some very powerful PWM features, but do not have flexible pin assignments. + +You should be able to use *most* (but not all!), pin combinations for attaching your motor's PWM pins. Please ignore the board descriptions and pinout diagrammes regarding PWM-pins on SAMD boards. They are pretty much all incorrect to varying degrees of awfulness. + +On SAMD we use TCC and TC timer peripherals (built into the SAMD chip) to control the PWM. Depending on the chip there are various timer units, whose PWM outputs are attached to various different pins, and it is all very complicated. Luckily SimpleFOC sets it all up automatically *if* there is a compatible configuration for those pins. + +Not all timers are created equal. The TCC timers are pretty awesome for PWM motor control, while the TC timers are just ok for the job. So to get best performance, you want to use just TCC timer pins if you can. + +By enabling + +``` + #define SIMPLEFOC_SAMD_DEBUG +``` + +in drivers/hardware_specific/samd_mcu.cpp
+you will see a table of pin assignments printed on the serial console, as well as the timers SimpleFOC was able to find and configure on the pins you specified. You can use this to optimize your choice of pins if you want. + +You can configure up to 12 pins for PWM motor control, i.e. 6x 2-PWM motors, 4x 3-PWM motors, 3x 4-PWM motors or 2x 6-PWM motors. + +## PWM control modes + +All modes (3-PWM, 6-PWM, Stepper 2-PWM and Stepper 4-PWM) are supported. + +For 2-, 3- amd 4- PWM, any valid pin-combinations can be used. If you stick to TCC timers rather than using TC timers, then you'll get getter PWM waveforms. If you use pins which are all on the same TCC unit, you'll get the best result, with the PWM signals all perfectly aligned as well. + +For 6-PWM, the situation is much more complicated:
+TC timers cannot be used for 6-PWM, only TCC timers. + +For Hardware Dead-Time insertion, you must use H and L pins for one phase from the same TCC unit, and on the same channel, but using complementary WOs (Waveform Outputs, i.e. PWM output pins). Check the table to find pins on the same channel (like TCC0-0) but complementary WOs (like TCC0-0[0] and TCC0-0[4] or TCC1-0[0] and TCC1-0[2]). + +For Software Dead-Time insertion, you must use the same TCC and different channels for the H and L pins of the same phase. + +Note: in all of the above note that you *cannot* set the timers or WOs used - they are fixed, and determined by the pins you selected. SimpleFOC will find the best combination of timers given the pins, trying to use TCC timers before TC, and trying to keep things on the same timers as much as possible. If you configure multiple motors, it will take into account the pins already assigned to other motors. +So it is matter of choosing the right pins, nothing else. + +Note also: Unfortunately you can't set the PWM frequency. It is currently fixed at 24KHz. This is a tradeoff between limiting PWM resolution vs +increasing frequency, and also due to keeping the pin assignemts flexible, which would not be possible if we ran the timers at different rates. + +## Status + +Currently, SAMD21 is supported, and SAMD51 is unsupported. SAMD51 support is in progress. + +Boards tested: + + * Arduino Nano 33 IoT + * Arduino MKR1000 + * Arduino MKR1010 Wifi + * Seeduino XIAO + * Feather M0 Basic + +Environments tested: + + * Arduino IDE + * Arduino Pro IDE + * Sloeber diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino new file mode 100644 index 0000000..70ec28b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SAMD_examples/nano33IoT/nano33IoT_velocity_control/nano33IoT_velocity_control.ino @@ -0,0 +1,64 @@ + +// show the infos for SAMD pin assignment on serial console +// set this #define SIMPLEFOC_SAMD_DEBUG in drivers/hardware_specific/samd21_mcu.h + + +#include "Arduino.h" +#include +#include + +// this is for an AS5048B absolute magnetic encoder on I2C address 0x41 +MagneticSensorI2C sensor = MagneticSensorI2C(0x41, 14, 0xFE, 8); + +// small BLDC gimbal motor, 7 pole-pairs +BLDCMotor motor = BLDCMotor(7); +// 3-PWM driving on pins 6, 5 and 8 - these are all on the same timer unit (TCC0), but different channels +BLDCDriver3PWM driver = BLDCDriver3PWM(6,5,8); + +// velocity set point variable +float target_velocity = 2.0f; +// instantiate the commander +Commander command = Commander(SerialUSB); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("Initializing..."); + + sensor.init(); + Wire.setClock(400000); + motor.linkSensor(&sensor); + driver.voltage_power_supply = 9; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity; + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0.001f; + motor.PID_velocity.output_ramp = 1000; + motor.LPF_velocity.Tf = 0.01f; + motor.voltage_limit = 9; + //motor.P_angle.P = 20; + motor.init(); + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + delay(100); +} + + + +void loop() { +// Serial.print("Sensor: "); +// Serial.println(sensor.getAngle()); + motor.loopFOC(); + motor.move(target_velocity); + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..eb52f51 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOC-PowerShield/version_v02/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,102 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8, 4, 7); + +// encoder instance +Encoder encoder = Encoder(10, 11, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +InlineCurrentSense current_sense = InlineCurrentSense(0.001f, 50.0f, A0, A1); + +// commander communication instance +Commander command = Commander(Serial); +// void doMotor(char* cmd){ command.motor(&motor, cmd); } +void doTarget(char* cmd){ command.scalar(&motor.target, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 20; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + motor.voltage_sensor_align = 1; + // set control loop type to be used + motor.torque_controller = TorqueControlType::foc_current; + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 0; + + // subscribe motor to the commander + // command.add('M', doMotor, "motor"); + command.add('T', doTarget, "target"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino new file mode 100644 index 0000000..9102c89 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCMini/angle_control/angle_control.ino @@ -0,0 +1,125 @@ +/** + * + * SimpleFOCMini motor control example + * + * For Arduino UNO, the most convenient way to use the board is to stack it to the pins: + * - 12 - GND + * - 11 - IN1 + * - 10 - IN2 + * - 9 - IN3 + * - 8 - EN + * + * For other boards with UNO headers but more PWM channles such as esp32, nucleo-64, samd51 metro etc, the best way to most convenient pinout is: + * - GND - GND + * - 13 - IN1 + * - 12 - IN2 + * - 11 - IN3 + * - 9 - EN + * + * For the boards without arduino uno headers, the choice of pinout is a lot less constrained. + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(11, 10, 9, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// instantiate the commander +Commander command = Commander(Serial); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +void setup() { + // if SimpleFOCMini is stacked in arduino headers + // on pins 12,11,10,9,8 + // pin 12 is used as ground + pinMode(12,OUTPUT); + pinMode(12,LOW); + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command M + command.add('M', doMotor, "motor"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino new file mode 100644 index 0000000..c048956 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/double_full_control_example/double_full_control_example.ino @@ -0,0 +1,118 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor1 = BLDCMotor(11); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(9, 5, 6, 8); + +// BLDC motor & driver instance +BLDCMotor motor2 = BLDCMotor(11); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(3, 10, 11, 7); + +// encoder instance +Encoder encoder1 = Encoder(2, A3, 500); +// channel A and B callbacks +void doA1(){encoder1.handleA();} +void doB1(){encoder1.handleB();} + +// encoder instance +Encoder encoder2 = Encoder(A1, A2, 500); +// channel A and B callbacks +void doA2(){encoder2.handleA();} +void doB2(){encoder2.handleB();} + +// commander communication instance +Commander command = Commander(Serial); +void doMotor1(char* cmd){ command.motor(&motor1, cmd); } +void doMotor2(char* cmd){ command.motor(&motor2, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder1.init(); + encoder1.enableInterrupts(doA1, doB1); + // initialize encoder sensor hardware + encoder2.init(); + encoder2.enableInterrupts(doA2, doB2); + // link the motor to the sensor + motor1.linkSensor(&encoder1); + motor2.linkSensor(&encoder2); + + + // driver config + // power supply voltage [V] + driver1.voltage_power_supply = 12; + driver1.init(); + // link driver + motor1.linkDriver(&driver1); + // power supply voltage [V] + driver2.voltage_power_supply = 12; + driver2.init(); + // link driver + motor2.linkDriver(&driver2); + + // set control loop type to be used + motor1.controller = MotionControlType::torque; + motor2.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor1.PID_velocity.P = 0.05f; + motor1.PID_velocity.I = 1; + motor1.PID_velocity.D = 0; + // default voltage_power_supply + motor1.voltage_limit = 12; + // contoller configuration based on the controll type + motor2.PID_velocity.P = 0.05f; + motor2.PID_velocity.I = 1; + motor2.PID_velocity.D = 0; + // default voltage_power_supply + motor2.voltage_limit = 12; + + // angle loop velocity limit + motor1.velocity_limit = 20; + motor2.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor1.useMonitoring(Serial); + motor2.useMonitoring(Serial); + + // initialise motor + motor1.init(); + // align encoder and start FOC + motor1.initFOC(); + + // initialise motor + motor2.init(); + // align encoder and start FOC + motor2.initFOC(); + + // set the inital target value + motor1.target = 2; + motor2.target = 2; + + // subscribe motor to the commander + command.add('A', doMotor1, "motor 1"); + command.add('B', doMotor2, "motor 2"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Double motor sketch ready.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor1.loopFOC(); + motor2.loopFOC(); + + // iterative function setting the outter loop target + motor1.move(); + motor2.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..10e26c9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v1/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,89 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// commander communication instance +Commander command = Commander(Serial); +void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino new file mode 100644 index 0000000..b40ab62 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/double_full_control_example/double_full_control_example.ino @@ -0,0 +1,144 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor1 = BLDCMotor(11); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(5, 10, 6, 8); + +// BLDC motor & driver instance +BLDCMotor motor2 = BLDCMotor(11); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(3, 9, 11, 7); + +// encoder instance +Encoder encoder1 = Encoder(12, 2, 500); +// channel A and B callbacks +void doA1(){encoder1.handleA();} +void doB1(){encoder1.handleB();} + +// encoder instance +Encoder encoder2 = Encoder(A5, A4, 500); +// channel A and B callbacks +void doA2(){encoder2.handleA();} +void doB2(){encoder2.handleB();} + + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense1 = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense2 = InlineCurrentSense(0.01f, 50.0f, A1, A3); + +// commander communication instance +Commander command = Commander(Serial); +// void doMotor1(char* cmd){ command.motor(&motor1, cmd); } +// void doMotor2(char* cmd){ command.motor(&motor2, cmd); } +void doTarget1(char* cmd){ command.scalar(&motor1.target, cmd); } +void doTarget2(char* cmd){ command.scalar(&motor2.target, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder1.init(); + encoder1.enableInterrupts(doA1, doB1); + // initialize encoder sensor hardware + encoder2.init(); + encoder2.enableInterrupts(doA2, doB2); + // link the motor to the sensor + motor1.linkSensor(&encoder1); + motor2.linkSensor(&encoder2); + + + // driver config + // power supply voltage [V] + driver1.voltage_power_supply = 12; + driver1.init(); + // link driver + motor1.linkDriver(&driver1); + // link current sense and the driver + current_sense1.linkDriver(&driver1); + + // power supply voltage [V] + driver2.voltage_power_supply = 12; + driver2.init(); + // link driver + motor2.linkDriver(&driver2); + // link current sense and the driver + current_sense2.linkDriver(&driver2); + + // set control loop type to be used + motor1.controller = MotionControlType::torque; + motor2.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor1.PID_velocity.P = 0.05f; + motor1.PID_velocity.I = 1; + motor1.PID_velocity.D = 0; + // default voltage_power_supply + motor1.voltage_limit = 12; + // contoller configuration based on the controll type + motor2.PID_velocity.P = 0.05f; + motor2.PID_velocity.I = 1; + motor2.PID_velocity.D = 0; + // default voltage_power_supply + motor2.voltage_limit = 12; + + // angle loop velocity limit + motor1.velocity_limit = 20; + motor2.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor1.useMonitoring(Serial); + motor2.useMonitoring(Serial); + + + // current sense init and linking + current_sense1.init(); + motor1.linkCurrentSense(¤t_sense1); + // current sense init and linking + current_sense2.init(); + motor2.linkCurrentSense(¤t_sense2); + + // initialise motor + motor1.init(); + // align encoder and start FOC + motor1.initFOC(); + + // initialise motor + motor2.init(); + // align encoder and start FOC + motor2.initFOC(); + + // set the inital target value + motor1.target = 2; + motor2.target = 2; + + // subscribe motor to the commander + // command.add('A', doMotor1, "motor 1"); + // command.add('B', doMotor2, "motor 2"); + command.add('A', doTarget1, "target 1"); + command.add('B', doTarget2, "target 2"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motors ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor1.loopFOC(); + motor2.loopFOC(); + + // iterative function setting the outter loop target + motor1.move(); + motor2.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..76a7296 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v2/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,101 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +// check if your board has R010 (0.01 ohm resistor) or R006 (0.006 mOhm resistor) +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// commander communication instance +Commander command = Commander(Serial); +void doMotion(char* cmd){ command.motion(&motor, cmd); } +// void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('T', doMotion, "motion control"); + // command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino new file mode 100644 index 0000000..0259328 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/SimpleFOCShield/version_v3/single_full_control_example/single_full_control_example.ino @@ -0,0 +1,101 @@ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(6, 10, 5, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// inline current sensor instance +// ACS712-05B has the resolution of 0.185mV per Amp +InlineCurrentSense current_sense = InlineCurrentSense(1.0f, 0.185f, A0, A2); + +// commander communication instance +Commander command = Commander(Serial); +void doMotion(char* cmd){ command.motion(&motor, cmd); } +// void doMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // controller configuration based on the control type + motor.PID_velocity.P = 0.05f; + motor.PID_velocity.I = 1; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 20; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_downsample = 0; // disable intially + motor.monitor_variables = _MON_TARGET | _MON_VEL | _MON_ANGLE; // monitor target velocity and angle + + // current sense init and linking + current_sense.init(); + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // subscribe motor to the commander + command.add('T', doMotion, "motion control"); + // command.add('M', doMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println("Motor ready."); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + motor.move(); + + // motor monitoring + motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino new file mode 100644 index 0000000..80f2fe6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Smart_Stepper/smartstepper_control/smartstepper_control.ino @@ -0,0 +1,61 @@ +/** + * Smart Stepper support with SimpleFOClibrary + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, A2); + +// Stepper motor & driver instance +StepperMotor motor = StepperMotor(50); +int in1[2] = {5, 6}; +int in2[2] = {A4, 7}; +StepperDriver2PWM driver = StepperDriver2PWM(4, in1, 9, in2); + +// instantiate the commander +Commander command = Commander(SerialUSB); +void doMotor(char* cmd) { command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with SerialUSB + SerialUSB.begin(115200); + // comment out if not needed + motor.useMonitoring(SerialUSB); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command M + command.add('M', doMotor, "my motor"); + + SerialUSB.println(F("Motor ready.")); + SerialUSB.println(F("Set the target voltage using Serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..7f59551 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,41 @@ +// 6pwm standalone example code for Teensy 3.x boards +#include + + +// BLDC driver instance +// using FTM0 timer +BLDCDriver6PWM driver = BLDCDriver6PWM(22,23, 9,10, 6,20, 8); +// using FTM3 timer - available on Teensy3.5 and Teensy3.6 +// BLDCDriver6PWM driver = BLDCDriver6PWM(2,14, 7,8, 35,36, 8); + +void setup() { + Serial.begin(115200); + // Enable debugging + // Driver init will show debugging output + SimpleFOCDebug::enable(&Serial); + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 30000; + // dead zone percentage of the duty cycle - default 0.02 - 2% + driver.dead_zone=0.02; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino new file mode 100644 index 0000000..40924ee --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy3/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino @@ -0,0 +1,65 @@ +// 6pwm openloop velocity example +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// using FTM0 timer +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +BLDCDriver6PWM driver = BLDCDriver6PWM(22,23, 9,10, 6,20, 8); +// using FTM3 timer - available on Teensy3.5 and Teensy3.6 +// BLDCDriver6PWM driver = BLDCDriver6PWM(2,14, 7,8, 35,36, 8); + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&motor.target, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + //initial motor target + motor.target=0; + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..f3df3db --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,57 @@ +// 6pwm standalone example code for Teensy 4.x boards +// +// Teensy4.x 6pwm driver generates a 6pwm signal using FlexTimers and it doesn't support the QuadTimers +// - Each high-low pair of the 6pwm has to be A and B channels of the same FlexTimer and to the same submodule +// +// List of available teensy 4.1 pins with their respective submodules and channels +// FlexPWM(timer number)_(submodule)_(channel) +// FlexPWM4_2_A pin 2 +// FlexPWM4_2_B pin 3 +// FlexPWM1_3_B pin 7 +// FlexPWM1_3_A pin 8 +// FlexPWM2_2_A pin 6 +// FlexPWM2_2_B pin 9 +// FlexPWM3_1_B pin 28 +// FlexPWM3_1_A pin 29 +// FlexPWM2_3_A pin 36 +// FlexPWM2_3_B pin 37 +#include + + +// BLDC driver instance +// make sure to provide channel A for high side and channel B for low side +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +// Example configuration +BLDCDriver6PWM driver = BLDCDriver6PWM(2,3, 6,9, 8,7); + +void setup() { + Serial.begin(115200); + // Enable debugging + // Driver init will show debugging output + SimpleFOCDebug::enable(&Serial); + + // pwm frequency to be used [Hz] + driver.pwm_frequency = 30000; + // dead zone percentage of the duty cycle - default 0.02 - 2% + driver.dead_zone=0.02; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino new file mode 100644 index 0000000..9c6eea0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/hardware_specific_examples/Teensy/Teensy4/open_loop_velocity_6pwm/open_loop_velocity_6pwm.ino @@ -0,0 +1,80 @@ +// 6pwm openloop velocity example +// +// Teensy4.x 6pwm driver generates a 6pwm signal using FlexTimers and it doesn't support the QuadTimers +// - Each high-low pair of the 6pwm has to be A and B channels of the same FlexTimer and to the same submodule +// +// List of available teensy 4.1 pins with their respective submodules and channels +// FlexPWM(timer number)_(submodule)_(channel) +// FlexPWM4_2_A pin 2 +// FlexPWM4_2_B pin 3 +// FlexPWM1_3_B pin 7 +// FlexPWM1_3_A pin 8 +// FlexPWM2_2_A pin 6 +// FlexPWM2_2_B pin 9 +// FlexPWM3_1_B pin 28 +// FlexPWM3_1_A pin 29 +// FlexPWM2_3_A pin 36 +// FlexPWM2_3_B pin 37 +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// make sure to provide channel A for high side and channel B for low side +// BLDCDriver6PWM(pwmA_H, pwmA_L, pwmB_H,pwmB_L, pwmC_H, pwmC_L) +// Example configuration +BLDCDriver6PWM driver = BLDCDriver6PWM(2,3, 6,9, 8,7); + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&motor.target, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + //initial motor target + motor.target=0; + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino new file mode 100644 index 0000000..5a915dd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_position_example/open_loop_position_example.ino @@ -0,0 +1,70 @@ +// Open loop motor control example + #include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM driver = BLDCDriver3PWM(pwmA, pwmB, pwmC, Enable(optional)); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +//target variable +float target_position = 0; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_position, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } +void doVelocity(char* cmd) { command.scalar(&motor.velocity_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // currnet = resistance*voltage, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + // limit/set the velocity of the transition in between + // target angles + motor.velocity_limit = 5; // [rad/s] cca 50rpm + // open loop control config + motor.controller = MotionControlType::angle_openloop; + + // init motor hardware + motor.init(); + + // add target command T + command.add('T', doTarget, "target angle"); + command.add('L', doLimit, "voltage limit"); + command.add('V', doLimit, "movement velocity"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target position [rad]"); + _delay(1000); +} + +void loop() { + // open loop angle movements + // using motor.voltage_limit and motor.velocity_limit + // angles can be positive or negative, negative angles correspond to opposite motor direction + motor.move(target_position); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino new file mode 100644 index 0000000..43fc6f7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/open_loop_motor_control/open_loop_velocity_example/open_loop_velocity_example.ino @@ -0,0 +1,68 @@ +// Open loop motor control example +#include + + +// BLDC motor & driver instance +// BLDCMotor motor = BLDCMotor(pole pair number); +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver3PWM driver = BLDCDriver3PWM(pwmA, pwmB, pwmC, Enable(optional)); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + + +//target variable +float target_velocity = 0; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } +void doLimit(char* cmd) { command.scalar(&motor.voltage_limit, cmd); } + +void setup() { + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + // limit the maximal dc voltage the driver can set + // as a protection measure for the low-resistance motors + // this value is fixed on startup + driver.voltage_limit = 6; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // limiting motor movements + // limit the voltage to be set to the motor + // start very low for high resistance motors + // current = voltage / resistance, so try to be well under 1Amp + motor.voltage_limit = 3; // [V] + + // open loop control config + motor.controller = MotionControlType::velocity_openloop; + + // init motor hardware + motor.init(); + + // add target command T + command.add('T', doTarget, "target velocity"); + command.add('L', doLimit, "voltage limit"); + + Serial.begin(115200); + Serial.println("Motor ready!"); + Serial.println("Set target velocity [rad/s]"); + _delay(1000); +} + +void loop() { + + // open loop velocity movement + // using motor.voltage_limit and motor.velocity_limit + // to turn the motor "backwards", just set a negative target_velocity + motor.move(target_velocity); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino new file mode 100644 index 0000000..ccb3980 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/encoder/angle_control/angle_control.ino @@ -0,0 +1,129 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and encoder + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + * + * NOTE : + * > Arduino UNO example code for running velocity motion control using an encoder with index significantly + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the encoder.enableInterrupts(doA,doB,doIndex) and avoid using PciManger + * + * > If you don't want to use index pin initialize the encoder class without index pin number: + * > For example: + * > - Encoder encoder = Encoder(2, 3, 8192); + * > and initialize interrupts like this: + * > - encoder.enableInterrupts(doA,doB) + * + * Check the docs.simplefoc.com for more info about the possible encoder configuration. + * + */ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino new file mode 100644 index 0000000..4bd7163 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/hall_sensor/angle_control/angle_control.ino @@ -0,0 +1,133 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and hall sensor + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + * + * NOTE : + * > This is for Arduino UNO example code for running angle motion control specifically + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the sensr.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenC(sensor.pinC, doC); + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialize sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 4; + + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino new file mode 100644 index 0000000..752030c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/position_motion_control/magnetic_sensor/angle_control/angle_control.ino @@ -0,0 +1,112 @@ +/** + * + * Position/angle motion control example + * Steps: + * 1) Configure the motor and magnetic sensor + * 2) Run the code + * 3) Set the target angle (in radians) from serial terminal + * + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// angle set point variable +float target_angle = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_angle, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // maximal voltage to be set to the motor + motor.voltage_limit = 6; + + // velocity low pass filtering time constant + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 20; + // maximal velocity of the position control + motor.velocity_limit = 20; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target angle"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target angle using serial terminal:")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_angle); + + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino new file mode 100644 index 0000000..c912340 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/current_control/current_control.ino @@ -0,0 +1,107 @@ +/** + * + * Torque control example using current control loop. + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// current sensor +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + +// current set point variable +float target_current = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_current, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + // link current sense and the driver + current_sense.linkDriver(&driver); + + // current sense init hardware + current_sense.init(); + // link the current sense to the motor + motor.linkCurrentSense(¤t_sense); + + // set torque mode: + // TorqueControlType::dc_current + // TorqueControlType::voltage + // TorqueControlType::foc_current + motor.torque_controller = TorqueControlType::foc_current; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // foc currnet control parameters (Arduino UNO/Mega) + motor.PID_current_q.P = 5; + motor.PID_current_q.I= 300; + motor.PID_current_d.P= 5; + motor.PID_current_d.I = 300; + motor.LPF_current_q.Tf = 0.01f; + motor.LPF_current_d.Tf = 0.01f; + // foc currnet control parameters (stm/esp/due/teensy) + // motor.PID_current_q.P = 5; + // motor.PID_current_q.I= 1000; + // motor.PID_current_d.P= 5; + // motor.PID_current_d.I = 1000; + // motor.LPF_current_q.Tf = 0.002f; // 1ms default + // motor.LPF_current_d.Tf = 0.002f; // 1ms default + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target current"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target current using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or torque (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_current); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino new file mode 100644 index 0000000..66ff456 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/encoder/voltage_control/voltage_control.ino @@ -0,0 +1,92 @@ +/** + * + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // aligning voltage + motor.voltage_sensor_align = 5; + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino new file mode 100644 index 0000000..7273d15 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/hall_sensor/voltage_control/voltage_control.ino @@ -0,0 +1,95 @@ +/** + * + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead of the current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB, doC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino new file mode 100644 index 0000000..d869f4d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/torque_control/magnetic_sensor/voltage_control/voltage_control.ino @@ -0,0 +1,84 @@ +/** + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - I2C +// MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// voltage set point variable +float target_voltage = 2; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 5; + // choose FOC modulation (optional) + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino new file mode 100644 index 0000000..f0f1e3e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/encoder/velocity_control/velocity_control.ino @@ -0,0 +1,137 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and encoder + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * + * NOTE : + * > Arduino UNO example code for running velocity motion control using an encoder with index significantly + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doIndex directly to the encoder.enableInterrupts(doA,doB,doIndex) and avoid using PciManger + * + * > If you don't want to use index pin initialize the encoder class without index pin number: + * > For example: + * > - Encoder encoder = Encoder(2, 3, 8192); + * > and initialize interrupts like this: + * > - encoder.enableInterrupts(doA,doB) + * + * Check the docs.simplefoc.com for more info about the possible encoder configuration. + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192, A0); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} +void doIndex(){encoder.handleIndex();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(encoder.index_pin, doIndex); + + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // software interrupts + PciManager.registerListener(&listenerIndex); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino new file mode 100644 index 0000000..1d39173 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/hall_sensor/velocity_control/velocity_control.ino @@ -0,0 +1,125 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * + * NOTE : + * > Specifically for Arduino UNO example code for running velocity motion control using a hall sensor + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doC directly to the sensor.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(sensor.pinC, doC); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void setup() { + + // initialize sensor sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenerIndex); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino new file mode 100644 index 0000000..98e0942 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motion_control/velocity_motion_control/magnetic_sensor/velocity_control/velocity_control.ino @@ -0,0 +1,106 @@ +/** + * + * Velocity motion control example + * Steps: + * 1) Configure the motor and magnetic sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * + * + * By using the serial terminal set the velocity value you want to motor to obtain + * + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering + // default 5ms - try different values to see what is the best. + // the lower the less filtered + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target velocity"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..186d257 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/encoder/full_control_serial/full_control_serial.ino @@ -0,0 +1,106 @@ +/** + * Comprehensive BLDC motor control example using encoder + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..df7b76a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/hall_sensor/full_control_serial/full_control_serial.ino @@ -0,0 +1,114 @@ +/** + * Comprehensive BLDC motor control example using Hall sensor + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include +// software interrupt library +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A, B and C callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenC(sensor.pinC, doC); + + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + // set control loop type to be used + motor.controller = MotionControlType::torque; + // contoller configuration based on the controll type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino new file mode 100644 index 0000000..098f688 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/motor_commands_serial_examples/magnetic_sensor/full_control_serial/full_control_serial.ino @@ -0,0 +1,104 @@ +/** + * Comprehensive BLDC motor control example using magnetic sensor + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - MagneticSensorI2C +//MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the control type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop controller + motor.P_angle.P = 20; + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/README.md b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/README.md new file mode 100644 index 0000000..bd6f776 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/README.md @@ -0,0 +1,28 @@ + +# OSC control examples + +OSC - opensoundcontrol.org is a simple message exchange protocol. Widely used in the Audio world it is being hailed as the successor to MIDI. But MIDI itself, and now OSC, has always been about sending and receiving fairly simple control messages, at medium speed, and that makes it well suited for the same kind of task in robotics or other control applications. + +As a protocol it is simple, making implementation quite easy, and while binary, simple enough to be fairly "human readable", which aids in debugging and reduces errors. + +The main advantage of using it is that there is a bunch of ready made software, and also libraries to use in your own programs, neaning you don't have to re-invent these things from scratch. + +## TouchOSC + +The first example shows how to set up control of a motor from TouchOSC. It's a super-fast way to set up a customizable GUI for your motor-project, that runs on your phone... + +## purr-Data + +The second example, "simplefoc\_osc\_esp32\_fullcontrol.ino" allows setting the variables and tuning the motor parameters, in the same way as the serial control but via OSC. The file "osc\_fullcontrol.pd" contains a sample GUI to go with that sketch, allowing the control and tuning of 2 BLDC motors. + +Here a screenshot of what it looks like in purr-Data: + +![Screenshot from pD](osc_fullcontrol_screenshot.png?raw=true "pD controlling 2 BLDC motors") + + +## Links to software used in examples + +- OSC - opensoundcontrol.org +- pD - https://puredata.info +- TouchOSC - https://hexler.net/products/touchosc + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/layout1.touchosc new file mode 100644 index 0000000000000000000000000000000000000000..ba4d2afca30fa1bd295d514e07167c165edd5323 GIT binary patch literal 511 zcmWIWW@Zs#;Nak3sATmCWIzI(Kz3$cN@|5(MQ+Z~$-eoA40zi9dp)&Y-1<>f@Pgjt zvKx$?+a5pJwk(9((ChH?_48*>JAW{B`rJq68z&#nfAX+m`;~)jITJSr3fQ&^Oz^Zg z>$@z$tTy*cqK#D6ozJ&opL|t_DDvV`nV;)-Y~g2h?m%|2m5C-L66*~XA9xTH^!U8v z54%9FCM8GTVokmIpAVg!oA_eux&1D_>CHDys%}@#)i|Lu@#CTiSJLG-Hd(%E+S4Iw zv0E>B&&uPct8A`5PPLu)+WoN058c;N)!TF@mUCGNyb;~9dsAL2@6qPgrabwk7Sm|f zoQo+XdFu;vUT$;U9v}F#%&UyCacR=gWfNq3xUXfahd*)KQ#NHDhw#Q3IXerNxLZ4lzWC#CHU8*~n)GScpMBZo zGw&SlUDNfE{WDyy_Vm>;1bDM^1mvEnsb*wgxWvo=Cur~n>=0p6@^ RASp&5GzZd~fW|Q}002w5+d=>U literal 0 HcmV?d00001 diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino new file mode 100644 index 0000000..afb9529 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_3pwm/osc_esp32_3pwm.ino @@ -0,0 +1,183 @@ +/** + * Arduino SimpleFOC + OSC control example + * + * Simple example to show how you can control SimpleFOC via OSC. + * + * It's a nice way to work, easier than changing speeds via Seerial text input. It could also be the basis for + * a functional UI, for example in a lab, art-gallery or similar situation. + * + * For this example we used an ESP32 to run the code, a AS5048B I2C absolute encoder + * and a generic L298 driver board to drive a Emax 4114 gimbal motor. But there is no reason the OSC part will + * not work with any other setup. + * + * You will need: + * + * - a working SimpleFOC setup - motor, driver, encoder + * - a MCU with WiFi and UDP support, for example an ESP32, or an Arduino with Ethernet Shield + * - a device to run an OSC UI + * - a configured OSC UI + * - a WiFi network + * + * To do the OSC UI I used TouchOSC from https://hexler.net/products/touchosc + * There is an example UI file that works with this sketch, see "layout1.touchosc" + * You can open the UI file in 'TouchOSC Editor' (free program) and transfer it to the TouchOSC app on your device. + * + * Alternatively, there are other OSC UIs which may work, e.g. http://opensoundcontrol.org/implementations + * + * Using: + * + * Change the values below to match the WiFi ssid/password of your network. + * Load and run the code on your ESP32. Take a note of the IP address of your ESP32. + * Load and run the UI in TouchOSC. + * Configure TouchOSC to connect to your ESP32. + * The first command you send will cause the ESP32 to start sending responses to your TouchOSC device. + * After this you will see motor position and speed in the UI. + * Have fun controlling your SimpleFOC motors from your smartphone! + * + */ + + +#include "Arduino.h" +#include + +#include +#include + +#include +#include +#include +#include + + +const char ssid[] = "myssid"; // your network SSID (name) +const char pass[] = "mypassword"; // your network password +WiFiUDP Udp; +IPAddress outIp(192,168,1,17); // remote IP (not needed for receive) +const unsigned int outPort = 8000; // remote port (not needed for receive) +const unsigned int inPort = 8000; // local port to listen for UDP packets (here's where we send the packets) + + +OSCErrorCode error; + +MagneticSensorI2C sensor = MagneticSensorI2C(0x40, 14, 0xFE, 8); +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(25, 26, 27); + +// commander interface +Commander command = Commander(Serial); + +void setup() { + Serial.begin(115200); + + WiFi.begin(ssid, pass); + + Serial.print("Connecting WiFi "); + Serial.println(ssid); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Udp.begin(inPort); + Serial.println(); + Serial.print("WiFi connected. Local OSC address: "); + Serial.print(WiFi.localIP()); + Serial.print(":"); + Serial.println(inPort); + + delay(2000); + Serial.println("Initializing motor."); + + sensor.init(); + motor.linkSensor(&sensor); + driver.voltage_power_supply = 9; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity; + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0.001f; + motor.PID_velocity.output_ramp = 1000; + motor.LPF_velocity.Tf = 0.01f; + motor.voltage_limit = 8; + //motor.P_angle.P = 20; + motor.init(); + motor.initFOC(); + + Serial.println("All initialization complete."); +} + +// velocity set point variable +float target_velocity = 2.0f; +// angle set point variable +float target_angle = 1.0f; + + +void motorControl(OSCMessage &msg){ + if (msg.isInt(0)) + target_velocity = radians(msg.getInt(0)); + else if (msg.isFloat(0)) + target_velocity = radians(msg.getFloat(0)); + else if (msg.isDouble(0)) + target_velocity = radians(msg.getDouble(0)); + Serial.print("Velocity set to "); + Serial.println(target_velocity); +} + +void cmdControl(OSCMessage &msg){ + char cmdStr[16]; + if (msg.isString(0)) { + msg.getString(0,cmdStr,16); + command.motor(&motor,cmdStr); + } +} + +long lastSend = 0; +OSCMessage bundleIN; + +void loop() { + OSCBundle bundleOUT; + + // FOC algorithm function + motor.move(target_velocity); + motor.loopFOC(); + + + int size = Udp.parsePacket(); + if (size > 0) { + while (size--) { + bundleIN.fill(Udp.read()); + } + if (!bundleIN.hasError()) { + bundleIN.dispatch("/mot1/S", motorControl); + bundleIN.dispatch("/mot1/C", cmdControl); + IPAddress ip = Udp.remoteIP(); + if (!( ip==outIp )) { + Serial.print("New connection from "); + Serial.println(ip); + outIp = ip; + } + } + else { + error = bundleIN.getError(); + Serial.print("error: "); + Serial.println(error); + } + bundleIN.empty(); + } + else { // don't receive and send in the same loop... + long now = millis(); + if (now - lastSend > 100) { + int ang = (int)degrees(motor.shaftAngle()) % 360; + if (ang<0) ang = 360-abs(ang); + //BOSCBundle's add' returns the OSCMessage so the message's 'add' can be composed together + bundleOUT.add("/mot1/A").add((int)ang); + bundleOUT.add("/mot1/V").add((int)degrees(motor.shaftVelocity())); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one + lastSend = now; + } + } + +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino new file mode 100644 index 0000000..7c6aa4f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_esp32_fullcontrol.ino @@ -0,0 +1,351 @@ +/** + * + * Control 2 motors on ESP32 using OSC + * + * In this example, all the commands available on the serial command interface are also available via OSC. + * Also, the example is for 2 motors. If you have only 1 motor, comment out the lines for the second motor. + * + * + * + * + */ + + +#include "Arduino.h" +#include +#include +#include "ssid.h" // create this file, which defines the constants MYSSID and MYPASS +#include +#include +#include + +#include +#include +#include + + +const char ssid[] = MYSSID; // your network SSID (name) +const char pass[] = MYPASS; // your network password +WiFiUDP Udp; +IPAddress outIp(192,168,1,13); // remote IP (not needed for receive) +const unsigned int outPort = 8000; // remote port (not needed for receive) +const unsigned int inPort = 8000; // local port to listen for UDP packets (here's where we send the packets) +#define POWER_SUPPLY 7.4f + + + +MagneticSensorI2C sensor2 = MagneticSensorI2C(0x40, 14, 0xFE, 8); +MagneticSensorI2C sensor1 = MagneticSensorI2C(0x41, 14, 0xFE, 8); +BLDCMotor motor1 = BLDCMotor(7); +BLDCMotor motor2 = BLDCMotor(7); +BLDCDriver3PWM driver1 = BLDCDriver3PWM(25, 26, 27); +BLDCDriver3PWM driver2 = BLDCDriver3PWM(0, 4, 16); + +String m1Prefix("/M1"); +String m2Prefix("/M2"); + + +// we store seperate set-points for each motor, of course +float set_point1 = 0.0f; +float set_point2 = 0.0f; + + +OSCErrorCode error; +OSCBundle bundleOUT; // outgoing message, gets re-used + + + +void setupOTA(const char* hostname) { + ArduinoOTA + .setPort(8266) + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }) + .onEnd([]() { + Serial.println("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\n", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }) + .setHostname(hostname) + .setMdnsEnabled(true); + ArduinoOTA.begin(); +} + + +void setup() { + // set pins low - motor initialization can take some time, + // and you don't want current flowing through the other motor while it happens... + pinMode(0,OUTPUT); + pinMode(4,OUTPUT); + pinMode(16,OUTPUT); + pinMode(25,OUTPUT); + pinMode(26,OUTPUT); + pinMode(27,OUTPUT); + digitalWrite(0, 0); + digitalWrite(4, 0); + digitalWrite(16, 0); + digitalWrite(25, 0); + digitalWrite(26, 0); + digitalWrite(27, 0); + + Serial.begin(115200); + delay(200); + + WiFi.begin(ssid, pass); + + Serial.print("Connecting WiFi "); + Serial.println(ssid); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Udp.begin(inPort); + Serial.println(); + Serial.print("WiFi connected. Local OSC address: "); + Serial.print(WiFi.localIP()); + Serial.print(":"); + Serial.println(inPort); + + setupOTA("smallrobot1"); + + Serial.println("Initializing motors."); + + Wire.setClock(400000); + + sensor1.init(); + motor1.linkSensor(&sensor1); + driver1.voltage_power_supply = POWER_SUPPLY; + driver1.init(); + motor1.linkDriver(&driver1); + motor1.foc_modulation = FOCModulationType::SpaceVectorPWM; + motor1.controller = MotionControlType::velocity; + motor1.PID_velocity.P = 0.2f; + motor1.PID_velocity.I = 20; + motor1.PID_velocity.D = 0.001f; + motor1.PID_velocity.output_ramp = 1000; + motor1.LPF_velocity.Tf = 0.01f; + motor1.voltage_limit = POWER_SUPPLY; + motor1.P_angle.P = 20; + motor1.init(); + motor1.initFOC(); + + sensor2.init(); + motor2.linkSensor(&sensor2); + driver2.voltage_power_supply = POWER_SUPPLY; + driver2.init(); + motor2.linkDriver(&driver2); + motor2.foc_modulation = FOCModulationType::SpaceVectorPWM; + motor2.controller = MotionControlType::velocity; + motor2.PID_velocity.P = 0.2f; + motor2.PID_velocity.I = 20; + motor2.PID_velocity.D = 0.001f; + motor2.PID_velocity.output_ramp = 1000; + motor2.LPF_velocity.Tf = 0.01f; + motor2.voltage_limit = POWER_SUPPLY; + motor2.P_angle.P = 20; + motor2.init(); + motor2.initFOC(); + + sendMotorParams(motor1, m1Prefix); + sendMotorParams(motor2, m2Prefix); + Serial.println("All initialization complete."); + _delay(1000); +} + + + + +template +void sendMessage(String& addr, T datum) { + bundleOUT.add(addr.c_str()).add(datum); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + + +float getNumber(OSCMessage &msg, int index) { + if (msg.getType(index)=='i') + return msg.getInt(index); + if (msg.getType(index)=='f') + return msg.getFloat(index); + if (msg.getType(index)=='d') + return msg.getDouble(index); + return 0; +} + + + +void motorCmd(OSCMessage &msg, int offset, BLDCMotor& motor, float* set_point, String& prefix){ + Serial.print("Command for "); + Serial.println(prefix); + if (msg.fullMatch("/P", offset)) { + motor.PID_velocity.P = getNumber(msg,0); + sendMessage(prefix+"/P", motor.PID_velocity.P); + } + else if (msg.fullMatch("/I", offset)) { + motor.PID_velocity.I = getNumber(msg,0); + sendMessage(prefix+"/I", motor.PID_velocity.I); + } + else if (msg.fullMatch("/D", offset)) { + motor.PID_velocity.D = getNumber(msg,0); + sendMessage(prefix+"/D", motor.PID_velocity.D); + } + else if (msg.fullMatch("/R", offset)) { + motor.PID_velocity.output_ramp = getNumber(msg,0); + sendMessage(prefix+"/R", motor.PID_velocity.output_ramp); + } + else if (msg.fullMatch("/F", offset)) { + motor.LPF_velocity.Tf = getNumber(msg,0); + sendMessage(prefix+"/F", motor.LPF_velocity.Tf); + } + else if (msg.fullMatch("/K", offset)) { + motor.P_angle.P = getNumber(msg,0); + sendMessage(prefix+"/K", motor.P_angle.P); + } + else if (msg.fullMatch("/N", offset)) { + motor.velocity_limit = getNumber(msg,0); + sendMessage(prefix+"/N", motor.velocity_limit); + } + else if (msg.fullMatch("/L", offset)) { + motor.voltage_limit = getNumber(msg,0); + sendMessage(prefix+"/L", motor.voltage_limit); + } + else if (msg.fullMatch("/C", offset)) { + motor.controller = (MotionControlType)msg.getInt(0); + sendMessage(prefix+"/C", motor.controller); + } + else if (msg.fullMatch("/t", offset)) { + *set_point = getNumber(msg,0); + sendMessage(prefix+"/3", *set_point); // TODO is the set-point the same as motor.target? + Serial.print("Setting set-point to "); + Serial.println(*set_point); + } + else if (msg.fullMatch("/o", offset)) { + motor.disable(); + } + else if (msg.fullMatch("/e", offset)) { + motor.enable(); + } + else if (msg.fullMatch("/params", offset)) { + sendMotorParams(motor, prefix); + } + else if (msg.fullMatch("/reinit", offset)) { + motor.disable(); + motor.init(); + motor.initFOC(); + } + +} + + + + + + +void sendMotorMonitoring() { + //Serial.println("Sending monitoring..."); + bundleOUT.add("/M1/0").add(motor1.voltage.q); + bundleOUT.add("/M1/1").add(motor1.shaft_velocity); + bundleOUT.add("/M1/2").add(motor1.shaft_angle); + bundleOUT.add("/M1/3").add(motor1.target); + bundleOUT.add("/M2/0").add(motor2.voltage.q); + bundleOUT.add("/M2/1").add(motor2.shaft_velocity); + bundleOUT.add("/M2/2").add(motor2.shaft_angle); + bundleOUT.add("/M2/3").add(motor2.target); + // TODO pack it into one message bundleOUT.add("/M2/i").add(motor2.voltage_q).add(motor2.shaft_velocity).add(motor2.shaft_angle).add(motor2.target); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + +void sendMotorParams(BLDCMotor& motor, String& prefix) { + bundleOUT.add((prefix+"/P").c_str()).add(motor.PID_velocity.P); + bundleOUT.add((prefix+"/I").c_str()).add(motor.PID_velocity.I); + bundleOUT.add((prefix+"/D").c_str()).add(motor.PID_velocity.D); + bundleOUT.add((prefix+"/R").c_str()).add(motor.PID_velocity.output_ramp); + bundleOUT.add((prefix+"/F").c_str()).add(motor.LPF_velocity.Tf); + bundleOUT.add((prefix+"/K").c_str()).add(motor.P_angle.P); + bundleOUT.add((prefix+"/N").c_str()).add(motor.velocity_limit); + bundleOUT.add((prefix+"/L").c_str()).add(motor.voltage_limit); + bundleOUT.add((prefix+"/C").c_str()).add(motor.controller); + Udp.beginPacket(outIp, outPort); + bundleOUT.send(Udp); + Udp.endPacket(); + bundleOUT.empty(); // empty the bundle to free room for a new one +} + + + + + +long lastSend = 0; +OSCMessage bundleIN; +int size; + + +void loop() { + + // FOC algorithm function + motor1.loopFOC(); + motor1.move(set_point1); + motor2.loopFOC(); + motor2.move(set_point2); + + + int size = Udp.parsePacket(); + if (size > 0) { + while (size--) { + bundleIN.fill(Udp.read()); + } + if (!bundleIN.hasError()) { + bundleIN.route("/M1", [](OSCMessage& msg, int offset){ motorCmd(msg, offset, motor1, &set_point1, m1Prefix); }, 0); + bundleIN.route("/M2", [](OSCMessage& msg, int offset){ motorCmd(msg, offset, motor2, &set_point2, m2Prefix); }, 0); + IPAddress ip = Udp.remoteIP(); + if (!( ip==outIp )) { + Serial.print("New connection from "); + Serial.println(ip); + outIp = ip; + sendMotorParams(motor1, m1Prefix); + sendMotorParams(motor2, m2Prefix); + } + } + else { + error = bundleIN.getError(); + Serial.print("error: "); + Serial.println(error); + } + bundleIN.empty(); + } + else { // don't receive and send in the same loop... + long now = millis(); + if (now - lastSend > 100) { + sendMotorMonitoring(); + lastSend = now; + } + } + + ArduinoOTA.handle(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd new file mode 100644 index 0000000..ddbf2f9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/osc_fullcontrol.pd @@ -0,0 +1,384 @@ +#N struct text float x float y text t; +#N canvas 1842 146 1519 1052 12; +#X obj 501 697 cnv 15 193 209 empty empty Tuning\ M1 20 12 0 14 #e0e0e0 +#404040 0; +#X obj 787 477 mrpeach/unpackOSC; +#X obj 132 586 print oscin; +#X obj 787 504 print oscout; +#X obj 723 449 spigot; +#X obj 774 452 tgl 15 1 empty empty Debug 17 7 0 10 #fcfcfc #000000 +#000000 1 1; +#X msg 591 503 disconnect; +#X obj 132 558 spigot; +#X obj 114 562 tgl 15 1 empty empty Debug -34 6 0 10 #fcfcfc #000000 +#000000 0 1; +#X obj 132 531 mrpeach/unpackOSC; +#X obj 673 477 mrpeach/udpsend; +#X obj 132 496 mrpeach/udpreceive 8000; +#X obj 673 422 mrpeach/packOSC; +#X obj 1043 150 hsl 249 25 -5000 5000 0 0 empty empty Set\ Point\ M1 +-2 -8 0 10 #fcfcfc #000000 #000000 0 1; +#X obj 1044 197 hsl 247 25 -15 15 0 0 empty empty Set\ Point\ M2 -2 +-8 0 10 #fcfcfc #000000 #000000 12300 1; +#X obj 120 153 bng 53 250 50 0 empty empty STOP 14 26 0 10 #fc1204 +#000000 #ffffff; +#X obj 200 102 * 0.10472; +#X obj 202 169 hsl 235 30 -520 520 0 0 empty empty Set\ point\ (Velocity) +-2 -8 0 10 #fcfcfc #000000 #000000 11500 1; +#X obj 673 449 spigot; +#X obj 653 452 tgl 15 1 empty empty Enable\ send -71 6 0 10 #fcfcfc +#000000 #000000 1 1; +#X msg 484 478 connect 192.168.1.43 8000; +#X obj 673 395 speedlim 100; +#X obj 231 573 mrpeach/routeOSC /M1 /M2; +#X obj 231 610 mrpeach/routeOSC /0 /1 /2 /3 /P /I /D /R /F /K /N /L +/C; +#X obj 326 844 hsl 101 29 0 6.3 0 0 empty empty rad -2 -8 0 10 #fcfcfc +#000000 #000000 0 1; +#X obj 326 812 % 6.28319; +#X obj 326 704 nbx 7 27 -1e+37 1e+37 0 0 empty empty V -5 0 0 18 #fcfcfc +#000000 #000000 0.137548 256 3; +#X obj 113 804 nbx 7 27 -1e+37 1e+37 0 0 empty empty Set\ point 0 -15 +0 18 #fcfcfc #000000 #000000 0 256 3; +#X obj 326 776 nbx 7 27 -1e+37 1e+37 0 0 empty empty Position -80 0 +0 18 #fcfcfc #000000 #000000 -348.637 256 3; +#X obj 326 740 nbx 7 27 -1e+37 1e+37 0 0 empty empty Velocity -80 0 +0 18 #fcfcfc #000000 #000000 0.0649328 256 3; +#X obj 538 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty P\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 0.2 256 3; +#X obj 537 776 nbx 5 14 -1e+37 1e+37 0 0 empty empty I\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 20 256 3; +#X obj 538 808 nbx 5 14 -1e+37 1e+37 0 0 empty empty D\ gain 0 -8 0 +10 #fcfcfc #000000 #000000 0.0001 256 3; +#X obj 539 838 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ Ramp 0 -8 0 +10 #fcfcfc #000000 #000000 1000 256 3; +#X obj 539 868 nbx 5 14 -1e+37 1e+37 0 0 empty empty LP\ time 0 -8 +0 10 #fcfcfc #000000 #000000 0.01 256 3; +#X obj 605 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 608 779 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ lim 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 609 836 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ limit 0 -8 +0 10 #fcfcfc #000000 #000000 8 256 3; +#X obj 122 278 hradio 53 0 1 3 motorselect empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 0; +#X scalar text 172 305 \; \;; +#X obj 122 372 select 0 1 2; +#X msg 122 399 prefix /M?; +#X msg 149 423 prefix /M1; +#X msg 178 445 prefix /M2; +#X obj 789 422 mrpeach/packOSC; +#X obj 789 395 speedlim 100; +#X msg 592 531 typetags \$1; +#X obj 571 535 tgl 15 1 empty empty OSC\ type\ tags -80 7 0 10 #ffffff +#000000 #000000 1 1; +#X text 63 286 Choose Motor, f 7; +#X text 137 339 All, f 4; +#X text 191 339 M1, f 4; +#X text 247 339 M2, f 4; +#X text 152 77 RPM; +#X obj 1008 148 bng 29 250 50 0 empty empty STOP 2 13 0 10 #fc1204 +#000000 #ffffff; +#X obj 1009 195 bng 29 250 50 0 empty empty STOP 2 13 0 10 #fc1204 +#000000 #ffffff; +#X obj 74 696 hradio 53 0 1 3 empty empty empty 0 -8 0 10 #fcfcfc #000000 +#000000 1; +#X text 8 711 Control; +#X text 67 752 Voltage; +#X text 124 753 Velocity; +#X text 189 754 Position; +#X obj 312 101 /; +#X obj 312 129 * 6.28319; +#X text 424 75 cm, f 4; +#X text 393 51 Wheel diameter; +#X obj 394 100 * 0.0314159; +#X msg 348 636 set \$1; +#X msg 376 637 set \$1; +#X msg 407 636 set \$1; +#X msg 435 636 set \$1; +#X msg 466 636 set \$1; +#X msg 495 636 set \$1; +#X msg 524 637 set \$1; +#X msg 554 637 set \$1; +#X msg 583 637 set \$1; +#X obj 75 898 s osctargetedout; +#X obj 75 866 prepend /M1/C; +#X obj 773 304 r osctargetedout; +#X obj 593 912 prepend /M1/K; +#X obj 602 925 prepend /M1/N; +#X obj 609 936 prepend /M1/L; +#X obj 564 976 s osctargetedout; +#X obj 1271 697 cnv 15 193 209 empty empty Tuning\ M2 20 12 0 14 #e0e0e0 +#404040 0; +#X obj 1001 610 mrpeach/routeOSC /0 /1 /2 /3 /P /I /D /R /F /K /N /L +/C; +#X obj 1096 844 hsl 101 29 0 6.3 0 0 empty empty rad -2 -8 0 10 #fcfcfc +#000000 #000000 0 1; +#X obj 1096 812 % 6.28319; +#X obj 1096 704 nbx 7 27 -1e+37 1e+37 0 0 empty empty V -5 0 0 18 #fcfcfc +#000000 #000000 -0.13018 256 3; +#X obj 883 804 nbx 7 27 -1e+37 1e+37 0 0 setpointin2 empty Set\ point +0 -15 0 18 #fcfcfc #000000 #000000 0 256 3; +#X obj 1096 776 nbx 7 27 -1e+37 1e+37 0 0 empty empty Position -80 +0 0 18 #fcfcfc #000000 #000000 -346.273 256 3; +#X obj 1096 740 nbx 7 27 -1e+37 1e+37 0 0 empty empty Velocity -80 +0 0 18 #fcfcfc #000000 #000000 0.0657255 256 3; +#X obj 1308 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty P\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 0.2 256 3; +#X obj 1308 778 nbx 5 14 -1e+37 1e+37 0 0 empty empty I\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1308 808 nbx 5 14 -1e+37 1e+37 0 0 empty empty D\ gain 0 -8 +0 10 #fcfcfc #000000 #000000 0.001 256 3; +#X obj 1309 838 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ Ramp 0 -8 +0 10 #fcfcfc #000000 #000000 1000 256 3; +#X obj 1309 868 nbx 5 14 -1e+37 1e+37 0 0 empty empty LP\ time 0 -8 +0 10 #fcfcfc #000000 #000000 0.01 256 3; +#X obj 1375 747 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ gain 0 +-8 0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1378 779 nbx 5 14 -1e+37 1e+37 0 0 empty empty angP\ lim 0 -8 +0 10 #fcfcfc #000000 #000000 20 256 3; +#X obj 1379 836 nbx 5 14 -1e+37 1e+37 0 0 empty empty V\ limit 0 -8 +0 10 #fcfcfc #000000 #000000 8 256 3; +#X obj 844 696 hradio 53 0 1 3 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 1; +#X text 778 711 Control; +#X text 837 752 Voltage; +#X text 894 753 Velocity; +#X text 959 754 Position; +#X msg 1118 636 set \$1; +#X msg 1146 637 set \$1; +#X msg 1177 636 set \$1; +#X msg 1205 636 set \$1; +#X msg 1236 636 set \$1; +#X msg 1265 636 set \$1; +#X msg 1294 637 set \$1; +#X msg 1324 637 set \$1; +#X msg 1353 637 set \$1; +#X obj 845 898 s osctargetedout; +#X obj 1325 976 s osctargetedout; +#X obj 1379 936 prepend /M2/L; +#X obj 1372 925 prepend /M2/N; +#X obj 1364 912 prepend /M2/K; +#X obj 1296 947 prepend /M2/F; +#X obj 1291 940 prepend /M2/R; +#X obj 1287 933 prepend /M2/D; +#X obj 1281 925 prepend /M2/I; +#X obj 1276 917 prepend /M2/P; +#X obj 526 947 prepend /M1/F; +#X obj 521 940 prepend /M1/R; +#X obj 517 933 prepend /M1/D; +#X obj 511 925 prepend /M1/I; +#X obj 506 917 prepend /M1/P; +#X obj 393 78 nbx 2 14 0 50 0 1 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 6 256 2; +#X obj 299 71 nbx 5 24 -20 20 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 -0.266666 256 2; +#X obj 179 74 nbx 7 23 -5000 5000 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 -84.8824 256 2; +#X obj 577 169 hsl 104 30 -3.1415 3.1415 0 0 empty empty Set\ point\ (Position) +-2 -8 0 10 #fcfcfc #000000 #000000 10300 1; +#X obj 708 68 vsl 31 122 0 12 0 0 empty empty Set\ point\ (Voltage) +0 -9 0 10 #fcfcfc #000000 #000000 0 1; +#X obj 246 13 / 0.10472; +#X msg 318 13 set \$1; +#X obj 457 11 *; +#X obj 384 11 / 6.28319; +#X obj 547 96 /; +#X obj 547 125 * 6.28319; +#X obj 547 66 nbx 5 24 -100 100 0 0 empty empty empty 0 -8 0 10 #fcfcfc +#000000 #000000 0.0599999 256 2; +#X text 534 68 m; +#X obj 20 6 r setpointin; +#X obj 17 36 r setpointin2; +#X obj 120 6 spigot; +#X obj 120 42 spigot; +#X obj 16 63 r motorselect; +#X obj 1339 99 r setpointin; +#X obj 1345 150 r setpointin2; +#X msg 493 11 set \$1; +#X obj 22 103 > 1; +#X obj 59 103 <= 1; +#X msg 1110 529 /M?/params; +#X obj 1110 502 loadbang; +#X msg 1339 126 set \$1; +#X msg 1343 174 set \$1; +#X obj 639 9 *; +#X obj 566 9 / 6.28319; +#X msg 675 9 set \$1; +#X obj 483 448 loadbang; +#X text 284 74 m/s; +#X obj 845 866 prepend /M2/C; +#X msg 163 226 sendtyped /M?/t f 0; +#X obj 458 224 prepend sendtyped /t f; +#X msg 991 301 sendtyped /M2/t f 0; +#X msg 983 274 sendtyped /M1/t f 0; +#X obj 1051 286 prepend sendtyped /M1/t f; +#X obj 1047 322 prepend sendtyped /M2/t f; +#X obj 475 171 nbx 7 21 -1e+37 1e+37 0 0 empty empty rad 0 -8 0 10 +#fcfcfc #000000 #000000 2 256 2; +#X connect 1 0 3 0; +#X connect 4 0 1 0; +#X connect 5 0 4 1; +#X connect 6 0 10 0; +#X connect 7 0 2 0; +#X connect 8 0 7 1; +#X connect 9 0 7 0; +#X connect 9 0 22 0; +#X connect 11 0 9 0; +#X connect 12 0 18 0; +#X connect 12 0 4 0; +#X connect 13 0 163 0; +#X connect 14 0 164 0; +#X connect 15 0 159 0; +#X connect 16 0 17 0; +#X connect 17 0 131 0; +#X connect 17 0 134 0; +#X connect 17 0 160 0; +#X connect 18 0 10 0; +#X connect 19 0 18 1; +#X connect 20 0 10 0; +#X connect 21 0 12 0; +#X connect 22 0 23 0; +#X connect 22 1 82 0; +#X connect 23 0 26 0; +#X connect 23 1 29 0; +#X connect 23 2 28 0; +#X connect 23 3 27 0; +#X connect 23 4 65 0; +#X connect 23 5 66 0; +#X connect 23 6 67 0; +#X connect 23 7 68 0; +#X connect 23 8 69 0; +#X connect 23 9 70 0; +#X connect 23 10 71 0; +#X connect 23 11 72 0; +#X connect 23 12 73 0; +#X connect 25 0 24 0; +#X connect 28 0 25 0; +#X connect 30 0 125 0; +#X connect 31 0 124 0; +#X connect 32 0 123 0; +#X connect 33 0 122 0; +#X connect 34 0 121 0; +#X connect 35 0 77 0; +#X connect 36 0 78 0; +#X connect 37 0 79 0; +#X connect 38 0 40 0; +#X connect 40 0 41 0; +#X connect 40 1 42 0; +#X connect 40 2 43 0; +#X connect 41 0 12 0; +#X connect 42 0 12 0; +#X connect 43 0 12 0; +#X connect 44 0 18 0; +#X connect 44 0 4 0; +#X connect 45 0 44 0; +#X connect 46 0 12 0; +#X connect 47 0 46 0; +#X connect 53 0 162 0; +#X connect 54 0 161 0; +#X connect 55 0 75 0; +#X connect 60 0 61 0; +#X connect 61 0 17 0; +#X connect 64 0 60 1; +#X connect 64 0 133 1; +#X connect 64 0 135 1; +#X connect 64 0 153 1; +#X connect 65 0 30 0; +#X connect 66 0 31 0; +#X connect 67 0 32 0; +#X connect 68 0 33 0; +#X connect 69 0 34 0; +#X connect 70 0 35 0; +#X connect 71 0 36 0; +#X connect 72 0 37 0; +#X connect 73 0 55 0; +#X connect 75 0 74 0; +#X connect 76 0 45 0; +#X connect 77 0 80 0; +#X connect 78 0 80 0; +#X connect 79 0 80 0; +#X connect 82 0 85 0; +#X connect 82 1 88 0; +#X connect 82 2 87 0; +#X connect 82 3 86 0; +#X connect 82 4 102 0; +#X connect 82 5 103 0; +#X connect 82 6 104 0; +#X connect 82 7 105 0; +#X connect 82 8 106 0; +#X connect 82 9 107 0; +#X connect 82 10 108 0; +#X connect 82 11 109 0; +#X connect 82 12 110 0; +#X connect 84 0 83 0; +#X connect 87 0 84 0; +#X connect 89 0 120 0; +#X connect 90 0 119 0; +#X connect 91 0 118 0; +#X connect 92 0 117 0; +#X connect 93 0 116 0; +#X connect 94 0 115 0; +#X connect 95 0 114 0; +#X connect 96 0 113 0; +#X connect 97 0 158 0; +#X connect 102 0 89 0; +#X connect 103 0 90 0; +#X connect 104 0 91 0; +#X connect 105 0 92 0; +#X connect 106 0 93 0; +#X connect 107 0 94 0; +#X connect 108 0 95 0; +#X connect 109 0 96 0; +#X connect 110 0 97 0; +#X connect 113 0 112 0; +#X connect 114 0 112 0; +#X connect 115 0 112 0; +#X connect 116 0 112 0; +#X connect 117 0 112 0; +#X connect 118 0 112 0; +#X connect 119 0 112 0; +#X connect 120 0 112 0; +#X connect 121 0 80 0; +#X connect 122 0 80 0; +#X connect 123 0 80 0; +#X connect 124 0 80 0; +#X connect 125 0 80 0; +#X connect 126 0 64 0; +#X connect 127 0 60 0; +#X connect 128 0 16 0; +#X connect 129 0 165 0; +#X connect 130 0 160 0; +#X connect 131 0 132 0; +#X connect 132 0 128 0; +#X connect 133 0 146 0; +#X connect 134 0 133 0; +#X connect 135 0 136 0; +#X connect 136 0 165 0; +#X connect 137 0 135 0; +#X connect 139 0 141 0; +#X connect 140 0 142 0; +#X connect 143 0 147 0; +#X connect 143 0 148 0; +#X connect 144 0 151 0; +#X connect 145 0 152 0; +#X connect 146 0 127 0; +#X connect 147 0 142 1; +#X connect 148 0 141 1; +#X connect 149 0 44 0; +#X connect 150 0 149 0; +#X connect 151 0 13 0; +#X connect 152 0 14 0; +#X connect 153 0 155 0; +#X connect 154 0 153 0; +#X connect 155 0 137 0; +#X connect 156 0 20 0; +#X connect 158 0 111 0; +#X connect 159 0 44 0; +#X connect 160 0 21 0; +#X connect 161 0 44 0; +#X connect 162 0 44 0; +#X connect 163 0 45 0; +#X connect 164 0 45 0; +#X connect 165 0 160 0; +#X connect 165 0 154 0; diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me new file mode 100644 index 0000000..9d3b03c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/osc_control_examples/osc_esp32_fullcontrol/ssid.h_rename_me @@ -0,0 +1,4 @@ + +#define MYSSID "yourssid" +#define MYPASS "yourpassword" + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino new file mode 100644 index 0000000..7324edf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/alignment_and_cogging_test/alignment_and_cogging_test.ino @@ -0,0 +1,125 @@ +#include +#include + +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); +MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); + + +/** + * This measures how closely sensor and electrical angle agree and how much your motor is affected by 'cogging'. + * It can be used to investigate how much non linearity there is between what we set (electrical angle) and what we read (sensor angle) + * This non linearity could be down to magnet placement, coil winding differences or simply that the magnetic field when travelling through a pole pair is not linear + * An alignment error of ~10 degrees and cogging of ~4 degrees is normal for small gimbal. + * The following article is an interesting read + * https://hackaday.com/2016/02/23/anti-cogging-algorithm-brings-out-the-best-in-your-hobby-brushless-motors/ + */ +void testAlignmentAndCogging(int direction) { + + motor.move(0); + _delay(200); + + sensor.update(); + float initialAngle = sensor.getAngle(); + + const int shaft_rotation = 720; // 720 deg test - useful to see repeating cog pattern + int sample_count = int(shaft_rotation * motor.pole_pairs); // test every electrical degree + + float stDevSum = 0; + + float mean = 0.0f; + float prev_mean = 0.0f; + + + for (int i = 0; i < sample_count; i++) { + + float shaftAngle = (float) direction * i * shaft_rotation / sample_count; + float electricAngle = (float) shaftAngle * motor.pole_pairs; + // move and wait + motor.move(shaftAngle * PI / 180); + _delay(5); + + // measure + sensor.update(); + float sensorAngle = (sensor.getAngle() - initialAngle) * 180 / PI; + float sensorElectricAngle = sensorAngle * motor.pole_pairs; + float electricAngleError = electricAngle - sensorElectricAngle; + + // plot this - especially electricAngleError + Serial.print(electricAngle); + Serial.print("\t"); + Serial.print(sensorElectricAngle ); + Serial.print("\t"); + Serial.println(electricAngleError); + + // use knuth standard deviation algorithm so that we don't need an array too big for an Uno + prev_mean = mean; + mean = mean + (electricAngleError-mean)/(i+1); + stDevSum = stDevSum + (electricAngleError-mean)*(electricAngleError-prev_mean); + + } + + Serial.println(); + Serial.println(F("ALIGNMENT AND COGGING REPORT")); + Serial.println(); + Serial.print(F("Direction: ")); + Serial.println(direction); + Serial.print(F("Mean error (alignment): ")); + Serial.print(mean); + Serial.println(" deg (electrical)"); + Serial.print(F("Standard Deviation (cogging): ")); + Serial.print(sqrt(stDevSum/sample_count)); + Serial.println(F(" deg (electrical)")); + Serial.println(); + Serial.println(F("Plotting 3rd column of data (electricAngleError) will likely show sinusoidal cogging pattern with a frequency of 4xpole_pairs per rotation")); + Serial.println(); + +} + +void setup() { + + Serial.begin(115200); + while (!Serial) ; + + // driver config + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + motor.voltage_sensor_align = 3; + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=motor.voltage_sensor_align; + + sensor.init(); + motor.linkSensor(&sensor); + + motor.useMonitoring(Serial); + motor.init(); + motor.initFOC(); + + testAlignmentAndCogging(1); + + motor.move(0); + Serial.println(F("Press any key to test in CCW direction")); + while (!Serial.available()) { } + + testAlignmentAndCogging(-1); + + Serial.println(F("Complete")); + + motor.voltage_limit = 0; + motor.move(0); + while (true) ; //do nothing; + +} + + + + +void loop() { + +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..76fb46b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/encoder/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,102 @@ +/** + * + * Find KV rating for motor with encoder + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + * + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder sensor = Encoder(2, 3, 8192); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} + + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..3abef46 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/hall_sensor/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,99 @@ +/** + * + * Find KV rating for motor with Hall sensors + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + */ +#include + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB, doC); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino new file mode 100644 index 0000000..f3dd74a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_kv_rating/magnetic_sensor/find_kv_rating/find_kv_rating.ino @@ -0,0 +1,96 @@ +/** + * Find KV rating for motor with magnetic sensors + * + * Motor KV rating is defiend as the increase of the motor velocity expressed in rotations per minute [rpm] per each 1 Volt int voltage control mode. + * + * This example will set your motor in the torque control mode using voltage and set 1 volt to the motor. By reading the velocity it will calculat the motors KV rating. + * - To make this esimation more credible you can try increasing the target voltage (or decrease in some cases) + * - The KV rating should be realatively static number - it should not change considerably with the increase in the voltage + */ +#include + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// magnetic sensor instance - I2C +// MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// voltage set point variable +float target_voltage = 1; + +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } +void calcKV(char* cmd) { + // calculate the KV + Serial.println(motor.shaft_velocity/motor.target*30.0f/_PI); + +} + +void setup() { + + // initialize encoder sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // IMPORTANT! + // make sure to set the correct power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // aligning voltage + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + command.add('K', calcKV, "calculate KV rating"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target voltage : - commnad T")); + Serial.println(F("Calculate the motor KV : - command K")); + _delay(1000); +} + + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino new file mode 100644 index 0000000..aac879c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/encoder/find_pole_pairs_number/find_pole_pairs_number.ino @@ -0,0 +1,173 @@ +/** + * Utility arduino sketch which finds pole pair number of the motor + * + * To run it just set the correct pin numbers for the BLDC driver and encoder A and B channel as well as the encoder PPR value. + * + * The program will rotate your motor a specific amount and check how much it moved, and by doing a simple calculation calculate your pole pair number. + * The pole pair number will be outputted to the serial terminal. + * + * If the pole pair number is well estimated your motor will start to spin in voltage mode with 2V target. + * + * If the code calculates negative pole pair number please invert your encoder A and B channel pins or motor connector. + * + * Try running this code several times to avoid statistical errors. + * > But in general if your motor spins, you have a good pole pairs number. + */ +#include + +// BLDC motor instance +// its important to put pole pairs number as 1!!! +BLDCMotor motor = BLDCMotor(1); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +// its important to put pole pairs number as 1!!! +//StepperMotor motor = StepperMotor(1); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// Encoder(int encA, int encB , int cpr, int index) +Encoder encoder = Encoder(2, 3, 2048); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +void setup() { + + // initialise encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage + // default 12V + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + + // initialize motor + motor.init(); + // monitoring port + Serial.begin(115200); + + // pole pairs calculation routine + Serial.println("Pole pairs (PP) estimator"); + Serial.println("-\n"); + + float pp_search_voltage = 4; // maximum power_supply_voltage/2 + float pp_search_angle = 6*_PI; // search electrical angle to turn + + // move motor to the electrical angle 0 + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=pp_search_voltage; + motor.move(0); + _delay(1000); + // read the encoder angle + encoder.update(); + float angle_begin = encoder.getAngle(); + _delay(50); + + // move the motor slowly to the electrical angle pp_search_angle + float motor_angle = 0; + while(motor_angle <= pp_search_angle){ + motor_angle += 0.01f; + motor.move(motor_angle); + _delay(1); + } + _delay(1000); + // read the encoder value for 180 + encoder.update(); + float angle_end = encoder.getAngle(); + _delay(50); + // turn off the motor + motor.move(0); + _delay(1000); + + // calculate the pole pair number + int pp = round((pp_search_angle)/(angle_end-angle_begin)); + + Serial.print(F("Estimated PP : ")); + Serial.println(pp); + Serial.println(F("PP = Electrical angle / Encoder angle ")); + Serial.print(pp_search_angle*180/_PI); + Serial.print("/"); + Serial.print((angle_end-angle_begin)*180/_PI); + Serial.print(" = "); + Serial.println((pp_search_angle)/(angle_end-angle_begin)); + Serial.println(); + + + // a bit of monitoring the result + if(pp <= 0 ){ + Serial.println(F("PP number cannot be negative")); + Serial.println(F(" - Try changing the search_voltage value or motor/encoder configuration.")); + return; + }else if(pp > 30){ + Serial.println(F("PP number very high, possible error.")); + }else{ + Serial.println(F("If PP is estimated well your motor should turn now!")); + Serial.println(F(" - If it is not moving try to relaunch the program!")); + Serial.println(F(" - You can also try to adjust the target voltage using serial terminal!")); + } + + + // set FOC loop to be used + motor.controller = MotionControlType::torque; + // set the pole pair number to the motor + motor.pole_pairs = pp; + //align encoder and start FOC + motor.initFOC(); + _delay(1000); + + Serial.println(F("\n Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); +} + +// uq voltage +float target_voltage = 2; + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // communicate with the user + serialReceiveUserCommand(); +} + + +// utility function enabling serial communication with the user to set the target values +// this function can be implemented in serialEvent function as well +void serialReceiveUserCommand() { + + // a string to hold incoming data + static String received_chars; + + while (Serial.available()) { + // get the new byte: + char inChar = (char)Serial.read(); + // add it to the string buffer: + received_chars += inChar; + // end of user input + if (inChar == '\n') { + + // change the motor target + target_voltage = received_chars.toFloat(); + Serial.print("Target voltage: "); + Serial.println(target_voltage); + + // reset the command buffer + received_chars = ""; + } + } +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino new file mode 100644 index 0000000..e98a452 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_pole_pair_number/magnetic_sensor/find_pole_pairs_number/find_pole_pairs_number.ino @@ -0,0 +1,173 @@ +/** + * Utility arduino sketch which finds pole pair number of the motor + * + * To run it just set the correct pin numbers for the BLDC driver and sensor CPR value and chip select pin. + * + * The program will rotate your motor a specific amount and check how much it moved, and by doing a simple calculation calculate your pole pair number. + * The pole pair number will be outputted to the serial terminal. + * + * If the pole pair number is well estimated your motor will start to spin in voltage mode with 2V target. + * + * If the code calculates negative pole pair number please invert your motor connector. + * + * Try running this code several times to avoid statistical errors. + * > But in general if your motor spins, you have a good pole pairs number. + */ +#include + +// BLDC motor instance +// its important to put pole pairs number as 1!!! +BLDCMotor motor = BLDCMotor(1); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +// its important to put pole pairs number as 1!!! +//StepperMotor motor = StepperMotor(1); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); +// magnetic sensor instance - analog output +// MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // power supply voltage + // default 12V + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // initialize motor hardware + motor.init(); + + // monitoring port + Serial.begin(115200); + + // pole pairs calculation routine + Serial.println("Pole pairs (PP) estimator"); + Serial.println("-\n"); + + float pp_search_voltage = 4; // maximum power_supply_voltage/2 + float pp_search_angle = 6*_PI; // search electrical angle to turn + + // move motor to the electrical angle 0 + motor.controller = MotionControlType::angle_openloop; + motor.voltage_limit=pp_search_voltage; + motor.move(0); + _delay(1000); + // read the sensor angle + sensor.update(); + float angle_begin = sensor.getAngle(); + _delay(50); + + // move the motor slowly to the electrical angle pp_search_angle + float motor_angle = 0; + while(motor_angle <= pp_search_angle){ + motor_angle += 0.01f; + sensor.update(); // keep track of the overflow + motor.move(motor_angle); + _delay(1); + } + _delay(1000); + // read the sensor value for 180 + sensor.update(); + float angle_end = sensor.getAngle(); + _delay(50); + // turn off the motor + motor.move(0); + _delay(1000); + + // calculate the pole pair number + int pp = round((pp_search_angle)/(angle_end-angle_begin)); + + Serial.print(F("Estimated PP : ")); + Serial.println(pp); + Serial.println(F("PP = Electrical angle / Encoder angle ")); + Serial.print(pp_search_angle*180/_PI); + Serial.print(F("/")); + Serial.print((angle_end-angle_begin)*180/_PI); + Serial.print(F(" = ")); + Serial.println((pp_search_angle)/(angle_end-angle_begin)); + Serial.println(); + + + // a bit of monitoring the result + if(pp <= 0 ){ + Serial.println(F("PP number cannot be negative")); + Serial.println(F(" - Try changing the search_voltage value or motor/sensor configuration.")); + return; + }else if(pp > 30){ + Serial.println(F("PP number very high, possible error.")); + }else{ + Serial.println(F("If PP is estimated well your motor should turn now!")); + Serial.println(F(" - If it is not moving try to relaunch the program!")); + Serial.println(F(" - You can also try to adjust the target voltage using serial terminal!")); + } + + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + // set the pole pair number to the motor + motor.pole_pairs = pp; + //align sensor and start FOC + motor.initFOC(); + _delay(1000); + + Serial.println(F("\n Motor ready.")); + Serial.println(F("Set the target voltage using serial terminal:")); +} + +// uq voltage +float target_voltage = 2; + +void loop() { + + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_voltage); + + // communicate with the user + serialReceiveUserCommand(); +} + + +// utility function enabling serial communication with the user to set the target values +// this function can be implemented in serialEvent function as well +void serialReceiveUserCommand() { + + // a string to hold incoming data + static String received_chars; + + while (Serial.available()) { + // get the new byte: + char inChar = (char)Serial.read(); + // add it to the string buffer: + received_chars += inChar; + // end of user input + if (inChar == '\n') { + + // change the motor target + target_voltage = received_chars.toFloat(); + Serial.print("Target voltage: "); + Serial.println(target_voltage); + + // reset the command buffer + received_chars = ""; + } + } +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino new file mode 100644 index 0000000..27163df --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/calibration/find_sensor_offset_and_direction/find_sensor_offset_and_direction.ino @@ -0,0 +1,84 @@ +/** + * Simple example intended to help users find the zero offset and natural direction of the sensor. + * + * These values can further be used to avoid motor and sensor alignment procedure. + * To use these values add them to the code:"); + * motor.sensor_direction=Direction::CW; // or Direction::CCW + * motor.zero_electric_angle=1.2345; // use the real value! + * + * This will only work for abosolute value sensors - magnetic sensors. + * Bypassing the alignment procedure is not possible for the encoders and for the current implementation of the Hall sensors. + * library version 1.4.2. + * + */ +#include + +// magnetic sensor instance - SPI +//MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); +// magnetic sensor instance - I2C +//MagneticSensorI2C sensor = MagneticSensorI2C(0x36, 12, 0X0C, 4); +// magnetic sensor instance - analog output +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +// BLDC motor instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +void setup() { + + // power supply voltage + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // aligning voltage + motor.voltage_sensor_align = 7; + + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // force direction search - because default is CW + motor.sensor_direction = Direction::UNKNOWN; + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + + Serial.begin(115200); + Serial.println("Sensor zero offset is:"); + Serial.println(motor.zero_electric_angle, 4); + Serial.println("Sensor natural direction is: "); + Serial.println(motor.sensor_direction == Direction::CW ? "Direction::CW" : "Direction::CCW"); + + Serial.println("To use these values add them to the code:"); + Serial.print(" motor.sensor_direction="); + Serial.print(motor.sensor_direction == Direction::CW ? "Direction::CW" : "Direction::CCW"); + Serial.println(";"); + Serial.print(" motor.zero_electric_angle="); + Serial.print(motor.zero_electric_angle, 4); + Serial.println(";"); + + _delay(1000); + Serial.println("If motor is not moving the alignment procedure was not successfull!!"); +} + + +void loop() { + + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(2); +} + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino new file mode 100644 index 0000000..60bbcf1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_extend_example/commander_extend_example.ino @@ -0,0 +1,53 @@ +/** + * Simple example of custom commands that have nothing to do with the simple foc library + */ + +#include + +// instantiate the commander +Commander command = Commander(Serial); + +// led control function +void doLed(char* cmd){ + if(atoi(cmd)) digitalWrite(LED_BUILTIN, HIGH); + else digitalWrite(LED_BUILTIN, LOW); +}; +// get analog input +void doAnalog(char* cmd){ + if (cmd[0] == '0') Serial.println(analogRead(A0)); + else if (cmd[0] == '1') Serial.println(analogRead(A1)); + else if (cmd[0] == '2') Serial.println(analogRead(A2)); + else if (cmd[0] == '3') Serial.println(analogRead(A3)); + else if (cmd[0] == '4') Serial.println(analogRead(A4)); +}; + +void setup() { + // define pins + pinMode(LED_BUILTIN, OUTPUT); + pinMode(A0, INPUT); + pinMode(A1, INPUT); + pinMode(A2, INPUT); + pinMode(A3, INPUT); + pinMode(A4, INPUT); + + // Serial port to be used + Serial.begin(115200); + + // add new commands + command.add('L', doLed, "led on/off"); + command.add('A', doAnalog, "analog read A0-A4"); + + Serial.println(F("Commander listening")); + Serial.println(F(" - Send ? to see the node list...")); + Serial.println(F(" - Send L0 to turn the led off and L1 to turn it off")); + Serial.println(F(" - Send A0-A4 to read the analog pins")); + _delay(1000); +} + + +void loop() { + + // user communication + command.run(); + _delay(10); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino new file mode 100644 index 0000000..1381515 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_no_serial/commander_no_serial.ino @@ -0,0 +1,51 @@ +/** + * Simple example of how to use the commander without serial - using just strings + */ + +#include + +// instantiate the commander +Commander command = Commander(); + +// led control function +void doLed(char* cmd){ + if(atoi(cmd)) digitalWrite(LED_BUILTIN, HIGH); + else digitalWrite(LED_BUILTIN, LOW); +}; +// get analog input +void doAnalog(char* cmd){ + if (cmd[0] == '0') Serial.println(analogRead(A0)); + else if (cmd[0] == '1') Serial.println(analogRead(A1)); +}; + +void setup() { + // define pins + pinMode(LED_BUILTIN, OUTPUT); + pinMode(A0, INPUT); + pinMode(A1, INPUT); + + // Serial port to be used + Serial.begin(115200); + + // add new commands + command.add('L', doLed, "led control"); + command.add('A', doAnalog, "analog read A0-A1"); + + Serial.println(F("Commander running")); + _delay(1000); +} + + +void loop() { + // user communication + command.run("?"); + _delay(2000); + command.run("L0"); + _delay(1000); + command.run("A0"); + _delay(1000); + command.run("A1"); + _delay(1000); + command.run("L1"); + _delay(1000); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino new file mode 100644 index 0000000..cbeb6ec --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/commander/commander_tune_custom_loop/commander_tune_custom_loop.ino @@ -0,0 +1,79 @@ +/** + * A simple example to show how to use the commander with the control loops outside of the scope of the SimpleFOC library +*/ +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA() { encoder.handleA(); } +void doB() { encoder.handleB(); } + +// target voltage to be set to the motor +float target_velocity = 0; + +// PID controllers and low pass filters +PIDController PIDv{0.05, 1, 0, 100000000, 12}; +LowPassFilter LPFv{0.01}; + +//add communication +Commander command = Commander(Serial); +void doController(char* cmd) { command.pid(&PIDv, cmd); } +void doFilter(char* cmd) { command.lpf(&LPFv, cmd); } +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set motion control loop to be used ( doing nothing ) + motor.torque_controller = TorqueControlType::voltage; + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + motor.useMonitoring(Serial); + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // subscribe the new commands + command.add('C', doController, "tune velocity pid"); + command.add('F', doFilter, "tune velocity LPF"); + command.add('T', doTarget, "motor target"); + + _delay(1000); + Serial.println(F("Commander listening")); + Serial.println(F(" - Send ? to see the node list...")); +} + + + +void loop() { + // looping foc + motor.loopFOC(); + + // calculate voltage + float target_voltage = PIDv(target_velocity - LPFv(motor.shaft_velocity)); + // set the voltage + motor.move(target_voltage); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino new file mode 100644 index 0000000..a0bebc3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_simple/step_dir_listener_simple.ino @@ -0,0 +1,36 @@ +/** + * A simple example of reading step/dir communication + * - this example uses hadware interrupts +*/ + +#include + +// angle +float received_angle = 0; + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(2, 3, 360.0/200.0); // receive the angle in degrees +void onStep() { step_dir.handle(); } + +void setup() { + + Serial.begin(115200); + + // init step and dir pins + step_dir.init(); + // enable interrupts + step_dir.enableInterrupt(onStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling step_dir.getValue(); + step_dir.attach(&received_angle); + + Serial.println(F("Step/Dir listenning.")); + _delay(1000); +} + +void loop() { + Serial.print(received_angle); + Serial.print("\t"); + Serial.println(step_dir.getValue()); + _delay(500); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino new file mode 100644 index 0000000..b6f0ea5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_listener_software_interrupt/step_dir_listener_software_interrupt.ino @@ -0,0 +1,44 @@ +/** + * A simple example of reading step/dir communication + * - this example uses software interrupts - this code is intended primarily + * for Arduino UNO/Mega and similar boards with very limited number of interrupt pins +*/ + +#include +// software interrupt library +#include +#include + + +// angle +float received_angle = 0; + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(4, 5, 2.0f*_PI/200.0); // receive the angle in radians +void onStep() { step_dir.handle(); } + +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenStep(step_dir.pin_step, onStep); + +void setup() { + + Serial.begin(115200); + + // init step and dir pins + step_dir.init(); + // enable software interrupts + PciManager.registerListener(&listenStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling step_dir.getValue(); + step_dir.attach(&received_angle); + + Serial.println(F("Step/Dir listenning.")); + _delay(1000); +} + +void loop() { + Serial.print(received_angle); + Serial.print("\t"); + Serial.println(step_dir.getValue()); + _delay(500); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino new file mode 100644 index 0000000..f897857 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/communication_test/step_dir/step_dir_motor_example/step_dir_motor_example.ino @@ -0,0 +1,98 @@ +/** + * A position control example using step/dir interface to update the motor position + */ + +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(10, 5, 6, 8); + +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// encoder instance +Encoder encoder = Encoder(2, 3, 500); +// channel A and B callbacks +void doA() { encoder.handleA(); } +void doB() { encoder.handleB(); } + +// StepDirListener( step_pin, dir_pin, counter_to_value) +StepDirListener step_dir = StepDirListener(A4, A5, 2.0f*_PI/200.0); +void onStep() { step_dir.handle(); } + +void setup() { + + // initialize encoder sensor hardware + encoder.init(); + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + // index search velocity [rad/s] + motor.velocity_index_search = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::angle; + + // contoller configuration + // default parameters in defaults.h + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle P controller + motor.P_angle.P = 10; + // maximal velocity of the position control + motor.velocity_limit = 100; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // init step and dir pins + step_dir.init(); + // enable interrupts + step_dir.enableInterrupt(onStep); + // attach the variable to be updated on each step (optional) + // the same can be done asynchronously by caling motor.move(step_dir.getValue()); + step_dir.attach(&motor.target); + + Serial.println(F("Motor ready.")); + Serial.println(F("Listening to step/dir commands!")); + _delay(1000); +} + +void loop() { + + // main FOC algorithm function + motor.loopFOC(); + + // Motion control function + motor.move(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino new file mode 100644 index 0000000..e999de2 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/generic_current_sense/generic_current_sense.ino @@ -0,0 +1,59 @@ +/** + * An example code for the generic current sensing implementation +*/ +#include + + +// user defined function for reading the phase currents +// returning the value per phase in amps +PhaseCurrent_s readCurrentSense(){ + PhaseCurrent_s c; + // dummy example only reading analog pins + c.a = analogRead(A0); + c.b = analogRead(A1); + c.c = analogRead(A2); // if no 3rd current sense set it to 0 + return(c); +} + +// user defined function for intialising the current sense +// it is optional and if provided it will be called in current_sense.init() +void initCurrentSense(){ + pinMode(A0,INPUT); + pinMode(A1,INPUT); + pinMode(A2,INPUT); +} + + +// GenericCurrentSense class constructor +// it receives the user defined callback for reading the current sense +// and optionally the user defined callback for current sense initialisation +GenericCurrentSense current_sense = GenericCurrentSense(readCurrentSense, initCurrentSense); + + +void setup() { + // if callbacks are not provided in the constructor + // they can be assigned directly: + //current_sense.readCallback = readCurrentSense; + //current_sense.initCallback = initCurrentSense; + + // initialise the current sensing + current_sense.init(); + + + Serial.begin(115200); + Serial.println("Current sense ready."); +} + +void loop() { + + PhaseCurrent_s currents = current_sense.getPhaseCurrents(); + float current_magnitude = current_sense.getDCCurrent(); + + Serial.print(currents.a); // milli Amps + Serial.print("\t"); + Serial.print(currents.b); // milli Amps + Serial.print("\t"); + Serial.print(currents.c); // milli Amps + Serial.print("\t"); + Serial.println(current_magnitude); // milli Amps +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino new file mode 100644 index 0000000..c1a5139 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/current_sense_test/inline_current_sense_test/inline_current_sense_test.ino @@ -0,0 +1,36 @@ +/** + * Testing example code for the Inline current sensing class +*/ +#include + +// current sensor +// shunt resistor value +// gain value +// pins phase A,B, (C optional) +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, A0, A2); + + +void setup() { + // initialise the current sensing + current_sense.init(); + + // for SimpleFOCShield v2.01/v2.0.2 + current_sense.gain_b *= -1; + + Serial.begin(115200); + Serial.println("Current sense ready."); +} + +void loop() { + + PhaseCurrent_s currents = current_sense.getPhaseCurrents(); + float current_magnitude = current_sense.getDCCurrent(); + + Serial.print(currents.a*1000); // milli Amps + Serial.print("\t"); + Serial.print(currents.b*1000); // milli Amps + Serial.print("\t"); + Serial.print(currents.c*1000); // milli Amps + Serial.print("\t"); + Serial.println(current_magnitude*1000); // milli Amps +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino new file mode 100644 index 0000000..1235fcc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_3pwm_standalone/bldc_driver_3pwm_standalone.ino @@ -0,0 +1,34 @@ +// BLDC driver standalone example +#include + + +// BLDC driver instance +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 50000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino new file mode 100644 index 0000000..478d397 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/bldc_driver_6pwm_standalone/bldc_driver_6pwm_standalone.ino @@ -0,0 +1,35 @@ +// BLDC driver standalone example +#include + +// BLDC driver instance +BLDCDriver6PWM driver = BLDCDriver6PWM(5, 6, 9,10, 3, 11, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 50000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + // daad_zone [0,1] - default 0.02f - 2% + driver.dead_zone = 0.05f; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + // phase C: 5V + driver.setPwm(3,6,5); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino new file mode 100644 index 0000000..4627efa --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_2pwm_standalone/stepper_driver_2pwm_standalone.ino @@ -0,0 +1,39 @@ +// Stepper driver standalone example +#include + + +// Stepper driver instance +// StepperDriver2PWM(pwm1, in1, pwm2, in2, (en1, en2 optional)) +int in1[] = {4,5}; +int in2[] = {9,8}; +StepperDriver2PWM driver = StepperDriver2PWM(3, in1, 10 , in2, 11, 12); + +// StepperDriver2PWM(pwm1, dir1, pwm2, dir2,(en1, en2 optional)) +// StepperDriver2PWM driver = StepperDriver2PWM(3, 4, 5, 6, 11, 12); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 30000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + driver.setPwm(3,6); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino new file mode 100644 index 0000000..e028a73 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/driver_standalone_test/stepper_driver_4pwm_standalone/stepper_driver_4pwm_standalone.ino @@ -0,0 +1,34 @@ +// Stepper driver standalone example +#include + + +// Stepper driver instance +// StepperDriver4PWM(ph1A, ph1B, ph2A, ph2B, (en1, en2 optional)) +StepperDriver4PWM driver = StepperDriver4PWM(5, 6, 9,10, 7, 8); + +void setup() { + + // pwm frequency to be used [Hz] + // for atmega328 fixed to 32kHz + // esp32/stm32/teensy configurable + driver.pwm_frequency = 30000; + // power supply voltage [V] + driver.voltage_power_supply = 12; + // Max DC voltage allowed - default voltage_power_supply + driver.voltage_limit = 12; + + // driver init + driver.init(); + + // enable driver + driver.enable(); + + _delay(1000); +} + +void loop() { + // setting pwm + // phase A: 3V + // phase B: 6V + driver.setPwm(3,6); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino new file mode 100644 index 0000000..f105f5b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_example/encoder_example.ino @@ -0,0 +1,44 @@ +/** + * Encoder example code + * + * This is a code intended to test the encoder connections and to demonstrate the encoder setup. + * + */ + +#include + + +Encoder encoder = Encoder(2, 3, 8192); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // enable/disable quadrature mode + encoder.quadrature = Quadrature::ON; + + // check if you need internal pullups + encoder.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + + Serial.println("Encoder ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // not doing much for the encoder though + encoder.update(); + // display the angle and the angular velocity to the terminal + Serial.print(encoder.getAngle()); + Serial.print("\t"); + Serial.println(encoder.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino new file mode 100644 index 0000000..ebf2dd4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/encoder/encoder_software_interrupts_example/encoder_software_interrupts_example.ino @@ -0,0 +1,57 @@ +/** + * Encoder example code using only software interrupts + * + * This is a code intended to test the encoder connections and to + * demonstrate the encoder setup fully using software interrupts. + * - We use PciManager library: https://github.com/prampec/arduino-pcimanager + * + * This code will work on Arduino devices but not on STM32 devices + * + */ + +#include +// software interrupt library +#include +#include + +// encoder instance +Encoder encoder = Encoder(A0, A1, 2048); +// interrupt routine intialisation +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + +// encoder interrupt init +PciListenerImp listenerA(encoder.pinA, doA); +PciListenerImp listenerB(encoder.pinB, doB); + +void setup() { + // monitoring port + Serial.begin(115200); + + // enable/disable quadrature mode + encoder.quadrature = Quadrature::ON; + + // check if you need internal pullups + encoder.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + encoder.init(); + + // interrupt initialization + PciManager.registerListener(&listenerA); + PciManager.registerListener(&listenerB); + + Serial.println("Encoder ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // not doing much for the encoder though + encoder.update(); + // display the angle and the angular velocity to the terminal + Serial.print(encoder.getAngle()); + Serial.print("\t"); + Serial.println(encoder.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino new file mode 100644 index 0000000..4a470e5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/generic_sensor/generic_sensor.ino @@ -0,0 +1,51 @@ +/** + * Generic sensor example code + * + * This is a code intended to demonstrate how to implement the generic sensor class + * + */ + +#include + +// sensor reading function example +// for the magnetic sensor with analog communication +// returning an angle in radians in between 0 and 2PI +float readSensor(){ + return analogRead(A0)*_2PI/1024.0; +} + +// sensor intialising function +void initSensor(){ + pinMode(A0,INPUT); +} + +// generic sensor class contructor +// - read sensor callback +// - init sensor callback (optional) +GenericSensor sensor = GenericSensor(readSensor, initSensor); + +void setup() { + // monitoring port + Serial.begin(115200); + + // if callbacks are not provided in the constructor + // they can be assigned directly: + //sensor.readCallback = readSensor; + //sensor.initCallback = initSensor; + + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino new file mode 100644 index 0000000..c4777ba --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_example/hall_sensor_example.ino @@ -0,0 +1,48 @@ +/** + * Hall sensor example code + * + * This is a code intended to test the hall sensors connections and to demonstrate the hall sensor setup. + * + */ + +#include + +// Hall sensor instance +// HallSensor(int hallA, int hallB , int cpr, int index) +// - hallA, hallB, hallC - HallSensor A, B and C pins +// - pp - pole pairs +HallSensor sensor = HallSensor(2, 3, 4, 14); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} + + +void setup() { + // monitoring port + Serial.begin(115200); + + // check if you need internal pullups + sensor.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + sensor.init(); + // hardware interrupt enable + sensor.enableInterrupts(doA, doB, doC); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); + delay(100); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino new file mode 100644 index 0000000..238eb2c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/hall_sensors/hall_sensor_software_interrupts_example/hall_sensor_software_interrupts_example.ino @@ -0,0 +1,59 @@ +/** + * Hall sensors example code using only software interrupts + * + * This is a code intended to test the hall sensor connections and to + * demonstrate the hall sensor setup fully using software interrupts. + * - We use PciManager library: https://github.com/prampec/arduino-pcimanager + * + * This code will work on Arduino devices but not on STM32 devices + */ + +#include +// software interrupt library +#include +#include + +// Hall sensor instance +// HallSensor(int hallA, int hallB , int cpr, int index) +// - hallA, hallB, hallC - HallSensor A, B and C pins +// - pp - pole pairs +HallSensor sensor = HallSensor(2, 3, 4, 11); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenA(sensor.pinA, doA); +PciListenerImp listenB(sensor.pinB, doB); +PciListenerImp listenC(sensor.pinC, doC); + +void setup() { + // monitoring port + Serial.begin(115200); + + // check if you need internal pullups + sensor.pullup = Pullup::USE_EXTERN; + + // initialise encoder hardware + sensor.init(); + + // software interrupts + PciManager.registerListener(&listenA); + PciManager.registerListener(&listenB); + PciManager.registerListener(&listenC); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino new file mode 100644 index 0000000..5fda5f5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/linear_hall_sensors/find_raw_centers/find_raw_centers.ino @@ -0,0 +1,62 @@ +/** + * An example to find the center offsets for both ADC channels used in the LinearHall sensor constructor + * Spin your motor through at least one full revolution to average out all of the variations in magnet strength. + */ + +//Change these defines to match the analog input pins that your hall sensors are connected to +#define LINEAR_HALL_CHANNEL_A 39 +#define LINEAR_HALL_CHANNEL_B 33 + + +//program variables +int minA, maxA, minB, maxB, centerA, centerB; +unsigned long timestamp; + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + pinMode(LINEAR_HALL_CHANNEL_A, INPUT); + pinMode(LINEAR_HALL_CHANNEL_B, INPUT); + + minA = analogRead(LINEAR_HALL_CHANNEL_A); + maxA = minA; + centerA = (minA + maxA) / 2; + minB = analogRead(LINEAR_HALL_CHANNEL_B); + maxB = minB; + centerB = (minB + maxB) / 2; + + Serial.println("Sensor ready"); + delay(1000); + timestamp = millis(); +} + +void loop() { + //read sensors and update variables + int tempA = analogRead(LINEAR_HALL_CHANNEL_A); + if (tempA < minA) minA = tempA; + if (tempA > maxA) maxA = tempA; + centerA = (minA + maxA) / 2; + int tempB = analogRead(LINEAR_HALL_CHANNEL_B); + if (tempB < minB) minB = tempB; + if (tempB > maxB) maxB = tempB; + centerB = (minB + maxB) / 2; + + if (millis() > timestamp + 100) { + timestamp = millis(); + // display the center counts, and max and min count + Serial.print("A:"); + Serial.print(centerA); + Serial.print("\t, B:"); + Serial.print(centerB); + Serial.print("\t, min A:"); + Serial.print(minA); + Serial.print("\t, max A:"); + Serial.print(maxA); + Serial.print("\t, min B:"); + Serial.print(minB); + Serial.print("\t, max B:"); + Serial.println(maxB); + } +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino new file mode 100644 index 0000000..34e68a1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/find_raw_min_max/find_raw_min_max.ino @@ -0,0 +1,56 @@ +#include + +/** + * An example to find out the raw max and min count to be provided to the constructor + * Spin your motor/sensor/magnet to see what is the maximum output of the sensor and what is the minimum value + * And replace values 14 and 1020 with new values. Once when you replace them make sure there is no jump in the angle reading sensor.getAngle(). + * If there is a jump that means you can still find better values. + */ + +/** + * Magnetic sensor reading analog voltage on pin A1. This voltage is proportional to rotation position. + * Tested on AS5600 magnetic sensor running in 'analog mode'. Note AS5600 works better in 'i2C mode' (less noise) but only supports one sensor per i2c bus. + * + * MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * - pinAnalog - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note ESP32 will be closer to 4096 with its 12bit ADC + */ +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +int max_count = 0; +int min_count = 100000; + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // keep track of min and max + if(sensor.raw_count > max_count) max_count = sensor.raw_count; + else if(sensor.raw_count < min_count) min_count = sensor.raw_count; + + // display the raw count, and max and min raw count + Serial.print("angle:"); + Serial.print(sensor.getAngle()); + Serial.print("\t, raw:"); + Serial.print(sensor.raw_count); + Serial.print("\t, min:"); + Serial.print(min_count); + Serial.print("\t, max:"); + Serial.println(max_count); + delay(100); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino new file mode 100644 index 0000000..2496151 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_analog/magnetic_sensor_analog_example/magnetic_sensor_analog_example.ino @@ -0,0 +1,37 @@ +#include + + + +/** + * Magnetic sensor reading analog voltage on pin A1. This voltage is proportional to rotation position. + * Tested on AS5600 magnetic sensor running in 'analog mode'. Note AS5600 works better in 'i2C mode' (less noise) but only supports one sensor per i2c bus. + * + * MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * - pinAnalog - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note ESP32 will be closer to 4096 with its 12bit ADC + */ +MagneticSensorAnalog sensor = MagneticSensorAnalog(A1, 14, 1020); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino new file mode 100644 index 0000000..0516ede --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/esp32_i2c_dual_bus_example/esp32_i2c_dual_bus_example.ino @@ -0,0 +1,40 @@ +#include + +/** Annoyingly some i2c sensors (e.g. AS5600) have a fixed chip address. This means only one of these devices can be addressed on a single bus + * This example shows how a second i2c bus can be used to communicate with a second sensor. + */ + +MagneticSensorI2C sensor0 = MagneticSensorI2C(AS5600_I2C); +MagneticSensorI2C sensor1 = MagneticSensorI2C(AS5600_I2C); + + +void setup() { + + Serial.begin(115200); + _delay(750); + + Wire.setClock(400000); + Wire1.setClock(400000); + + // Normally SimpleFOC will call begin for i2c but with esp32 begin() is the only way to set pins! + // It seems safe to call begin multiple times + Wire1.begin(19, 23, (uint32_t)400000); + + sensor0.init(); + sensor1.init(&Wire1); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor0.update(); + sensor1.update(); + + _delay(200); + Serial.print(sensor0.getAngle()); + Serial.print(" - "); + Serial.print(sensor1.getAngle()); + Serial.println(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino new file mode 100644 index 0000000..08fb145 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_dual_bus_examples/stm32_i2c_dual_bus_example/stm32_i2c_dual_bus_example.ino @@ -0,0 +1,39 @@ +#include + +/** Annoyingly some i2c sensors (e.g. AS5600) have a fixed chip address. This means only one of these devices can be addressed on a single bus + * This example shows how a second i2c bus can be used to communicate with a second sensor. + */ + +MagneticSensorI2C sensor0 = MagneticSensorI2C(AS5600_I2C); +MagneticSensorI2C sensor1 = MagneticSensorI2C(AS5600_I2C); + +// example of stm32 defining 2nd bus +TwoWire Wire1(PB11, PB10); + + +void setup() { + + Serial.begin(115200); + _delay(750); + + Wire.setClock(400000); + Wire1.setClock(400000); + + sensor0.init(); + sensor1.init(&Wire1); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor0.update(); + sensor1.update(); + + _delay(200); + Serial.print(sensor0.getAngle()); + Serial.print(" - "); + Serial.print(sensor1.getAngle()); + Serial.println(); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino new file mode 100644 index 0000000..4e060c0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_i2c/magnetic_sensor_i2c_example/magnetic_sensor_i2c_example.ino @@ -0,0 +1,43 @@ +#include + +// MagneticSensorI2C(uint8_t _chip_address, float _cpr, uint8_t _angle_register_msb) +// chip_address I2C chip address +// bit_resolution resolution of the sensor +// angle_register_msb angle read register msb +// bits_used_msb number of used bits in msb register +// +// make sure to read the chip address and the chip angle register msb value from the datasheet +// also in most cases you will need external pull-ups on SDA and SCL lines!!!!! +// +// For AS5058B +// MagneticSensorI2C sensor = MagneticSensorI2C(0x40, 14, 0xFE, 8); + +// Example of AS5600 configuration +MagneticSensorI2C sensor = MagneticSensorI2C(AS5600_I2C); + + +void setup() { + // monitoring port + Serial.begin(115200); + + // configure i2C + Wire.setClock(400000); + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino new file mode 100644 index 0000000..ac9bf04 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/find_raw_min_max/find_raw_min_max.ino @@ -0,0 +1,49 @@ +#include + + +/** + * An example to find out the raw max and min count to be provided to the constructor + * SPin your motor/sensor/magnet to see what is the maximum output of the sensor and what is the minimum value + * And replace values 4 and 904 with new values. Once when you replace them make sure there is no jump in the angle reading sensor.getAngle(). + * If there is a jump that means you can still find better values. + */ +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +void doPWM(){sensor.handlePWM();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + sensor.enableInterrupt(doPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +int max_pulse= 0; +int min_pulse = 10000; + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + + // keep track of min and max + if(sensor.pulse_length_us > max_pulse) max_pulse = sensor.pulse_length_us; + else if(sensor.pulse_length_us < min_pulse) min_pulse = sensor.pulse_length_us; + + // display the raw count, and max and min raw count + Serial.print("angle:"); + Serial.print(sensor.getAngle()); + Serial.print("\t, raw:"); + Serial.print(sensor.pulse_length_us); + Serial.print("\t, min:"); + Serial.print(min_pulse); + Serial.print("\t, max:"); + Serial.println(max_pulse); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino new file mode 100644 index 0000000..6ae0a3e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_example/magnetic_sensor_pwm_example.ino @@ -0,0 +1,38 @@ +#include + + +/** + * Magnetic sensor reading pwm signal on pin 2. The pwm duty cycle is proportional to the sensor angle. + * + * MagneticSensorPWM(uint8_t MagneticSensorPWM, int _min, int _max) + * - pinPWM - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~5. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 1kHz = 1000 it is often ~910. depending on the exact frequency and saturation + */ +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +void doPWM(){sensor.handlePWM();} + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + sensor.enableInterrupt(doPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino new file mode 100644 index 0000000..10dc8a6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_pwm/magnetic_sensor_pwm_software_interrupt/magnetic_sensor_pwm_software_interrupt.ino @@ -0,0 +1,44 @@ +#include + +// software interrupt library +#include +#include + +/** + * Magnetic sensor reading analog voltage on pin which does not have hardware interrupt support. Such as A0. + * + * MagneticSensorPWM(uint8_t MagneticSensorPWM, int _min, int _max) + * - pinPWM - the pin that is reading the pwm from magnetic sensor + * - min_raw_count - the smallest expected reading. Whilst you might expect it to be 0 it is often ~5. Getting this wrong results in a small click once per revolution + * - max_raw_count - the largest value read. whilst you might expect it to be 1kHz = 1000 it is often ~910. depending on the exact frequency and saturation + */ +MagneticSensorPWM sensor = MagneticSensorPWM(A0, 4, 904); +void doPWM(){sensor.handlePWM();} + +// encoder interrupt init +PciListenerImp listenerPWM(sensor.pinPWM, doPWM); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + // comment out to use sensor in blocking (non-interrupt) way + PciManager.registerListener(&listenerPWM); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino new file mode 100644 index 0000000..cbb72af --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/esp32_spi_alt_example/esp32_spi_alt_example.ino @@ -0,0 +1,41 @@ +#include + +// alternative pinout +#define HSPI_MISO 12 +#define HSPI_MOSI 13 +#define HSPI_SCLK 14 +#define HSPI_SS 15 + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, HSPI_SS); + +// for esp 32, it has 2 spi interfaces VSPI (default) and HPSI as the second one +// to enable it instatiate the object +SPIClass SPI_2(HSPI); + +void setup() { + // monitoring port + Serial.begin(115200); + + // start the newly defined spi communication + SPI_2.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_SS); //SCLK, MISO, MOSI, SS + // initialise magnetic sensor hardware + sensor.init(&SPI_2); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino new file mode 100644 index 0000000..713a19b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_alternative_examples/stm32_spi_alt_example/stm32_spi_alt_example.ino @@ -0,0 +1,32 @@ +#include + +// MagneticSensorSPI(int cs, float _cpr, int _angle_register) +// config - SPI config +// cs - SPI chip select pin +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, PA15); + +// these are valid pins (mosi, miso, sclk) for 2nd SPI bus on storm32 board (stm32f107rc) +SPIClass SPI_2(PB15, PB14, PB13); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(&SPI_2); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino new file mode 100644 index 0000000..048620a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/examples/utils/sensor_test/magnetic_sensors/magnetic_sensor_spi/magnetic_sensor_spi_example/magnetic_sensor_spi_example.ino @@ -0,0 +1,32 @@ +#include + +// MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs) +// config - SPI config +// cs - SPI chip select pin +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5147_SPI, 10); +// alternative constructor (chipselsect, bit_resolution, angle_read_register, ) +// MagneticSensorSPI sensor = MagneticSensorSPI(10, 14, 0x3FFF); + +void setup() { + // monitoring port + Serial.begin(115200); + + // initialise magnetic sensor hardware + sensor.init(); + + Serial.println("Sensor ready"); + _delay(1000); +} + +void loop() { + // iterative function updating the sensor internal variables + // it is usually called in motor.loopFOC() + // this function reads the sensor hardware and + // has to be called before getAngle nad getVelocity + sensor.update(); + // display the angle and the angular velocity to the terminal + Serial.print(sensor.getAngle()); + Serial.print("\t"); + Serial.println(sensor.getVelocity()); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/keywords.txt b/.pio/libdeps/lemon-pepper/Simple FOC/keywords.txt new file mode 100644 index 0000000..1f200e5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/keywords.txt @@ -0,0 +1,251 @@ +ArduinoFOC KEYWORD1 +SimpleFOC KEYWORD1 +BLDCMotor KEYWORD1 +FOCMotor KEYWORD1 +StepperMotor KEYWORD1 +Encoder KEYWORD1 +HallSensors KEYWORD1 +MagneticSensorSPI KEYWORD1 +MagneticSensorI2C KEYWORD1 +MagneticSensorAnalog KEYWORD1 +MagneticSensorPWM KEYWORD1 +BLDCDriver3PWM KEYWORD1 +BLDCDriver6PWM KEYWORD1 +BLDCDriver KEYWORD1 +StepperDriver4PWM KEYWORD1 +StepperDriver2PWM KEYWORD1 +StepperDriver KEYWORD1 +PIDController KEYWORD1 +LowPassFilter KEYWORD1 +InlineCurrentSense KEYWORD1 +LowsideCurrentSense KEYWORD1 +CurrentSense KEYWORD1 +Commander KEYWORD1 +StepDirListener KEYWORD1 +GenericCurrentSense KEYWORD1 +GenericSensor KEYWORD1 +SimpleFOCDebug KEYWORD1 + +initFOC KEYWORD2 +loopFOC KEYWORD2 +disable KEYWORD2 + +_delay KEYWORD3 +_sqrt KEYWORD3 +_micros KEYWORD3 +_sin KEYWORD3 +_cos KEYWORD3 +_setPwmFrequency KEYWORD3 +_writeDutyCycle KEYWORD3 +_round KEYWORD3 +_sign KEYWORD3 +_constrain KEYWORD3 +monitor KEYWORD3 +command KEYWORD3 + +PID_velocity KEYWORD2 +PID_current_q KEYWORD2 +PID_current_d KEYWORD2 +LPF_velocity KEYWORD2 +LPF_current_q KEYWORD2 +LPF_current_d KEYWORD2 +P_angle KEYWORD2 +LPF_angle KEYWORD2 +lpf_a KEYWORD2 +lpf_b KEYWORD2 +lpf_c KEYWORD2 + +MotionControlType KEYWORD1 +TorqueControlType KEYWORD1 +FOCModulationType KEYWORD2 +Quadrature KEYWORD1 +Pullup KEYWORD1 +Direction KEYWORD1 +MagneticSensorI2CConfig_s KEYWORD1 +MagneticSensorSPIConfig_s KEYWORD1 +DQVoltage_s KEYWORD1 +DQCurrent_s KEYWORD1 +PhaseCurrent_s KEYWORD1 + +linkDriver KEYWORD2 +linkSensor KEYWORD2 +linkCurrentSense KEYWORD2 +handleA KEYWORD2 +handleB KEYWORD2 +handleIndex KEYWORD2 +handleC KEYWORD2 +enableInterrupts KEYWORD2 +ISR KEYWORD2 +getVelocity KEYWORD2 +setPhaseVoltage KEYWORD2 +getAngle KEYWORD2 +getMechanicalAngle KEYWORD2 +getSensorAngle KEYWORD2 +update KEYWORD2 +needsSearch KEYWORD2 +useMonitoring KEYWORD2 +angleOpenloop KEYWORD2 +velocityOpenloop KEYWORD2 +getPhaseCurrents KEYWORD2 +getFOCCurrents KEYWORD2 +getDCCurrent KEYWORD2 +setPwm KEYWORD2 +driverAlign KEYWORD2 +linkDriver KEYWORD2 +add KEYWORD2 +run KEYWORD2 +attach KEYWORD2 +enableInterrupt KEYWORD2 +getValue KEYWORD2 +handle KEYWORD2 +scalar KEYWORD2 +pid KEYWORD2 +lpf KEYWORD2 +motor KEYWORD2 +handlePWM KEYWORD2 +enableInterrupt KEYWORD2 +readCallback KEYWORD2 +initCallback KEYWORD2 + + + +current KEYWORD2 +current_measured KEYWORD2 +shaft_angle_sp KEYWORD2 +electrical_angle KEYWORD2 +shaft_velocity_sp KEYWORD2 +shaft_angle KEYWORD2 +shaft_velocity KEYWORD2 +torque_controller KEYWORD2 +controller KEYWORD2 +pullup KEYWORD2 +quadrature KEYWORD2 +foc_modulation KEYWORD2 +target KEYWORD2 +motion KEYWORD2 +pwm_frequency KEYWORD2 +dead_zone KEYWORD2 +gain_a KEYWORD2 +gain_b KEYWORD2 +gain_c KEYWORD2 +skip_align KEYWORD2 +sensor_direction KEYWORD2 +sensor_offset KEYWORD2 +zero_electric_angle KEYWORD2 +verbose KEYWORD2 +decimal_places KEYWORD2 +phase_resistance KEYWORD2 +phase_inductance KEYWORD2 +modulation_centered KEYWORD2 + + + +voltage KEYWORD2 +velocity KEYWORD2 +velocity_openloop KEYWORD2 +angle KEYWORD2 +angle_openloop KEYWORD2 +torque KEYWORD2 +dc_current KEYWORD2 +foc_current KEYWORD2 + + +ON KEYWORD2 +OFF KEYWORD2 +CW KEYWORD2 +CCW KEYWORD2 +UNKNOWN KEYWORD2 + +P KEYWORD2 +I KEYWORD2 +D KEYWORD2 +Tf KEYWORD2 +voltage_limit KEYWORD2 +current_limit KEYWORD2 +output_ramp KEYWORD2 +limit KEYWORD2 +velocity_limit KEYWORD2 +voltage_power_supply KEYWORD2 +voltage_sensor_align KEYWORD2 +velocity_index_search KEYWORD2 +monitor_downsample KEYWORD2 +monitor_start_char KEYWORD2 +monitor_end_char KEYWORD2 +monitor_separator KEYWORD2 +monitor_decimals KEYWORD2 +monitor_variables KEYWORD2 +motion_downsample KEYWORD2 + +pinA KEYWORD2 +pinB KEYWORD2 +pinC KEYWORD2 +index_pin KEYWORD2 + +USE_INTERN KEYWORD2 +USE_EXTERN KEYWORD2 +DISABLE KEYWORD2 +ENABLE KEYWORD2 +SpaceVectorPWM KEYWORD2 +SinePWM KEYWORD2 +Trapezoid_120 KEYWORD2 +Trapezoid_150 KEYWORD2 + +pwmA KEYWORD2 +pwmB KEYWORD2 +pwmC KEYWORD2 +pwm1A KEYWORD2 +pwm1B KEYWORD2 +pwm2A KEYWORD2 +pwm2B KEYWORD2 +Ualpha KEYWORD2 +Ubeta KEYWORD2 +Ua KEYWORD2 +Ub KEYWORD2 +Uc KEYWORD2 +enable_pin KEYWORD2 +enable_pin1 KEYWORD2 +enable_pin2 KEYWORD2 +pole_pairs KEYWORD2 +dc_a KEYWORD2 +dc_b KEYWORD2 +dc_c KEYWORD2 + +DEF_POWER_SUPPLY LITERAL1 +DEF_PID_VEL_P LITERAL1 +DEF_PID_VEL_I LITERAL1 +DEF_PID_VEL_D LITERAL1 +DEF_PID_VEL_RAMP LITERAL1 +DEF_P_ANGLE_P LITERAL1 +DEF_PID_VEL_LIMIT LITERAL1 +DEF_INDEX_SEARCH_TARGET_VELOCITY LITERAL1 +DEF_VOLTAGE_SENSOR_ALIGN LITERAL1 +DEF_VEL_FILTER_Tf LITERAL1 +DEF_CURRENT_LIM LITERAL1 +DEF_CURR_FILTER_Tf LITERAL1 +DEF_PID_CURR_LIMIT LITERAL1 +DEF_PID_CURR_RAMP LITERAL1 +DEF_PID_CURR_D LITERAL1 +DEF_PID_CURR_I LITERAL1 +DEF_PID_CURR_P LITERAL1 +_2_SQRT3 LITERAL1 +_1_SQRT3 LITERAL1 +_SQRT3_2 LITERAL1 +_SQRT2 LITERAL1 +_120_D2R LITERAL1 +_PI_2 LITERAL1 +_PI_6 LITERAL1 +_2PI LITERAL1 +_3PI_2 LITERAL1 +_PI_3 LITERAL1 +_SQRT3 LITERAL1 +_PI LITERAL1 +_HIGH_Z LITERAL1 +_NC LITERAL1 + +_MON_TARGET LITERAL1 +_MON_VOLT_Q LITERAL1 +_MON_VOLT_D LITERAL1 +_MON_CURR_Q LITERAL1 +_MON_CURR_D LITERAL1 +_MON_VEL LITERAL1 +_MON_ANGLE LITERAL1 \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/library.properties b/.pio/libdeps/lemon-pepper/Simple FOC/library.properties new file mode 100644 index 0000000..6904387 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/library.properties @@ -0,0 +1,10 @@ +name=Simple FOC +version=2.3.1 +author=Simplefoc +maintainer=Simplefoc +sentence=A library demistifying FOC for BLDC motors +paragraph=Simple library intended for hobby comunity to run the BLDC and Stepper motor using FOC algorithm. It is intended to support as many BLDC/Stepper motor+sensor+driver combinations as possible and in the same time maintain simplicity of usage. Library supports Arudino devices such as Arduino UNO, MEGA, NANO and similar, stm32 boards such as Nucleo and Bluepill, ESP32 and Teensy boards. +category=Device Control +url=https://docs.simplefoc.com +architectures=* +includes=SimpleFOC.h diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.cpp new file mode 100644 index 0000000..b72728b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.cpp @@ -0,0 +1,736 @@ +#include "BLDCMotor.h" +#include "./communication/SimpleFOCDebug.h" + + +// see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 5 +// each is 60 degrees with values for 3 phases of 1=positive -1=negative 0=high-z +int trap_120_map[6][3] = { + {_HIGH_IMPEDANCE,1,-1}, + {-1,1,_HIGH_IMPEDANCE}, + {-1,_HIGH_IMPEDANCE,1}, + {_HIGH_IMPEDANCE,-1,1}, + {1,-1,_HIGH_IMPEDANCE}, + {1,_HIGH_IMPEDANCE,-1} +}; + +// see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 8 +// each is 30 degrees with values for 3 phases of 1=positive -1=negative 0=high-z +int trap_150_map[12][3] = { + {_HIGH_IMPEDANCE,1,-1}, + {-1,1,-1}, + {-1,1,_HIGH_IMPEDANCE}, + {-1,1,1}, + {-1,_HIGH_IMPEDANCE,1}, + {-1,-1,1}, + {_HIGH_IMPEDANCE,-1,1}, + {1,-1,1}, + {1,-1,_HIGH_IMPEDANCE}, + {1,-1,-1}, + {1,_HIGH_IMPEDANCE,-1}, + {1,1,-1} +}; + +// BLDCMotor( int pp , float R) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance +BLDCMotor::BLDCMotor(int pp, float _R, float _KV, float _inductance) +: FOCMotor() +{ + // save pole pairs number + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/KV + // 1/sqrt(2) - rms value + KV_rating = NOT_SET; + if (_isset(_KV)) + KV_rating = _KV*_SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + torque_controller = TorqueControlType::voltage; +} + + +/** + Link the driver which controls the motor +*/ +void BLDCMotor::linkDriver(BLDCDriver* _driver) { + driver = _driver; +} + +// init hardware pins +void BLDCMotor::init() { + if (!driver || !driver->initialized) { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if(voltage_limit > driver->voltage_limit) voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if(voltage_sensor_align > voltage_limit) voltage_sensor_align = voltage_limit; + + // update the controller limits + if(current_sense){ + // current control loop controls voltage + PID_current_q.limit = voltage_limit; + PID_current_d.limit = voltage_limit; + } + if(_isset(phase_resistance) || torque_controller != TorqueControlType::voltage){ + // velocity control loop controls current + PID_velocity.limit = current_limit; + }else{ + // velocity control loop controls the voltage + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + // if using open loop control, set a CW as the default direction if not already set + if ((controller==MotionControlType::angle_openloop + ||controller==MotionControlType::velocity_openloop) + && (sensor_direction == Direction::UNKNOWN)) { + sensor_direction = Direction::CW; + } + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + motor_status = FOCMotorStatus::motor_uncalibrated; +} + + +// disable motor driver +void BLDCMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0, 0); + // disable the driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void BLDCMotor::enable() +{ + // enable the driver + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0, 0); + // motor status update + enabled = 1; +} + +/** + FOC functions +*/ +// FOC initialization function +int BLDCMotor::initFOC() { + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // align motor if necessary + // alignment necessary for encoders! + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if(sensor){ + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = shaftAngle(); + }else { + exit_flag = 0; // no FOC without sensor + SIMPLEFOC_DEBUG("MOT: No sensor."); + } + + // aligning the current sensor - can be skipped + // checks if driver phases are the same as current sense phases + // and checks the direction of measuremnt. + _delay(500); + if(exit_flag){ + if(current_sense){ + if (!current_sense->initialized) { + motor_status = FOCMotorStatus::motor_calib_failed; + SIMPLEFOC_DEBUG("MOT: Init FOC error, current sense not initialized"); + exit_flag = 0; + }else{ + exit_flag *= alignCurrentSense(); + } + } + else { SIMPLEFOC_DEBUG("MOT: No current sense."); } + } + + if(exit_flag){ + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + }else{ + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Calibarthe the motor and current sense phases +int BLDCMotor::alignCurrentSense() { + int exit_flag = 1; // success + + SIMPLEFOC_DEBUG("MOT: Align current sense."); + + // align current sense and the driver + exit_flag = current_sense->driverAlign(voltage_sensor_align); + if(!exit_flag){ + // error in current sense - phase either not measured or bad connection + SIMPLEFOC_DEBUG("MOT: Align error!"); + exit_flag = 0; + }else{ + // output the alignment status flag + SIMPLEFOC_DEBUG("MOT: Success: ", exit_flag); + } + + return exit_flag > 0; +} + +// Encoder alignment to electrical 0 angle +int BLDCMotor::alignSensor() { + int exit_flag = 1; //success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // check if sensor needs zero search + if(sensor->needsSearch()) exit_flag = absoluteZeroSearch(); + // stop init if not found index + if(!exit_flag) return exit_flag; + + // if unknown natural direction + if(sensor_direction==Direction::UNKNOWN){ + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + float moved = fabs(mid_angle - end_angle); + if (moved 0.5f ) { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI/moved); + } else { + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + + } else { SIMPLEFOC_DEBUG("MOT: Skip dir calib."); } + + // zero electric angle not known + if(!_isset(zero_electric_angle)){ + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + //zero_electric_angle = _normalizeAngle(_electricalAngle(sensor_direction*sensor->getAngle(), pole_pairs)); + _delay(20); + if(monitor_port){ + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } else { SIMPLEFOC_DEBUG("MOT: Skip offset calib."); } + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int BLDCMotor::absoluteZeroSearch() { + // sensor precision: this is all ok, as the search happens near the 0-angle, where the precision + // of float is sufficient. + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while(sensor->needsSearch() && shaft_angle < _2PI){ + angleOpenloop(1.5f*_2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if(monitor_port){ + if(sensor->needsSearch()) { SIMPLEFOC_DEBUG("MOT: Error: Not found!"); } + else { SIMPLEFOC_DEBUG("MOT: Success!"); } + } + return !sensor->needsSearch(); +} + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void BLDCMotor::loopFOC() { + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) sensor->update(); + + // if open-loop do nothing + if( controller==MotionControlType::angle_openloop || controller==MotionControlType::velocity_openloop ) return; + + // if disabled do nothing + if(!enabled) return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + switch (torque_controller) { + case TorqueControlType::voltage: + // no need to do anything really + break; + case TorqueControlType::dc_current: + if(!current_sense) return; + // read overall current magnitude + current.q = current_sense->getDCCurrent(electrical_angle); + // filter the value values + current.q = LPF_current_q(current.q); + // calculate the phase voltage + voltage.q = PID_current_q(current_sp - current.q); + // d voltage - lag compensation + if(_isset(phase_inductance)) voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + else voltage.d = 0; + break; + case TorqueControlType::foc_current: + if(!current_sense) return; + // read dq currents + current = current_sense->getFOCCurrents(electrical_angle); + // filter values + current.q = LPF_current_q(current.q); + current.d = LPF_current_d(current.d); + // calculate the phase voltages + voltage.q = PID_current_q(current_sp - current.q); + voltage.d = PID_current_d(-current.d); + // d voltage - lag compensation - TODO verify + // if(_isset(phase_inductance)) voltage.d = _constrain( voltage.d - current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + default: + // no torque control selected + SIMPLEFOC_DEBUG("MOT: no torque control selected!"); + break; + } + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or torque loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void BLDCMotor::move(float new_target) { + + // downsampling (optional) + if(motion_cnt++ < motion_downsample) return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if( controller!=MotionControlType::angle_openloop && controller!=MotionControlType::velocity_openloop ) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity TODO the velocity reading probably also shouldn't happen in open loop modes? + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if(!enabled) return; + // set internal target variable + if(_isset(new_target)) target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) voltage_bemf = shaft_velocity/KV_rating/_RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if(!current_sense && _isset(phase_resistance)) current.q = (voltage.q - voltage_bemf)/phase_resistance; + + // upgrade the current based voltage limit + switch (controller) { + case MotionControlType::torque: + if(torque_controller == TorqueControlType::voltage){ // if voltage torque control + if(!_isset(phase_resistance)) voltage.q = target; + else voltage.q = target*phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -target*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + }else{ + current_sp = target; // if current/foc_current torque control + } + break; + case MotionControlType::angle: + // TODO sensor precision: this calculation is not numerically precise. The target value cannot express precise positions when + // the angles are large. This results in not being able to command small changes at high position values. + // to solve this, the delta-angle has to be calculated in a numerically precise way. + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = feed_forward_velocity + P_angle( shaft_angle_sp - shaft_angle ); + shaft_angle_sp = _constrain(shaft_angle_sp,-velocity_limit, velocity_limit); + // calculate the torque command - sensor precision: this calculation is ok, but based on bad value from previous calculation + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + if(torque_controller == TorqueControlType::voltage){ + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + } + break; + case MotionControlType::velocity: + // velocity set point - sensor precision: this calculation is numerically precise. + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + if(torque_controller == TorqueControlType::voltage){ + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + } + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop - sensor precision: this calculation is numerically precise. + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; + break; + case MotionControlType::angle_openloop: + // angle control in open loop - + // TODO sensor precision: this calculation NOT numerically precise, and subject + // to the same problems in small set-point changes at high angles + // as the closed loop version. + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; + break; + } +} + + +// Method using FOC to set Uq and Ud to the motor at the optimal angle +// Function implementing Space Vector PWM and Sine PWM algorithms +// +// Function using sine approximation +// regular sin + cos ~300us (no memory usage) +// approx _sin + _cos ~110us (400Byte ~ 20% of memory) +void BLDCMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) { + + float center; + int sector; + float _ca,_sa; + + switch (foc_modulation) + { + case FOCModulationType::Trapezoid_120 : + // see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 5 + // determine the sector + sector = 6 * (_normalizeAngle(angle_el + _PI_6 ) / _2PI); // adding PI/6 to align with other modes + // centering the voltages around either + // modulation_centered == true > driver.voltage_limit/2 + // modulation_centered == false > or Adaptable centering, all phases drawn to 0 when Uq=0 + center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + + if(trap_120_map[sector][0] == _HIGH_IMPEDANCE){ + Ua= center; + Ub = trap_120_map[sector][1] * Uq + center; + Uc = trap_120_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_120_map[sector][1] == _HIGH_IMPEDANCE){ + Ua = trap_120_map[sector][0] * Uq + center; + Ub = center; + Uc = trap_120_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_OFF, PhaseState::PHASE_ON);// disable phase if possible + }else{ + Ua = trap_120_map[sector][0] * Uq + center; + Ub = trap_120_map[sector][1] * Uq + center; + Uc = center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_OFF);// disable phase if possible + } + + break; + + case FOCModulationType::Trapezoid_150 : + // see https://www.youtube.com/watch?v=InzXA7mWBWE Slide 8 + // determine the sector + sector = 12 * (_normalizeAngle(angle_el + _PI_6 ) / _2PI); // adding PI/6 to align with other modes + // centering the voltages around either + // modulation_centered == true > driver.voltage_limit/2 + // modulation_centered == false > or Adaptable centering, all phases drawn to 0 when Uq=0 + center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + + if(trap_150_map[sector][0] == _HIGH_IMPEDANCE){ + Ua= center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_150_map[sector][1] == _HIGH_IMPEDANCE){ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_OFF, PhaseState::PHASE_ON); // disable phase if possible + }else if(trap_150_map[sector][2] == _HIGH_IMPEDANCE){ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_OFF); // disable phase if possible + }else{ + Ua = trap_150_map[sector][0] * Uq + center; + Ub = trap_150_map[sector][1] * Uq + center; + Uc = trap_150_map[sector][2] * Uq + center; + driver->setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_ON); // enable all phases + } + + break; + + case FOCModulationType::SinePWM : + // Sinusoidal PWM modulation + // Inverse Park + Clarke transformation + _sincos(angle_el, &_sa, &_ca); + + // Inverse park transform + Ualpha = _ca * Ud - _sa * Uq; // -sin(angle) * Uq; + Ubeta = _sa * Ud + _ca * Uq; // cos(angle) * Uq; + + // center = modulation_centered ? (driver->voltage_limit)/2 : Uq; + center = driver->voltage_limit/2; + // Clarke transform + Ua = Ualpha + center; + Ub = -0.5f * Ualpha + _SQRT3_2 * Ubeta + center; + Uc = -0.5f * Ualpha - _SQRT3_2 * Ubeta + center; + + if (!modulation_centered) { + float Umin = min(Ua, min(Ub, Uc)); + Ua -= Umin; + Ub -= Umin; + Uc -= Umin; + } + + break; + + case FOCModulationType::SpaceVectorPWM : + // Nice video explaining the SpaceVectorModulation (SVPWM) algorithm + // https://www.youtube.com/watch?v=QMSWUMEAejg + + // the algorithm goes + // 1) Ualpha, Ubeta + // 2) Uout = sqrt(Ualpha^2 + Ubeta^2) + // 3) angle_el = atan2(Ubeta, Ualpha) + // + // equivalent to 2) because the magnitude does not change is: + // Uout = sqrt(Ud^2 + Uq^2) + // equivalent to 3) is + // angle_el = angle_el + atan2(Uq,Ud) + + float Uout; + // a bit of optitmisation + if(Ud){ // only if Ud and Uq set + // _sqrt is an approx of sqrt (3-4% error) + Uout = _sqrt(Ud*Ud + Uq*Uq) / driver->voltage_limit; + // angle normalisation in between 0 and 2pi + // only necessary if using _sin and _cos - approximation functions + angle_el = _normalizeAngle(angle_el + atan2(Uq, Ud)); + }else{// only Uq available - no need for atan2 and sqrt + Uout = Uq / driver->voltage_limit; + // angle normalisation in between 0 and 2pi + // only necessary if using _sin and _cos - approximation functions + angle_el = _normalizeAngle(angle_el + _PI_2); + } + // find the sector we are in currently + sector = floor(angle_el / _PI_3) + 1; + // calculate the duty cycles + float T1 = _SQRT3*_sin(sector*_PI_3 - angle_el) * Uout; + float T2 = _SQRT3*_sin(angle_el - (sector-1.0f)*_PI_3) * Uout; + // two versions possible + float T0 = 0; // pulled to 0 - better for low power supply voltage + if (modulation_centered) { + T0 = 1 - T1 - T2; // modulation_centered around driver->voltage_limit/2 + } + + // calculate the duty cycles(times) + float Ta,Tb,Tc; + switch(sector){ + case 1: + Ta = T1 + T2 + T0/2; + Tb = T2 + T0/2; + Tc = T0/2; + break; + case 2: + Ta = T1 + T0/2; + Tb = T1 + T2 + T0/2; + Tc = T0/2; + break; + case 3: + Ta = T0/2; + Tb = T1 + T2 + T0/2; + Tc = T2 + T0/2; + break; + case 4: + Ta = T0/2; + Tb = T1+ T0/2; + Tc = T1 + T2 + T0/2; + break; + case 5: + Ta = T2 + T0/2; + Tb = T0/2; + Tc = T1 + T2 + T0/2; + break; + case 6: + Ta = T1 + T2 + T0/2; + Tb = T0/2; + Tc = T1 + T0/2; + break; + default: + // possible error state + Ta = 0; + Tb = 0; + Tc = 0; + } + + // calculate the phase voltages and center + Ua = Ta*driver->voltage_limit; + Ub = Tb*driver->voltage_limit; + Uc = Tc*driver->voltage_limit; + break; + + } + + // set the voltages in driver + driver->setPwm(Ua, Ub, Uc); +} + + + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float BLDCMotor::velocityOpenloop(float target_velocity){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float BLDCMotor::angleOpenloop(float target_angle){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + // TODO sensor precision: this calculation is not numerically precise. The angle can grow to the point + // where small position changes are no longer captured by the precision of floats + // when the total position is large. + if(abs( target_angle - shaft_angle ) > abs(velocity_limit*Ts)){ + shaft_angle += _sign(target_angle - shaft_angle) * abs( velocity_limit )*Ts; + shaft_velocity = velocity_limit; + }else{ + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + // sensor precision: this calculation is OK due to the normalisation + setPhaseVoltage(Uq, 0, _electricalAngle(_normalizeAngle(shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.h new file mode 100644 index 0000000..a1f196a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/BLDCMotor.h @@ -0,0 +1,115 @@ +#ifndef BLDCMotor_h +#define BLDCMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/Sensor.h" +#include "common/base_classes/BLDCDriver.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + BLDC motor class +*/ +class BLDCMotor: public FOCMotor +{ + public: + /** + BLDCMotor class constructor + @param pp pole pairs number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + BLDCMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver BLDCDriver class implementing all the hardware specific functions necessary PWM setting + */ + virtual void linkDriver(BLDCDriver* driver); + + /** + * BLDCDriver link: + * - 3PWM + * - 6PWM + */ + BLDCDriver* driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + */ + int initFOC() override; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + + /** + * Function executing the control loops set by the controller parameter of the BLDCMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ua, Ub, Uc;//!< Current phase voltages Ua,Ub and Uc set to motor + float Ualpha, Ubeta; //!< Phase voltages U alpha and U beta used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + + private: + // FOC methods + + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Current sense and motor phase alignment */ + int alignCurrentSense(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/SimpleFOC.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/SimpleFOC.h new file mode 100644 index 0000000..4e6815e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/SimpleFOC.h @@ -0,0 +1,119 @@ +/*! + * @file SimpleFOC.h + * + * @mainpage Simple Field Oriented Control BLDC motor control library + * + * @section intro_sec Introduction + * + * Proper low-cost and low-power FOC supporting boards are very hard to find these days and even may not exist.
Even harder to find is a stable and simple FOC algorithm code capable of running on Arduino devices. Therefore this is an attempt to: + * - Demystify FOC algorithm and make a robust but simple Arduino library: Arduino SimpleFOC library + * - Develop a modular BLDC driver board: Arduino SimpleFOC shield. + * + * @section features Features + * - Arduino compatible: Arduino library code + * - Easy to setup and configure: + * - Easy hardware configuration + * - Easy tuning the control loops + * - Modular: + * - Supports as many sensors , BLDC motors and driver boards as possible + * - Supports as many application requirements as possible + * - Plug & play: Arduino SimpleFOC shield + * + * @section dependencies Supported Hardware + * - Motors + * - BLDC motors + * - Stepper motors + * - Drivers + * - BLDC drivers + * - Gimbal drivers + * - Stepper drivers + * - Position sensors + * - Encoders + * - Magnetic sensors + * - Hall sensors + * - Open-loop control + * - Microcontrollers + * - Arduino + * - STM32 + * - ESP32 + * - Teensy + * + * @section example_code Example code + * @code +#include + +// BLDCMotor( pole_pairs ) +BLDCMotor motor = BLDCMotor(11); +// BLDCDriver( pin_pwmA, pin_pwmB, pin_pwmC, enable (optional) ) +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11, 8); +// Encoder(pin_A, pin_B, CPR) +Encoder encoder = Encoder(2, 3, 2048); +// channel A and B callbacks +void doA(){encoder.handleA();} +void doB(){encoder.handleB();} + + +void setup() { + // initialize encoder hardware + encoder.init(); + // hardware interrupt enable + encoder.enableInterrupts(doA, doB); + // link the motor to the sensor + motor.linkSensor(&encoder); + + // power supply voltage [V] + driver.voltage_power_supply = 12; + // initialise driver hardware + driver.init(); + // link driver + motor.linkDriver(&driver); + + // set control loop type to be used + motor.controller = MotionControlType::velocity; + // initialize motor + motor.init(); + + // align encoder and start FOC + motor.initFOC(); +} + +void loop() { + // FOC algorithm function + motor.loopFOC(); + + // velocity control loop function + // setting the target velocity or 2rad/s + motor.move(2); +} + * @endcode + * + * @section license License + * + * MIT license, all text here must be included in any redistribution. + * + */ + +#ifndef SIMPLEFOC_H +#define SIMPLEFOC_H + +#include "BLDCMotor.h" +#include "StepperMotor.h" +#include "sensors/Encoder.h" +#include "sensors/MagneticSensorSPI.h" +#include "sensors/MagneticSensorI2C.h" +#include "sensors/MagneticSensorAnalog.h" +#include "sensors/MagneticSensorPWM.h" +#include "sensors/HallSensor.h" +#include "sensors/GenericSensor.h" +#include "drivers/BLDCDriver3PWM.h" +#include "drivers/BLDCDriver6PWM.h" +#include "drivers/StepperDriver4PWM.h" +#include "drivers/StepperDriver2PWM.h" +#include "current_sense/InlineCurrentSense.h" +#include "current_sense/LowsideCurrentSense.h" +#include "current_sense/GenericCurrentSense.h" +#include "communication/Commander.h" +#include "communication/StepDirListener.h" +#include "communication/SimpleFOCDebug.h" + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.cpp new file mode 100644 index 0000000..491a20e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.cpp @@ -0,0 +1,443 @@ +#include "StepperMotor.h" +#include "./communication/SimpleFOCDebug.h" + + +// StepperMotor(int pp) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance [H] +StepperMotor::StepperMotor(int pp, float _R, float _KV, float _inductance) +: FOCMotor() +{ + // number od pole pairs + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/K_bemf + // usually used rms + KV_rating = _KV*_SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + // current and foc_current not supported yet + torque_controller = TorqueControlType::voltage; +} + +/** + Link the driver which controls the motor +*/ +void StepperMotor::linkDriver(StepperDriver* _driver) { + driver = _driver; +} + +// init hardware pins +void StepperMotor::init() { + if (!driver || !driver->initialized) { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if(voltage_limit > driver->voltage_limit) voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if(voltage_sensor_align > voltage_limit) voltage_sensor_align = voltage_limit; + + // update the controller limits + if(_isset(phase_resistance)){ + // velocity control loop controls current + PID_velocity.limit = current_limit; + }else{ + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + // if using open loop control, set a CW as the default direction if not already set + if ((controller==MotionControlType::angle_openloop + ||controller==MotionControlType::velocity_openloop) + && (sensor_direction == Direction::UNKNOWN)) { + sensor_direction = Direction::CW; + } + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + + motor_status = FOCMotorStatus::motor_uncalibrated; +} + + +// disable motor driver +void StepperMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0); + // disable driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void StepperMotor::enable() +{ + // disable enable + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0); + // motor status update + enabled = 1; +} + + +/** + FOC functions +*/ +// FOC initialization function +int StepperMotor::initFOC() { + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // align motor if necessary + // alignment necessary for encoders! + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if(sensor){ + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = sensor->getAngle(); + } else { SIMPLEFOC_DEBUG("MOT: No sensor."); } + + if(exit_flag){ + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + }else{ + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Encoder alignment to electrical 0 angle +int StepperMotor::alignSensor() { + int exit_flag = 1; //success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // if unknown natural direction + if(!_isset(sensor_direction)){ + // check if sensor needs zero search + if(sensor->needsSearch()) exit_flag = absoluteZeroSearch(); + // stop init if not found index + if(!exit_flag) return exit_flag; + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + if (mid_angle == end_angle) { + SIMPLEFOC_DEBUG("MOT: Failed to notice movement"); + return 0; // failed calibration + } else if (mid_angle < end_angle) { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CCW"); + sensor_direction = Direction::CCW; + } else{ + SIMPLEFOC_DEBUG("MOT: sensor_direction==CW"); + sensor_direction = Direction::CW; + } + // check pole pair number + float moved = fabs(mid_angle - end_angle); + if( fabs(moved*pole_pairs - _2PI) > 0.5f ) { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI/moved); + } else { + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + + } else { + SIMPLEFOC_DEBUG("MOT: Skip dir calib."); + } + + // zero electric angle not known + if(!_isset(zero_electric_angle)){ + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + _delay(20); + if(monitor_port){ + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } else { SIMPLEFOC_DEBUG("MOT: Skip offset calib."); } + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int StepperMotor::absoluteZeroSearch() { + + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while(sensor->needsSearch() && shaft_angle < _2PI){ + angleOpenloop(1.5f*_2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if(monitor_port){ + if(sensor->needsSearch()) SIMPLEFOC_DEBUG("MOT: Error: Not found!"); + else { SIMPLEFOC_DEBUG("MOT: Success!"); } + } + return !sensor->needsSearch(); +} + + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void StepperMotor::loopFOC() { + + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) sensor->update(); + + // if open-loop do nothing + if( controller==MotionControlType::angle_openloop || controller==MotionControlType::velocity_openloop ) return; + // shaft angle + shaft_angle = shaftAngle(); + + // if disabled do nothing + if(!enabled) return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or voltage loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void StepperMotor::move(float new_target) { + + // downsampling (optional) + if(motion_cnt++ < motion_downsample) return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if( controller!=MotionControlType::angle_openloop && controller!=MotionControlType::velocity_openloop ) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if(!enabled) return; + + // set internal target variable + if(_isset(new_target) ) target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) voltage_bemf = shaft_velocity/KV_rating/_RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if(!current_sense && _isset(phase_resistance)) current.q = (voltage.q - voltage_bemf)/phase_resistance; + + // choose control loop + switch (controller) { + case MotionControlType::torque: + if(!_isset(phase_resistance)) voltage.q = target; // if voltage torque control + else voltage.q = target*phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -target*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::angle: + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = feed_forward_velocity + P_angle( shaft_angle_sp - shaft_angle ); + shaft_angle_sp = _constrain(shaft_angle_sp, -velocity_limit, velocity_limit); + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity: + // velocity set point + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + // use voltage if phase-resistance not provided + if(!_isset(phase_resistance)) voltage.q = current_sp; + else voltage.q = _constrain( current_sp*phase_resistance + voltage_bemf , -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if(!_isset(phase_inductance)) voltage.d = 0; + else voltage.d = _constrain( -current_sp*shaft_velocity*pole_pairs*phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + case MotionControlType::angle_openloop: + // angle control in open loop + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + } +} + + +// Method using FOC to set Uq and Ud to the motor at the optimal angle +// Function implementing Sine PWM algorithms +// - space vector not implemented yet +// +// Function using sine approximation +// regular sin + cos ~300us (no memory usaage) +// approx _sin + _cos ~110us (400Byte ~ 20% of memory) +void StepperMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) { + // Sinusoidal PWM modulation + // Inverse Park transformation + float _sa, _ca; + _sincos(angle_el, &_sa, &_ca); + + // Inverse park transform + Ualpha = _ca * Ud - _sa * Uq; // -sin(angle) * Uq; + Ubeta = _sa * Ud + _ca * Uq; // cos(angle) * Uq; + + // set the voltages in hardware + driver->setPwm(Ualpha, Ubeta); +} + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float StepperMotor::velocityOpenloop(float target_velocity){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity*Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float StepperMotor::angleOpenloop(float target_angle){ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + if(abs( target_angle - shaft_angle ) > abs(velocity_limit*Ts)){ + shaft_angle += _sign(target_angle - shaft_angle) * abs( velocity_limit )*Ts; + shaft_velocity = velocity_limit; + }else{ + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if(_isset(phase_resistance)){ + Uq = _constrain(current_limit*phase_resistance + fabs(voltage_bemf),-voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf))/phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle((shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.h new file mode 100644 index 0000000..7eda316 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/StepperMotor.h @@ -0,0 +1,115 @@ +/** + * @file StepperMotor.h + * + */ + +#ifndef StepperMotor_h +#define StepperMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/StepperDriver.h" +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + Stepper Motor class +*/ +class StepperMotor: public FOCMotor +{ + public: + /** + StepperMotor class constructor + @param pp pole pair number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + StepperMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver StepperDriver class implementing all the hardware specific functions necessary PWM setting + */ + void linkDriver(StepperDriver* driver); + + /** + * StepperDriver link: + * - 4PWM - L298N for example + */ + StepperDriver* driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + * + * - If zero_electric_offset parameter is set the alignment procedure is skipped + */ + int initFOC() override; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + /** + * Function executing the control loops set by the controller parameter of the StepperMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ualpha,Ubeta; //!< Phase voltages U alpha and U beta used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + + private: + + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/BLDCDriver.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/BLDCDriver.h new file mode 100644 index 0000000..f405d78 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/BLDCDriver.h @@ -0,0 +1,56 @@ +#ifndef BLDCDRIVER_H +#define BLDCDRIVER_H + +#include "Arduino.h" + + +enum PhaseState : uint8_t { + PHASE_OFF = 0, // both sides of the phase are off + PHASE_ON = 1, // both sides of the phase are driven with PWM, dead time is applied in 6-PWM mode + PHASE_HI = 2, // only the high side of the phase is driven with PWM (6-PWM mode only) + PHASE_LO = 3, // only the low side of the phase is driven with PWM (6-PWM mode only) +}; + + +class BLDCDriver{ + public: + + /** Initialise hardware */ + virtual int init() = 0; + /** Enable hardware */ + virtual void enable() = 0; + /** Disable hardware */ + virtual void disable() = 0; + + long pwm_frequency; //!< pwm frequency value in hertz + float voltage_power_supply; //!< power supply voltage + float voltage_limit; //!< limiting voltage set to the motor + + + float dc_a; //!< currently set duty cycle on phaseA + float dc_b; //!< currently set duty cycle on phaseB + float dc_c; //!< currently set duty cycle on phaseC + + bool initialized = false; // true if driver was successfully initialized + void* params = 0; // pointer to hardware specific parameters of driver + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + virtual void setPwm(float Ua, float Ub, float Uc) = 0; + + /** + * Set phase state, enable/disable + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) = 0; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.cpp new file mode 100644 index 0000000..609162c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.cpp @@ -0,0 +1,99 @@ +#include "CurrentSense.h" + + +// get current magnitude +// - absolute - if no electrical_angle provided +// - signed - if angle provided +float CurrentSense::getDCCurrent(float motor_electrical_angle){ + // read current phase currents + PhaseCurrent_s current = getPhaseCurrents(); + // currnet sign - if motor angle not provided the magnitude is always positive + float sign = 1; + + // calculate clarke transform + float i_alpha, i_beta; + if(!current.c){ + // if only two measured currents + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * current.b; + }if(!current.a){ + // if only two measured currents + float a = -current.c - current.b; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * current.b; + }if(!current.b){ + // if only two measured currents + float b = -current.a - current.c; + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * b; + }else{ + // signal filtering using identity a + b + c = 0. Assumes measurement error is normally distributed. + float mid = (1.f/3) * (current.a + current.b + current.c); + float a = current.a - mid; + float b = current.b - mid; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * b; + } + + // if motor angle provided function returns signed value of the current + // determine the sign of the current + // sign(atan2(current.q, current.d)) is the same as c.q > 0 ? 1 : -1 + if(motor_electrical_angle) { + float ct; + float st; + _sincos(motor_electrical_angle, &st, &ct); + sign = (i_beta*ct - i_alpha*st) > 0 ? 1 : -1; + } + // return current magnitude + return sign*_sqrt(i_alpha*i_alpha + i_beta*i_beta); +} + +// function used with the foc algorihtm +// calculating DQ currents from phase currents +// - function calculating park and clarke transform of the phase currents +// - using getPhaseCurrents internally +DQCurrent_s CurrentSense::getFOCCurrents(float angle_el){ + // read current phase currents + PhaseCurrent_s current = getPhaseCurrents(); + + // calculate clarke transform + float i_alpha, i_beta; + if(!current.c){ + // if only two measured currents + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * current.b; + }if(!current.a){ + // if only two measured currents + float a = -current.c - current.b; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * current.b; + }if(!current.b){ + // if only two measured currents + float b = -current.a - current.c; + i_alpha = current.a; + i_beta = _1_SQRT3 * current.a + _2_SQRT3 * b; + } else { + // signal filtering using identity a + b + c = 0. Assumes measurement error is normally distributed. + float mid = (1.f/3) * (current.a + current.b + current.c); + float a = current.a - mid; + float b = current.b - mid; + i_alpha = a; + i_beta = _1_SQRT3 * a + _2_SQRT3 * b; + } + + // calculate park transform + float ct; + float st; + _sincos(angle_el, &st, &ct); + DQCurrent_s return_current; + return_current.d = i_alpha * ct + i_beta * st; + return_current.q = i_beta * ct - i_alpha * st; + return return_current; +} + +/** + Driver linking to the current sense +*/ +void CurrentSense::linkDriver(BLDCDriver* _driver) { + driver = _driver; +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.h new file mode 100644 index 0000000..ad9f926 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/CurrentSense.h @@ -0,0 +1,75 @@ +#ifndef CURRENTSENSE_H +#define CURRENTSENSE_H + +#include "BLDCDriver.h" +#include "../foc_utils.h" + +/** + * Current sensing abstract class defintion + * Each current sensoring implementation needs to extend this interface + */ +class CurrentSense{ + public: + + /** + * Function intialising the CurrentSense class + * - All the necessary intialisations of adc and sync should be implemented here + * + * @returns - 0 - for failure & 1 - for success + */ + virtual int init() = 0; + + /** + * Linking the current sense with the motor driver + * Only necessary if synchronisation in between the two is required + */ + void linkDriver(BLDCDriver *driver); + + // variables + bool skip_align = false; //!< variable signaling that the phase current direction should be verified during initFOC() + + BLDCDriver* driver = nullptr; //!< driver link + bool initialized = false; // true if current sense was successfully initialized + void* params = 0; //!< pointer to hardware specific parameters of current sensing + + /** + * Function intended to verify if: + * - phase current are oriented properly + * - if their order is the same as driver phases + * + * This function corrects the alignment errors if possible ans if no such thing is needed it can be left empty (return 1) + * @returns - 0 - for failure & positive number (with status) - for success + */ + virtual int driverAlign(float align_voltage) = 0; + + /** + * Function rading the phase currents a, b and c + * This function will be used with the foc control throught the function + * CurrentSense::getFOCCurrents(electrical_angle) + * - it returns current c equal to 0 if only two phase measurements available + * + * @return PhaseCurrent_s current values + */ + virtual PhaseCurrent_s getPhaseCurrents() = 0; + /** + * Function reading the magnitude of the current set to the motor + * It returns the abosolute or signed magnitude if possible + * It can receive the motor electrical angle to help with calculation + * This function is used with the current control (not foc) + * + * @param angle_el - electrical angle of the motor (optional) + */ + virtual float getDCCurrent(float angle_el = 0); + + /** + * Function used for FOC contorl, it reads the DQ currents of the motor + * It uses the function getPhaseCurrents internally + * + * @param angle_el - motor electrical angle + */ + DQCurrent_s getFOCCurrents(float angle_el); + + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.cpp new file mode 100644 index 0000000..d1427bc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.cpp @@ -0,0 +1,155 @@ +#include "FOCMotor.h" +#include "../../communication/SimpleFOCDebug.h" + +/** + * Default constructor - setting all variabels to default values + */ +FOCMotor::FOCMotor() +{ + // maximum angular velocity to be used for positioning + velocity_limit = DEF_VEL_LIM; + // maximum voltage to be set to the motor + voltage_limit = DEF_POWER_SUPPLY; + // not set on the begining + current_limit = DEF_CURRENT_LIM; + + // index search velocity + velocity_index_search = DEF_INDEX_SEARCH_TARGET_VELOCITY; + // sensor and motor align voltage + voltage_sensor_align = DEF_VOLTAGE_SENSOR_ALIGN; + + // default modulation is SinePWM + foc_modulation = FOCModulationType::SinePWM; + + // default target value + target = 0; + voltage.d = 0; + voltage.q = 0; + // current target values + current_sp = 0; + current.q = 0; + current.d = 0; + + // voltage bemf + voltage_bemf = 0; + + //monitor_port + monitor_port = nullptr; + //sensor + sensor_offset = 0.0f; + sensor = nullptr; + //current sensor + current_sense = nullptr; +} + + +/** + Sensor linking method +*/ +void FOCMotor::linkSensor(Sensor* _sensor) { + sensor = _sensor; +} + +/** + CurrentSense linking method +*/ +void FOCMotor::linkCurrentSense(CurrentSense* _current_sense) { + current_sense = _current_sense; +} + +// shaft angle calculation +float FOCMotor::shaftAngle() { + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return shaft_angle; + return sensor_direction*LPF_angle(sensor->getAngle()) - sensor_offset; +} +// shaft velocity calculation +float FOCMotor::shaftVelocity() { + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return shaft_velocity; + return sensor_direction*LPF_velocity(sensor->getVelocity()); +} + +float FOCMotor::electricalAngle(){ + // if no sensor linked return previous value ( for open loop ) + if(!sensor) return electrical_angle; + return _normalizeAngle( (float)(sensor_direction * pole_pairs) * sensor->getMechanicalAngle() - zero_electric_angle ); +} + +/** + * Monitoring functions + */ +// function implementing the monitor_port setter +void FOCMotor::useMonitoring(Print &print){ + monitor_port = &print; //operate on the address of print + #ifndef SIMPLEFOC_DISABLE_DEBUG + SimpleFOCDebug::enable(&print); + SIMPLEFOC_DEBUG("MOT: Monitor enabled!"); + #endif +} + +// utility function intended to be used with serial plotter to monitor motor variables +// significantly slowing the execution down!!!! +void FOCMotor::monitor() { + if( !monitor_downsample || monitor_cnt++ < monitor_downsample ) return; + monitor_cnt = 0; + if(!monitor_port) return; + bool printed = 0; + + if(monitor_variables & _MON_TARGET){ + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + monitor_port->print(target,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_VOLT_Q) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(voltage.q,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_VOLT_D) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(voltage.d,monitor_decimals); + printed= true; + } + // read currents if possible - even in voltage mode (if current_sense available) + if(monitor_variables & _MON_CURR_Q || monitor_variables & _MON_CURR_D) { + DQCurrent_s c = current; + if( current_sense && torque_controller != TorqueControlType::foc_current ){ + c = current_sense->getFOCCurrents(electrical_angle); + c.q = LPF_current_q(c.q); + c.d = LPF_current_d(c.d); + } + if(monitor_variables & _MON_CURR_Q) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(c.q*1000, monitor_decimals); // mAmps + printed= true; + } + if(monitor_variables & _MON_CURR_D) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(c.d*1000, monitor_decimals); // mAmps + printed= true; + } + } + + if(monitor_variables & _MON_VEL) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(shaft_velocity,monitor_decimals); + printed= true; + } + if(monitor_variables & _MON_ANGLE) { + if(!printed && monitor_start_char) monitor_port->print(monitor_start_char); + else if(printed) monitor_port->print(monitor_separator); + monitor_port->print(shaft_angle,monitor_decimals); + printed= true; + } + if(printed){ + if(monitor_end_char) monitor_port->println(monitor_end_char); + else monitor_port->println(""); + } +} + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.h new file mode 100644 index 0000000..318be99 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/FOCMotor.h @@ -0,0 +1,251 @@ +#ifndef FOCMOTOR_H +#define FOCMOTOR_H + +#include "Arduino.h" +#include "Sensor.h" +#include "CurrentSense.h" + +#include "../time_utils.h" +#include "../foc_utils.h" +#include "../defaults.h" +#include "../pid.h" +#include "../lowpass_filter.h" + + +// monitoring bitmap +#define _MON_TARGET 0b1000000 // monitor target value +#define _MON_VOLT_Q 0b0100000 // monitor voltage q value +#define _MON_VOLT_D 0b0010000 // monitor voltage d value +#define _MON_CURR_Q 0b0001000 // monitor current q value - if measured +#define _MON_CURR_D 0b0000100 // monitor current d value - if measured +#define _MON_VEL 0b0000010 // monitor velocity value +#define _MON_ANGLE 0b0000001 // monitor angle value + +/** + * Motiron control type + */ +enum MotionControlType : uint8_t { + torque = 0x00, //!< Torque control + velocity = 0x01, //!< Velocity motion control + angle = 0x02, //!< Position/angle motion control + velocity_openloop = 0x03, + angle_openloop = 0x04 +}; + +/** + * Motiron control type + */ +enum TorqueControlType : uint8_t { + voltage = 0x00, //!< Torque control using voltage + dc_current = 0x01, //!< Torque control using DC current (one current magnitude) + foc_current = 0x02, //!< torque control using dq currents +}; + +/** + * FOC modulation type + */ +enum FOCModulationType : uint8_t { + SinePWM = 0x00, //!< Sinusoidal PWM modulation + SpaceVectorPWM = 0x01, //!< Space vector modulation method + Trapezoid_120 = 0x02, + Trapezoid_150 = 0x03, +}; + + + +enum FOCMotorStatus : uint8_t { + motor_uninitialized = 0x00, //!< Motor is not yet initialized + motor_initializing = 0x01, //!< Motor intiialization is in progress + motor_uncalibrated = 0x02, //!< Motor is initialized, but not calibrated (open loop possible) + motor_calibrating = 0x03, //!< Motor calibration in progress + motor_ready = 0x04, //!< Motor is initialized and calibrated (closed loop possible) + motor_error = 0x08, //!< Motor is in error state (recoverable, e.g. overcurrent protection active) + motor_calib_failed = 0x0E, //!< Motor calibration failed (possibly recoverable) + motor_init_failed = 0x0F, //!< Motor initialization failed (not recoverable) +}; + + + +/** + Generic motor class +*/ +class FOCMotor +{ + public: + /** + * Default constructor - setting all variabels to default values + */ + FOCMotor(); + + /** Motor hardware init function */ + virtual void init()=0; + /** Motor disable function */ + virtual void disable()=0; + /** Motor enable function */ + virtual void enable()=0; + + /** + * Function linking a motor and a sensor + * + * @param sensor Sensor class wrapper for the FOC algorihtm to read the motor angle and velocity + */ + void linkSensor(Sensor* sensor); + + /** + * Function linking a motor and current sensing + * + * @param current_sense CurrentSense class wrapper for the FOC algorihtm to read the motor current measurements + */ + void linkCurrentSense(CurrentSense* current_sense); + + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + * + * - If zero_electric_offset parameter is set the alignment procedure is skipped + */ + virtual int initFOC()=0; + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + virtual void loopFOC()=0; + /** + * Function executing the control loops set by the controller parameter of the BLDCMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + virtual void move(float target = NOT_SET)=0; + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + virtual void setPhaseVoltage(float Uq, float Ud, float angle_el)=0; + + // State calculation methods + /** Shaft angle calculation in radians [rad] */ + float shaftAngle(); + /** + * Shaft angle calculation function in radian per second [rad/s] + * It implements low pass filtering + */ + float shaftVelocity(); + + + + /** + * Electrical angle calculation + */ + float electricalAngle(); + + // state variables + float target; //!< current target value - depends of the controller + float feed_forward_velocity = 0.0f; //!< current feed forward velocity + float shaft_angle;//!< current motor angle + float electrical_angle;//!< current electrical angle + float shaft_velocity;//!< current motor velocity + float current_sp;//!< target current ( q current ) + float shaft_velocity_sp;//!< current target velocity + float shaft_angle_sp;//!< current target angle + DQVoltage_s voltage;//!< current d and q voltage set to the motor + DQCurrent_s current;//!< current d and q current measured + float voltage_bemf; //!< estimated backemf voltage (if provided KV constant) + + // motor configuration parameters + float voltage_sensor_align;//!< sensor and motor align voltage parameter + float velocity_index_search;//!< target velocity for index search + + // motor physical parameters + float phase_resistance; //!< motor phase resistance + int pole_pairs;//!< motor pole pairs number + float KV_rating; //!< motor KV rating + float phase_inductance; //!< motor phase inductance + + // limiting variables + float voltage_limit; //!< Voltage limiting variable - global limit + float current_limit; //!< Current limiting variable - global limit + float velocity_limit; //!< Velocity limiting variable - global limit + + // motor status vairables + int8_t enabled = 0;//!< enabled or disabled motor flag + FOCMotorStatus motor_status = FOCMotorStatus::motor_uninitialized; //!< motor status + + // pwm modulation related variables + FOCModulationType foc_modulation;//!< parameter determining modulation algorithm + int8_t modulation_centered = 1;//!< flag (1) centered modulation around driver limit /2 or (0) pulled to 0 + + + // configuration structures + TorqueControlType torque_controller; //!< parameter determining the torque control type + MotionControlType controller; //!< parameter determining the control loop to be used + + // controllers and low pass filters + PIDController PID_current_q{DEF_PID_CURR_P,DEF_PID_CURR_I,DEF_PID_CURR_D,DEF_PID_CURR_RAMP, DEF_POWER_SUPPLY};//!< parameter determining the q current PID config + PIDController PID_current_d{DEF_PID_CURR_P,DEF_PID_CURR_I,DEF_PID_CURR_D,DEF_PID_CURR_RAMP, DEF_POWER_SUPPLY};//!< parameter determining the d current PID config + LowPassFilter LPF_current_q{DEF_CURR_FILTER_Tf};//!< parameter determining the current Low pass filter configuration + LowPassFilter LPF_current_d{DEF_CURR_FILTER_Tf};//!< parameter determining the current Low pass filter configuration + PIDController PID_velocity{DEF_PID_VEL_P,DEF_PID_VEL_I,DEF_PID_VEL_D,DEF_PID_VEL_RAMP,DEF_PID_VEL_LIMIT};//!< parameter determining the velocity PID configuration + PIDController P_angle{DEF_P_ANGLE_P,0,0,0,DEF_VEL_LIM}; //!< parameter determining the position PID configuration + LowPassFilter LPF_velocity{DEF_VEL_FILTER_Tf};//!< parameter determining the velocity Low pass filter configuration + LowPassFilter LPF_angle{0.0};//!< parameter determining the angle low pass filter configuration + unsigned int motion_downsample = DEF_MOTION_DOWNSMAPLE; //!< parameter defining the ratio of downsampling for move commad + unsigned int motion_cnt = 0; //!< counting variable for downsampling for move commad + + // sensor related variabels + float sensor_offset; //!< user defined sensor zero offset + float zero_electric_angle = NOT_SET;//!< absolute zero electric angle - if available + Direction sensor_direction = Direction::UNKNOWN; //!< default is CW. if sensor_direction == Direction::CCW then direction will be flipped compared to CW. Set to UNKNOWN to set by calibration + + /** + * Function providing BLDCMotor class with the + * Serial interface and enabling monitoring mode + * + * @param serial Monitoring Serial class reference + */ + void useMonitoring(Print &serial); + + /** + * Utility function intended to be used with serial plotter to monitor motor variables + * significantly slowing the execution down!!!! + */ + void monitor(); + unsigned int monitor_downsample = DEF_MON_DOWNSMAPLE; //!< show monitor outputs each monitor_downsample calls + char monitor_start_char = '\0'; //!< monitor starting character + char monitor_end_char = '\0'; //!< monitor outputs ending character + char monitor_separator = '\t'; //!< monitor outputs separation character + unsigned int monitor_decimals = 4; //!< monitor outputs decimal places + // initial monitoring will display target, voltage, velocity and angle + uint8_t monitor_variables = _MON_TARGET | _MON_VOLT_Q | _MON_VEL | _MON_ANGLE; //!< Bit array holding the map of variables the user wants to monitor + + /** + * Sensor link: + * - Encoder + * - MagneticSensor* + * - HallSensor + */ + Sensor* sensor; + /** + * CurrentSense link + */ + CurrentSense* current_sense; + + // monitoring functions + Print* monitor_port; //!< Serial terminal variable if provided + private: + // monitor counting variable + unsigned int monitor_cnt = 0 ; //!< counting variable +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.cpp new file mode 100644 index 0000000..a80fa43 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.cpp @@ -0,0 +1,78 @@ +#include "Sensor.h" +#include "../foc_utils.h" +#include "../time_utils.h" + + + +void Sensor::update() { + float val = getSensorAngle(); + angle_prev_ts = _micros(); + float d_angle = val - angle_prev; + // if overflow happened track it as full rotation + if(abs(d_angle) > (0.8f*_2PI) ) full_rotations += ( d_angle > 0 ) ? -1 : 1; + angle_prev = val; +} + + + /** get current angular velocity (rad/s) */ +float Sensor::getVelocity() { + // calculate sample time + float Ts = (angle_prev_ts - vel_angle_prev_ts)*1e-6; + if (Ts < 0.0f) { // handle micros() overflow - we need to reset vel_angle_prev_ts + vel_angle_prev = angle_prev; + vel_full_rotations = full_rotations; + vel_angle_prev_ts = angle_prev_ts; + return velocity; + } + if (Ts < min_elapsed_time) return velocity; // don't update velocity if deltaT is too small + + velocity = ( (float)(full_rotations - vel_full_rotations)*_2PI + (angle_prev - vel_angle_prev) ) / Ts; + vel_angle_prev = angle_prev; + vel_full_rotations = full_rotations; + vel_angle_prev_ts = angle_prev_ts; + return velocity; +} + + + +void Sensor::init() { + // initialize all the internal variables of Sensor to ensure a "smooth" startup (without a 'jump' from zero) + getSensorAngle(); // call once + delayMicroseconds(1); + vel_angle_prev = getSensorAngle(); // call again + vel_angle_prev_ts = _micros(); + delay(1); + getSensorAngle(); // call once + delayMicroseconds(1); + angle_prev = getSensorAngle(); // call again + angle_prev_ts = _micros(); +} + + +float Sensor::getMechanicalAngle() { + return angle_prev; +} + + + +float Sensor::getAngle(){ + return (float)full_rotations * _2PI + angle_prev; +} + + + +double Sensor::getPreciseAngle() { + return (double)full_rotations * (double)_2PI + (double)angle_prev; +} + + + +int32_t Sensor::getFullRotations() { + return full_rotations; +} + + + +int Sensor::needsSearch() { + return 0; // default false +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.h new file mode 100644 index 0000000..c77eb91 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/Sensor.h @@ -0,0 +1,139 @@ +#ifndef SENSOR_H +#define SENSOR_H + +#include + +/** + * Direction structure + */ +enum Direction : int8_t { + CW = 1, // clockwise + CCW = -1, // counter clockwise + UNKNOWN = 0 // not yet known or invalid state +}; + + +/** + * Pullup configuration structure + */ +enum Pullup : uint8_t { + USE_INTERN = 0x00, //!< Use internal pullups + USE_EXTERN = 0x01 //!< Use external pullups +}; + +/** + * Sensor abstract class defintion + * + * This class is purposefully kept simple, as a base for all kinds of sensors. Currently we have + * Encoders, Magnetic Encoders and Hall Sensor implementations. This base class extracts the + * most basic common features so that a FOC driver can obtain the data it needs for operation. + * + * To implement your own sensors, create a sub-class of this class, and implement the getSensorAngle() + * method. getSensorAngle() returns a float value, in radians, representing the current shaft angle in the + * range 0 to 2*PI (one full turn). + * + * To function correctly, the sensor class update() method has to be called sufficiently quickly. Normally, + * the BLDCMotor's loopFOC() function calls it once per iteration, so you must ensure to call loopFOC() quickly + * enough, both for correct motor and sensor operation. + * + * The Sensor base class provides an implementation of getVelocity(), and takes care of counting full + * revolutions in a precise way, but if you wish you can additionally override these methods to provide more + * optimal implementations for your hardware. + * + */ +class Sensor{ + friend class SmoothingSensor; + public: + /** + * Get mechanical shaft angle in the range 0 to 2PI. This value will be as precise as possible with + * the hardware. Base implementation uses the values returned by update() so that + * the same values are returned until update() is called again. + */ + virtual float getMechanicalAngle(); + + /** + * Get current position (in rad) including full rotations and shaft angle. + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + * Note that this value has limited precision as the number of rotations increases, + * because the limited precision of float can't capture the large angle of the full + * rotations and the small angle of the shaft angle at the same time. + */ + virtual float getAngle(); + + /** + * On architectures supporting it, this will return a double precision position value, + * which should have improved precision for large position values. + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + */ + virtual double getPreciseAngle(); + + /** + * Get current angular velocity (rad/s) + * Can be overridden in subclasses. Base implementation uses the values + * returned by update() so that it only makes sense to call this if update() + * has been called in the meantime. + */ + virtual float getVelocity(); + + /** + * Get the number of full rotations + * Base implementation uses the values returned by update() so that the same + * values are returned until update() is called again. + */ + virtual int32_t getFullRotations(); + + /** + * Updates the sensor values by reading the hardware sensor. + * Some implementations may work with interrupts, and not need this. + * The base implementation calls getSensorAngle(), and updates internal + * fields for angle, timestamp and full rotations. + * This method must be called frequently enough to guarantee that full + * rotations are not "missed" due to infrequent polling. + * Override in subclasses if alternative behaviours are required for your + * sensor hardware. + */ + virtual void update(); + + /** + * returns 0 if it does need search for absolute zero + * 0 - magnetic sensor (& encoder with index which is found) + * 1 - ecoder with index (with index not found yet) + */ + virtual int needsSearch(); + + /** + * Minimum time between updates to velocity. If time elapsed is lower than this, the velocity is not updated. + */ + float min_elapsed_time = 0.000100; // default is 100 microseconds, or 10kHz + + protected: + /** + * Get current shaft angle from the sensor hardware, and + * return it as a float in radians, in the range 0 to 2PI. + * + * This method is pure virtual and must be implemented in subclasses. + * Calling this method directly does not update the base-class internal fields. + * Use update() when calling from outside code. + */ + virtual float getSensorAngle()=0; + /** + * Call Sensor::init() from your sensor subclass's init method if you want smoother startup + * The base class init() method calls getSensorAngle() several times to initialize the internal fields + * to current values, ensuring there is no discontinuity ("jump from zero") during the first calls + * to sensor.getAngle() and sensor.getVelocity() + */ + virtual void init(); + + // velocity calculation variables + float velocity=0.0f; + float angle_prev=0.0f; // result of last call to getSensorAngle(), used for full rotations and velocity + long angle_prev_ts=0; // timestamp of last call to getAngle, used for velocity + float vel_angle_prev=0.0f; // angle at last call to getVelocity, used for velocity + long vel_angle_prev_ts=0; // last velocity calculation timestamp + int32_t full_rotations=0; // full rotation tracking + int32_t vel_full_rotations=0; // previous full rotation value for velocity calculation +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/StepperDriver.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/StepperDriver.h new file mode 100644 index 0000000..2006fc8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/base_classes/StepperDriver.h @@ -0,0 +1,32 @@ +#ifndef STEPPERDRIVER_H +#define STEPPERDRIVER_H + +#include "drivers/hardware_api.h" + +class StepperDriver{ + public: + + /** Initialise hardware */ + virtual int init() = 0; + /** Enable hardware */ + virtual void enable() = 0; + /** Disable hardware */ + virtual void disable() = 0; + + long pwm_frequency; //!< pwm frequency value in hertz + float voltage_power_supply; //!< power supply voltage + float voltage_limit; //!< limiting voltage set to the motor + + bool initialized = false; // true if driver was successfully initialized + void* params = 0; // pointer to hardware specific parameters of driver + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + virtual void setPwm(float Ua, float Ub) = 0; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/defaults.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/defaults.h new file mode 100644 index 0000000..c0a4618 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/defaults.h @@ -0,0 +1,49 @@ +// default configuration values +// change this file to optimal values for your application + +#define DEF_POWER_SUPPLY 12.0f //!< default power supply voltage +// velocity PI controller params +#define DEF_PID_VEL_P 0.5f //!< default PID controller P value +#define DEF_PID_VEL_I 10.0f //!< default PID controller I value +#define DEF_PID_VEL_D 0.0f //!< default PID controller D value +#define DEF_PID_VEL_RAMP 1000.0f //!< default PID controller voltage ramp value +#define DEF_PID_VEL_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit + +// current sensing PID values +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) +// for 16Mhz controllers like Arduino uno and mega +#define DEF_PID_CURR_P 2 //!< default PID controller P value +#define DEF_PID_CURR_I 100 //!< default PID controller I value +#define DEF_PID_CURR_D 0.0f //!< default PID controller D value +#define DEF_PID_CURR_RAMP 1000.0f //!< default PID controller voltage ramp value +#define DEF_PID_CURR_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit +#define DEF_CURR_FILTER_Tf 0.01f //!< default velocity filter time constant +#else +// for stm32, due, teensy, esp32 and similar +#define DEF_PID_CURR_P 3 //!< default PID controller P value +#define DEF_PID_CURR_I 300.0f //!< default PID controller I value +#define DEF_PID_CURR_D 0.0f //!< default PID controller D value +#define DEF_PID_CURR_RAMP 0 //!< default PID controller voltage ramp value +#define DEF_PID_CURR_LIMIT (DEF_POWER_SUPPLY) //!< default PID controller voltage limit +#define DEF_CURR_FILTER_Tf 0.005f //!< default currnet filter time constant +#endif +// default current limit values +#define DEF_CURRENT_LIM 2.0f //!< 2Amps current limit by default + +// default monitor downsample +#define DEF_MON_DOWNSMAPLE 100 //!< default monitor downsample +#define DEF_MOTION_DOWNSMAPLE 0 //!< default motion downsample - disable + +// angle P params +#define DEF_P_ANGLE_P 20.0f //!< default P controller P value +#define DEF_VEL_LIM 20.0f //!< angle velocity limit default + +// index search +#define DEF_INDEX_SEARCH_TARGET_VELOCITY 1.0f //!< default index search velocity +// align voltage +#define DEF_VOLTAGE_SENSOR_ALIGN 3.0f //!< default voltage for sensor and motor zero alignemt +// low pass filter velocity +#define DEF_VEL_FILTER_Tf 0.005 //!< default velocity filter time constant + +// current sense default parameters +#define DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf 0.0f //!< default currnet sense per phase low pass filter time constant diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.cpp new file mode 100644 index 0000000..4cb0986 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.cpp @@ -0,0 +1,73 @@ +#include "foc_utils.h" + + +// function approximating the sine calculation by using fixed size array +// uses a 65 element lookup table and interpolation +// thanks to @dekutree for his work on optimizing this +__attribute__((weak)) float _sin(float a){ + // 16bit integer array for sine lookup. interpolation is used for better precision + // 16 bit precision on sine value, 8 bit fractional value for interpolation, 6bit LUT size + // resulting precision compared to stdlib sine is 0.00006480 (RMS difference in range -PI,PI for 3217 steps) + static uint16_t sine_array[65] = {0,804,1608,2411,3212,4011,4808,5602,6393,7180,7962,8740,9512,10279,11039,11793,12540,13279,14010,14733,15447,16151,16846,17531,18205,18868,19520,20160,20788,21403,22006,22595,23170,23732,24279,24812,25330,25833,26320,26791,27246,27684,28106,28511,28899,29269,29622,29957,30274,30572,30853,31114,31357,31581,31786,31972,32138,32286,32413,32522,32610,32679,32729,32758,32768}; + unsigned int i = (unsigned int)(a * (64*4*256.0 /_2PI)); + int t1, t2, frac = i & 0xff; + i = (i >> 8) & 0xff; + if (i < 64) { + t1 = sine_array[i]; t2 = sine_array[i+1]; + } + else if(i < 128) { + t1 = sine_array[128 - i]; t2 = sine_array[127 - i]; + } + else if(i < 192) { + t1 = -sine_array[-128 + i]; t2 = -sine_array[-127 + i]; + } + else { + t1 = -sine_array[256 - i]; t2 = -sine_array[255 - i]; + } + return (1.0f/32768.0f) * (t1 + (((t2 - t1) * frac) >> 8)); +} + +// function approximating cosine calculation by using fixed size array +// ~55us (float array) +// ~56us (int array) +// precision +-0.005 +// it has to receive an angle in between 0 and 2PI +__attribute__((weak)) float _cos(float a){ + float a_sin = a + _PI_2; + a_sin = a_sin > _2PI ? a_sin - _2PI : a_sin; + return _sin(a_sin); +} + + +__attribute__((weak)) void _sincos(float a, float* s, float* c){ + *s = _sin(a); + *c = _cos(a); +} + + +// normalizing radian angle to [0,2PI] +__attribute__((weak)) float _normalizeAngle(float angle){ + float a = fmod(angle, _2PI); + return a >= 0 ? a : (a + _2PI); +} + +// Electrical angle calculation +float _electricalAngle(float shaft_angle, int pole_pairs) { + return (shaft_angle * pole_pairs); +} + +// square root approximation function using +// https://reprap.org/forum/read.php?147,219210 +// https://en.wikipedia.org/wiki/Fast_inverse_square_root +__attribute__((weak)) float _sqrtApprox(float number) {//low in fat + // float x; + // const float f = 1.5F; // better precision + + // x = number * 0.5F; + float y = number; + long i = * ( long * ) &y; + i = 0x5f375a86 - ( i >> 1 ); + y = * ( float * ) &i; + // y = y * ( f - ( x * y * y ) ); // better precision + return number * y; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.h new file mode 100644 index 0000000..0efe3b5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/foc_utils.h @@ -0,0 +1,106 @@ +#ifndef FOCUTILS_LIB_H +#define FOCUTILS_LIB_H + +#include "Arduino.h" + +// sign function +#define _sign(a) ( ( (a) < 0 ) ? -1 : ( (a) > 0 ) ) +#ifndef _round +#define _round(x) ((x)>=0?(long)((x)+0.5f):(long)((x)-0.5f)) +#endif +#define _constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt))) +#define _sqrt(a) (_sqrtApprox(a)) +#define _isset(a) ( (a) != (NOT_SET) ) +#define _UNUSED(v) (void) (v) +#define _powtwo(x) (1 << (x)) + +// utility defines +#define _2_SQRT3 1.15470053838f +#define _SQRT3 1.73205080757f +#define _1_SQRT3 0.57735026919f +#define _SQRT3_2 0.86602540378f +#define _SQRT2 1.41421356237f +#define _120_D2R 2.09439510239f +#define _PI 3.14159265359f +#define _PI_2 1.57079632679f +#define _PI_3 1.0471975512f +#define _2PI 6.28318530718f +#define _3PI_2 4.71238898038f +#define _PI_6 0.52359877559f +#define _RPM_TO_RADS 0.10471975512f + +#define NOT_SET -12345.0f +#define _HIGH_IMPEDANCE 0 +#define _HIGH_Z _HIGH_IMPEDANCE +#define _ACTIVE 1 +#define _NC (NOT_SET) + +#define MIN_ANGLE_DETECT_MOVEMENT (_2PI/101.0f) + +// dq current structure +struct DQCurrent_s +{ + float d; + float q; +}; +// phase current structure +struct PhaseCurrent_s +{ + float a; + float b; + float c; +}; +// dq voltage structs +struct DQVoltage_s +{ + float d; + float q; +}; + + +/** + * Function approximating the sine calculation by using fixed size array + * - execution time ~40us (Arduino UNO) + * + * @param a angle in between 0 and 2PI + */ +float _sin(float a); +/** + * Function approximating cosine calculation by using fixed size array + * - execution time ~50us (Arduino UNO) + * + * @param a angle in between 0 and 2PI + */ +float _cos(float a); +/** + * Function returning both sine and cosine of the angle in one call. + * Internally it uses the _sin and _cos functions, but you may wish to + * provide your own implementation which is more optimized. + */ +void _sincos(float a, float* s, float* c); + + +/** + * normalizing radian angle to [0,2PI] + * @param angle - angle to be normalized + */ +float _normalizeAngle(float angle); + + +/** + * Electrical angle calculation + * + * @param shaft_angle - shaft angle of the motor + * @param pole_pairs - number of pole pairs + */ +float _electricalAngle(float shaft_angle, int pole_pairs); + +/** + * Function approximating square root function + * - using fast inverse square root + * + * @param value - number + */ +float _sqrtApprox(float value); + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.cpp new file mode 100644 index 0000000..ffb15cf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.cpp @@ -0,0 +1,28 @@ +#include "lowpass_filter.h" + +LowPassFilter::LowPassFilter(float time_constant) + : Tf(time_constant) + , y_prev(0.0f) +{ + timestamp_prev = _micros(); +} + + +float LowPassFilter::operator() (float x) +{ + unsigned long timestamp = _micros(); + float dt = (timestamp - timestamp_prev)*1e-6f; + + if (dt < 0.0f ) dt = 1e-3f; + else if(dt > 0.3f) { + y_prev = x; + timestamp_prev = timestamp; + return x; + } + + float alpha = Tf/(Tf + dt); + float y = alpha*y_prev + (1.0f - alpha)*x; + y_prev = y; + timestamp_prev = timestamp; + return y; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.h new file mode 100644 index 0000000..1f24e80 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/lowpass_filter.h @@ -0,0 +1,28 @@ +#ifndef LOWPASS_FILTER_H +#define LOWPASS_FILTER_H + + +#include "time_utils.h" +#include "foc_utils.h" + +/** + * Low pass filter class + */ +class LowPassFilter +{ +public: + /** + * @param Tf - Low pass filter time constant + */ + LowPassFilter(float Tf); + ~LowPassFilter() = default; + + float operator() (float x); + float Tf; //!< Low pass filter time constant + +protected: + unsigned long timestamp_prev; //!< Last execution timestamp + float y_prev; //!< filtered value in previous execution step +}; + +#endif // LOWPASS_FILTER_H \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.cpp new file mode 100644 index 0000000..da1bee1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.cpp @@ -0,0 +1,64 @@ +#include "pid.h" + +PIDController::PIDController(float P, float I, float D, float ramp, float limit) + : P(P) + , I(I) + , D(D) + , output_ramp(ramp) // output derivative limit [volts/second] + , limit(limit) // output supply limit [volts] + , error_prev(0.0f) + , output_prev(0.0f) + , integral_prev(0.0f) +{ + timestamp_prev = _micros(); +} + +// PID controller function +float PIDController::operator() (float error){ + // calculate the time from the last call + unsigned long timestamp_now = _micros(); + float Ts = (timestamp_now - timestamp_prev) * 1e-6f; + // quick fix for strange cases (micros overflow) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // u(s) = (P + I/s + Ds)e(s) + // Discrete implementations + // proportional part + // u_p = P *e(k) + float proportional = P * error; + // Tustin transform of the integral part + // u_ik = u_ik_1 + I*Ts/2*(ek + ek_1) + float integral = integral_prev + I*Ts*0.5f*(error + error_prev); + // antiwindup - limit the output + integral = _constrain(integral, -limit, limit); + // Discrete derivation + // u_dk = D(ek - ek_1)/Ts + float derivative = D*(error - error_prev)/Ts; + + // sum all the components + float output = proportional + integral + derivative; + // antiwindup - limit the output variable + output = _constrain(output, -limit, limit); + + // if output ramp defined + if(output_ramp > 0){ + // limit the acceleration by ramping the output + float output_rate = (output - output_prev)/Ts; + if (output_rate > output_ramp) + output = output_prev + output_ramp*Ts; + else if (output_rate < -output_ramp) + output = output_prev - output_ramp*Ts; + } + // saving for the next pass + integral_prev = integral; + output_prev = output; + error_prev = error; + timestamp_prev = timestamp_now; + return output; +} + +void PIDController::reset(){ + integral_prev = 0.0f; + output_prev = 0.0f; + error_prev = 0.0f; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.h new file mode 100644 index 0000000..acd68d6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/pid.h @@ -0,0 +1,41 @@ +#ifndef PID_H +#define PID_H + + +#include "time_utils.h" +#include "foc_utils.h" + +/** + * PID controller class + */ +class PIDController +{ +public: + /** + * + * @param P - Proportional gain + * @param I - Integral gain + * @param D - Derivative gain + * @param ramp - Maximum speed of change of the output value + * @param limit - Maximum output value + */ + PIDController(float P, float I, float D, float ramp, float limit); + ~PIDController() = default; + + float operator() (float error); + void reset(); + + float P; //!< Proportional gain + float I; //!< Integral gain + float D; //!< Derivative gain + float output_ramp; //!< Maximum speed of change of the output value + float limit; //!< Maximum output value + +protected: + float error_prev; //!< last tracking error value + float output_prev; //!< last pid output value + float integral_prev; //!< last integral component value + unsigned long timestamp_prev; //!< Last execution timestamp +}; + +#endif // PID_H \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.cpp new file mode 100644 index 0000000..2acb47a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.cpp @@ -0,0 +1,31 @@ +#include "time_utils.h" + +// function buffering delay() +// arduino uno function doesn't work well with interrupts +void _delay(unsigned long ms){ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) + // if arduino uno and other atmega328p chips + // use while instad of delay, + // due to wrong measurement based on changed timer0 + unsigned long t = _micros() + ms*1000; + while( _micros() < t ){}; +#else + // regular micros + delay(ms); +#endif +} + + +// function buffering _micros() +// arduino function doesn't work well with interrupts +unsigned long _micros(){ +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega32U4__) +// if arduino uno and other atmega328p chips + //return the value based on the prescaler + if((TCCR0B & 0b00000111) == 0x01) return (micros()/32); + else return (micros()); +#else + // regular micros + return micros(); +#endif +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.h new file mode 100644 index 0000000..143d485 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/common/time_utils.h @@ -0,0 +1,22 @@ +#ifndef TIME_UTILS_H +#define TIME_UTILS_H + +#include "foc_utils.h" + +/** + * Function implementing delay() function in milliseconds + * - blocking function + * - hardware specific + + * @param ms number of milliseconds to wait + */ +void _delay(unsigned long ms); + +/** + * Function implementing timestamp getting function in microseconds + * hardware specific + */ +unsigned long _micros(); + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.cpp new file mode 100644 index 0000000..ee6be9f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.cpp @@ -0,0 +1,685 @@ +#include "Commander.h" + +Commander::Commander(Stream& serial, char eol, bool echo){ + com_port = &serial; + this->eol = eol; + this->echo = echo; +} +Commander::Commander(char eol, bool echo){ + this->eol = eol; + this->echo = echo; +} + + +void Commander::add(char id, CommandCallback onCommand, char* label ){ + call_list[call_count] = onCommand; + call_ids[call_count] = id; + call_label[call_count] = label; + call_count++; +} + + +void Commander::run(){ + if(!com_port) return; + run(*com_port, eol); +} + +void Commander::run(Stream& serial, char eol){ + Stream* tmp = com_port; // save the serial instance + char eol_tmp = this->eol; + this->eol = eol; + com_port = &serial; + + // a string to hold incoming data + while (serial.available()) { + // get the new byte: + int ch = serial.read(); + received_chars[rec_cnt++] = (char)ch; + // end of user input + if(echo) + print((char)ch); + if (isSentinel(ch)) { + // execute the user command + run(received_chars); + + // reset the command buffer + received_chars[0] = 0; + rec_cnt=0; + } + if (rec_cnt>=MAX_COMMAND_LENGTH) { // prevent buffer overrun if message is too long + received_chars[0] = 0; + rec_cnt=0; + } + } + + com_port = tmp; // reset the instance to the internal value + this->eol = eol_tmp; +} + +void Commander::run(char* user_input){ + // execute the user command + char id = user_input[0]; + switch(id){ + case CMD_SCAN: + for(int i=0; i < call_count; i++){ + printMachineReadable(CMD_SCAN); + print(call_ids[i]); + print(":"); + if(call_label[i]) println(call_label[i]); + else println(""); + } + break; + case CMD_VERBOSE: + if(!isSentinel(user_input[1])) verbose = (VerboseMode)atoi(&user_input[1]); + printVerbose(F("Verb:")); + printMachineReadable(CMD_VERBOSE); + switch (verbose){ + case VerboseMode::nothing: + println(F("off!")); + break; + case VerboseMode::on_request: + case VerboseMode::user_friendly: + println(F("on!")); + break; + case VerboseMode::machine_readable: + printlnMachineReadable(F("machine")); + break; + } + break; + case CMD_DECIMAL: + if(!isSentinel(user_input[1])) decimal_places = atoi(&user_input[1]); + printVerbose(F("Decimal:")); + printMachineReadable(CMD_DECIMAL); + println(decimal_places); + break; + default: + for(int i=0; i < call_count; i++){ + if(id == call_ids[i]){ + printMachineReadable(user_input[0]); + call_list[i](&user_input[1]); + break; + } + } + break; + } +} + +void Commander::motor(FOCMotor* motor, char* user_command) { + + // if target setting + if(isDigit(user_command[0]) || user_command[0] == '-' || user_command[0] == '+' || isSentinel(user_command[0])){ + target(motor, user_command); + return; + } + + // parse command letter + char cmd = user_command[0]; + char sub_cmd = user_command[1]; + // check if there is a subcommand or not + int value_index = (sub_cmd >= 'A' && sub_cmd <= 'Z') || (sub_cmd == '#') ? 2 : 1; + // check if get command + bool GET = isSentinel(user_command[value_index]); + // parse command values + float value = atof(&user_command[value_index]); + printMachineReadable(cmd); + if (sub_cmd >= 'A' && sub_cmd <= 'Z') { + printMachineReadable(sub_cmd); + } + + // a bit of optimisation of variable memory for Arduino UNO (atmega328) + switch(cmd){ + case CMD_C_Q_PID: // + printVerbose(F("PID curr q| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_current_q, &user_command[1]); + else pid(&motor->PID_current_q,&user_command[1]); + break; + case CMD_C_D_PID: // + printVerbose(F("PID curr d| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_current_d, &user_command[1]); + else pid(&motor->PID_current_d, &user_command[1]); + break; + case CMD_V_PID: // + printVerbose(F("PID vel| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_velocity, &user_command[1]); + else pid(&motor->PID_velocity, &user_command[1]); + break; + case CMD_A_PID: // + printVerbose(F("PID angle| ")); + if(sub_cmd == SCMD_LPF_TF) lpf(&motor->LPF_angle, &user_command[1]); + else pid(&motor->P_angle, &user_command[1]); + break; + case CMD_LIMITS: // + printVerbose(F("Limits| ")); + switch (sub_cmd){ + case SCMD_LIM_VOLT: // voltage limit change + printVerbose(F("volt: ")); + if(!GET) { + motor->voltage_limit = value; + motor->PID_current_d.limit = value; + motor->PID_current_q.limit = value; + // change velocity pid limit if in voltage mode and no phase resistance set + if( !_isset(motor->phase_resistance) && motor->torque_controller==TorqueControlType::voltage) motor->PID_velocity.limit = value; + } + println(motor->voltage_limit); + break; + case SCMD_LIM_CURR: // current limit + printVerbose(F("curr: ")); + if(!GET){ + motor->current_limit = value; + // if phase resistance specified or the current control is on set the current limit to the velocity PID + if(_isset(motor->phase_resistance) || motor->torque_controller != TorqueControlType::voltage ) motor->PID_velocity.limit = value; + } + println(motor->current_limit); + break; + case SCMD_LIM_VEL: // velocity limit + printVerbose(F("vel: ")); + if(!GET){ + motor->velocity_limit = value; + motor->P_angle.limit = value; + } + println(motor->velocity_limit); + break; + default: + printError(); + break; + } + break; + case CMD_MOTION_TYPE: + case CMD_TORQUE_TYPE: + case CMD_STATUS: + motion(motor, &user_command[0]); + break; + case CMD_PWMMOD: + // PWM modulation change + printVerbose(F("PWM Mod | ")); + switch (sub_cmd){ + case SCMD_PWMMOD_TYPE: // zero offset + printVerbose(F("type: ")); + if(!GET) motor->foc_modulation = (FOCModulationType)value; + switch(motor->foc_modulation){ + case FOCModulationType::SinePWM: + println(F("SinePWM")); + break; + case FOCModulationType::SpaceVectorPWM: + println(F("SVPWM")); + break; + case FOCModulationType::Trapezoid_120: + println(F("Trap 120")); + break; + case FOCModulationType::Trapezoid_150: + println(F("Trap 150")); + break; + } + break; + case SCMD_PWMMOD_CENTER: // centered modulation + printVerbose(F("center: ")); + if(!GET) motor->modulation_centered = value; + println(motor->modulation_centered); + break; + default: + printError(); + break; + } + break; + case CMD_RESIST: + printVerbose(F("R phase: ")); + if(!GET){ + motor->phase_resistance = value; + if(motor->torque_controller==TorqueControlType::voltage) + motor->PID_velocity.limit= motor->current_limit; + } + if(_isset(motor->phase_resistance)) println(motor->phase_resistance); + else println(0); + break; + case CMD_INDUCTANCE: + printVerbose(F("L phase: ")); + if(!GET){ + motor->phase_inductance = value; + } + if(_isset(motor->phase_inductance)) println(motor->phase_inductance); + else println(0); + break; + case CMD_KV_RATING: + printVerbose(F("Motor KV: ")); + if(!GET){ + motor->KV_rating = value; + } + if(_isset(motor->KV_rating)) println(motor->KV_rating); + else println(0); + break; + case CMD_SENSOR: + // Sensor zero offset + printVerbose(F("Sensor | ")); + switch (sub_cmd){ + case SCMD_SENS_MECH_OFFSET: // zero offset + printVerbose(F("offset: ")); + if(!GET) motor->sensor_offset = value; + println(motor->sensor_offset); + break; + case SCMD_SENS_ELEC_OFFSET: // electrical zero offset - not suggested to touch + printVerbose(F("el. offset: ")); + if(!GET) motor->zero_electric_angle = value; + println(motor->zero_electric_angle); + break; + default: + printError(); + break; + } + break; + case CMD_MONITOR: // get current values of the state variables + printVerbose(F("Monitor | ")); + switch (sub_cmd){ + case SCMD_GET: // get command + switch((uint8_t)value){ + case 0: // get target + printVerbose(F("target: ")); + println(motor->target); + break; + case 1: // get voltage q + printVerbose(F("Vq: ")); + println(motor->voltage.q); + break; + case 2: // get voltage d + printVerbose(F("Vd: ")); + println(motor->voltage.d); + break; + case 3: // get current q + printVerbose(F("Cq: ")); + println(motor->current.q); + break; + case 4: // get current d + printVerbose(F("Cd: ")); + println(motor->current.d); + break; + case 5: // get velocity + printVerbose(F("vel: ")); + println(motor->shaft_velocity); + break; + case 6: // get angle + printVerbose(F("angle: ")); + println(motor->shaft_angle); + break; + case 7: // get all states + printVerbose(F("all: ")); + print(motor->target); + print(";"); + print(motor->voltage.q); + print(";"); + print(motor->voltage.d); + print(";"); + print(motor->current.q); + print(";"); + print(motor->current.d); + print(";"); + print(motor->shaft_velocity); + print(";"); + println(motor->shaft_angle); + break; + default: + printError(); + break; + } + break; + case SCMD_DOWNSAMPLE: + printVerbose(F("downsample: ")); + if(!GET) motor->monitor_downsample = value; + println((int)motor->monitor_downsample); + break; + case SCMD_CLEAR: + motor->monitor_variables = (uint8_t) 0; + println(F("clear")); + break; + case CMD_DECIMAL: + printVerbose(F("decimal: ")); + motor->monitor_decimals = value; + println((int)motor->monitor_decimals); + break; + case SCMD_SET: + if(!GET){ + // set the variables + motor->monitor_variables = (uint8_t) 0; + for(int i = 0; i < 7; i++){ + if(isSentinel(user_command[value_index+i])) break; + motor->monitor_variables |= (user_command[value_index+i] - '0') << (6-i); + } + } + // print the variables + for(int i = 0; i < 7; i++){ + print( (motor->monitor_variables & (1 << (6-i))) >> (6-i)); + } + println(""); + break; + default: + printError(); + break; + } + break; + default: // unknown cmd + printVerbose(F("unknown cmd ")); + printError(); + } +} + +void Commander::motion(FOCMotor* motor, char* user_cmd, char* separator){ + char cmd = user_cmd[0]; + char sub_cmd = user_cmd[1]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[(sub_cmd >= 'A' && sub_cmd <= 'Z') ? 2 : 1]); + + switch(cmd){ + case CMD_MOTION_TYPE: + printVerbose(F("Motion:")); + switch(sub_cmd){ + case SCMD_DOWNSAMPLE: + printVerbose(F(" downsample: ")); + if(!GET) motor->motion_downsample = value; + println((int)motor->motion_downsample); + break; + default: + // change control type + if(!GET && value >= 0 && (int)value < 5) // if set command + motor->controller = (MotionControlType)value; + switch(motor->controller){ + case MotionControlType::torque: + println(F("torque")); + break; + case MotionControlType::velocity: + println(F("vel")); + break; + case MotionControlType::angle: + println(F("angle")); + break; + case MotionControlType::velocity_openloop: + println(F("vel open")); + break; + case MotionControlType::angle_openloop: + println(F("angle open")); + break; + } + break; + } + break; + case CMD_TORQUE_TYPE: + // change control type + printVerbose(F("Torque: ")); + if(!GET && (int8_t)value >= 0 && (int8_t)value < 3)// if set command + motor->torque_controller = (TorqueControlType)value; + switch(motor->torque_controller){ + case TorqueControlType::voltage: + println(F("volt")); + // change the velocity control limits if necessary + if( !_isset(motor->phase_resistance) ) motor->PID_velocity.limit = motor->voltage_limit; + break; + case TorqueControlType::dc_current: + println(F("dc curr")); + // change the velocity control limits if necessary + motor->PID_velocity.limit = motor->current_limit; + break; + case TorqueControlType::foc_current: + println(F("foc curr")); + // change the velocity control limits if necessary + motor->PID_velocity.limit = motor->current_limit; + break; + } + break; + case CMD_STATUS: + // enable/disable + printVerbose(F("Status: ")); + if(!GET) (bool)value ? motor->enable() : motor->disable(); + println(motor->enabled); + break; + default: + target(motor, user_cmd, separator); + break; + } +} + +void Commander::pid(PIDController* pid, char* user_cmd){ + char cmd = user_cmd[0]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[1]); + + switch (cmd){ + case SCMD_PID_P: // P gain change + printVerbose("P: "); + if(!GET) pid->P = value; + println(pid->P); + break; + case SCMD_PID_I: // I gain change + printVerbose("I: "); + if(!GET) pid->I = value; + println(pid->I); + break; + case SCMD_PID_D: // D gain change + printVerbose("D: "); + if(!GET) pid->D = value; + println(pid->D); + break; + case SCMD_PID_RAMP: // ramp change + printVerbose("ramp: "); + if(!GET) pid->output_ramp = value; + println(pid->output_ramp); + break; + case SCMD_PID_LIM: // limit change + printVerbose("limit: "); + if(!GET) pid->limit = value; + println(pid->limit); + break; + default: + printError(); + break; + } +} + +void Commander::lpf(LowPassFilter* lpf, char* user_cmd){ + char cmd = user_cmd[0]; + bool GET = isSentinel(user_cmd[1]); + float value = atof(&user_cmd[1]); + + switch (cmd){ + case SCMD_LPF_TF: // Tf value change + printVerbose(F("Tf: ")); + if(!GET) lpf->Tf = value; + println(lpf->Tf); + break; + default: + printError(); + break; + } +} + +void Commander::scalar(float* value, char* user_cmd){ + bool GET = isSentinel(user_cmd[0]); + if(!GET) *value = atof(user_cmd); + println(*value); +} + + +void Commander::target(FOCMotor* motor, char* user_cmd, char* separator){ + // if no values sent + if(isSentinel(user_cmd[0])) { + printlnMachineReadable(motor->target); + return; + }; + + float pos, vel, torque; + char* next_value; + switch(motor->controller){ + case MotionControlType::torque: // setting torque target + torque = atof(strtok (user_cmd, separator)); + motor->target = torque; + break; + case MotionControlType::velocity: // setting velocity target + torque limit + // set the target + vel= atof(strtok (user_cmd, separator)); + motor->target = vel; + + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value){ + torque = atof(next_value); + motor->PID_velocity.limit = torque; + // torque command can be voltage or current + if(!_isset(motor->phase_resistance) && motor->torque_controller == TorqueControlType::voltage) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + break; + case MotionControlType::angle: // setting angle target + torque, velocity limit + // setting the target position + pos= atof(strtok (user_cmd, separator)); + motor->target = pos; + + // allow for setting only the target position without chaning the velocity/torque limits + next_value = strtok (NULL, separator); + if( next_value ){ + vel = atof(next_value); + motor->velocity_limit = vel; + motor->P_angle.limit = vel; + + // allow for setting only the target position and velocity limit without the torque limit + next_value = strtok (NULL, separator); + if( next_value ){ + torque= atof(next_value); + motor->PID_velocity.limit = torque; + // torque command can be voltage or current + if(!_isset(motor->phase_resistance) && motor->torque_controller == TorqueControlType::voltage) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + } + break; + case MotionControlType::velocity_openloop: // setting velocity target + torque limit + // set the target + vel= atof(strtok (user_cmd, separator)); + motor->target = vel; + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value ){ + torque = atof(next_value); + // torque command can be voltage or current + if(!_isset(motor->phase_resistance)) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + break; + case MotionControlType::angle_openloop: // setting angle target + torque, velocity limit + // set the target + pos= atof(strtok (user_cmd, separator)); + motor->target = pos; + + // allow for setting only the target position without chaning the velocity/torque limits + next_value = strtok (NULL, separator); + if( next_value ){ + vel = atof(next_value); + motor->velocity_limit = vel; + // allow for setting only the target velocity without chaning the torque limit + next_value = strtok (NULL, separator); + if (next_value ){ + torque = atof(next_value); + // torque command can be voltage or current + if(!_isset(motor->phase_resistance)) motor->voltage_limit = torque; + else motor->current_limit = torque; + } + } + break; + } + printVerbose(F("Target: ")); + println(motor->target); +} + + +bool Commander::isSentinel(char ch) +{ + if(ch == eol) + return true; + else if (ch == '\r') + { + printVerbose(F("Warn: \\r detected! \n")); + return true; // lets still consider it to end the line... + } + return false; +} + +void Commander::print(const int number){ + if( !com_port || verbose == VerboseMode::nothing ) return; + com_port->print(number); +} +void Commander::print(const float number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print((float)number,(int)decimal_places); +} +void Commander::print(const char* message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} +void Commander::print(const __FlashStringHelper *message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} +void Commander::print(const char message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->print(message); +} + +void Commander::println(const int number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(number); +} +void Commander::println(const float number){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println((float)number, (int)decimal_places); +} +void Commander::println(const char* message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} +void Commander::println(const __FlashStringHelper *message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} +void Commander::println(const char message){ + if(!com_port || verbose == VerboseMode::nothing ) return; + com_port->println(message); +} + + +void Commander::printVerbose(const char* message){ + if(verbose == VerboseMode::user_friendly) print(message); +} +void Commander::printVerbose(const __FlashStringHelper *message){ + if(verbose == VerboseMode::user_friendly) print(message); +} + +void Commander::printMachineReadable(const int number){ + if(verbose == VerboseMode::machine_readable) print(number); +} +void Commander::printMachineReadable(const float number){ + if(verbose == VerboseMode::machine_readable) print(number); +} +void Commander::printMachineReadable(const char* message){ + if(verbose == VerboseMode::machine_readable) print(message); +} +void Commander::printMachineReadable(const __FlashStringHelper *message){ + if(verbose == VerboseMode::machine_readable) print(message); +} +void Commander::printMachineReadable(const char message){ + if(verbose == VerboseMode::machine_readable) print(message); +} + +void Commander::printlnMachineReadable(const int number){ + if(verbose == VerboseMode::machine_readable) println(number); +} +void Commander::printlnMachineReadable(const float number){ + if(verbose == VerboseMode::machine_readable) println(number); +} +void Commander::printlnMachineReadable(const char* message){ + if(verbose == VerboseMode::machine_readable) println(message); +} +void Commander::printlnMachineReadable(const __FlashStringHelper *message){ + if(verbose == VerboseMode::machine_readable) println(message); +} +void Commander::printlnMachineReadable(const char message){ + if(verbose == VerboseMode::machine_readable) println(message); +} + +void Commander::printError(){ + println(F("err")); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.h new file mode 100644 index 0000000..91e3dc4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/Commander.h @@ -0,0 +1,301 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +#include "Arduino.h" +#include "../common/base_classes/FOCMotor.h" +#include "../common/pid.h" +#include "../common/lowpass_filter.h" +#include "commands.h" + + +#define MAX_COMMAND_LENGTH 20 + + +// Commander verbose display to the user type +enum VerboseMode : uint8_t { + nothing = 0x00, // display nothing - good for monitoring + on_request = 0x01, // display only on user request + user_friendly = 0x02, // display textual messages to the user + machine_readable = 0x03 // display machine readable commands, matching commands to set each settings +}; + + +// callback function pointer definiton +typedef void (* CommandCallback)(char*); //!< command callback function pointer + +/** + * Commander class implementing string communication protocol based on IDvalue (ex AB5.321 - command id `A`, sub-command id `B`,value `5.321`) + * + * - This class can be used in combination with HardwareSerial instance which it would read and write + * or it can be used to parse strings that have been received from the user outside this library + * - Commander class implements command protocol for few standard components of the SimpleFOC library + * - FOCMotor + * - PIDController + * - LowPassFilter + * - Commander also provides a very simple command > callback interface that enables user to + * attach a callback function to certain command id - see function add() + */ +class Commander +{ + public: + /** + * Default constructor receiving a serial interface that it uses to output the values to + * Also if the function run() is used it uses this serial instance to read the serial for user commands + * + * @param serial - Serial com port instance + * @param eol - the end of line sentinel character + * @param echo - echo last typed character (for command line feedback) + */ + Commander(Stream &serial, char eol = '\n', bool echo = false); + Commander(char eol = '\n', bool echo = false); + + /** + * Function reading the serial port and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + */ + void run(); + /** + * Function reading the string of user input and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + * + * @param reader - temporary stream to read user input + * @param eol - temporary end of line sentinel + */ + void run(Stream &reader, char eol = '\n'); + /** + * Function reading the string of user input and firing callbacks that have been added to the commander + * once the user has requested them - when he sends the command + * + * - It has default commands (the letters can be changed in the commands.h file) + * '@' - Verbose mode + * '#' - Number of decimal places + * '?' - Scan command - displays all the labels of attached nodes + * + * @param user_input - string of user inputs + */ + void run(char* user_input); + + /** + * Function adding a callback to the coomander withe the command id + * @param id - char command letter + * @param onCommand - function pointer void function(char*) + * @param label - string label to be displayed when scan command sent + */ + void add(char id , CommandCallback onCommand, char* label = nullptr); + + // printing variables + VerboseMode verbose = VerboseMode::user_friendly; //!< flag signaling that the commands should output user understanable text + uint8_t decimal_places = 3; //!< number of decimal places to be used when displaying numbers + + // monitoring functions + Stream* com_port = nullptr; //!< Serial terminal variable if provided + char eol = '\n'; //!< end of line sentinel character + bool echo = false; //!< echo last typed character (for command line feedback) + + /** + * + * FOC motor (StepperMotor and BLDCMotor) command interface + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * + * - It has several paramters (the letters can be changed in the commands.h file) + * 'Q' - Q current PID controller & LPF (see function pid and lpf for commands) + * 'D' - D current PID controller & LPF (see function pid and lpf for commands) + * 'V' - Velocity PID controller & LPF (see function pid and lpf for commands) + * 'A' - Angle PID controller & LPF (see function pid and lpf for commands) + * 'L' - Limits + * sub-commands: + * 'C' - Current + * 'U' - Voltage + * 'V' - Velocity + * 'C' - Motion control type config + * sub-commands: + * 'D' - downsample motiron loop + * '0' - torque + * '1' - velocity + * '2' - angle + * 'T' - Torque control type + * sub-commands: + * '0' - voltage + * '1' - current + * '2' - foc_current + * 'E' - Motor status (enable/disable) + * sub-commands: + * '0' - enable + * '1' - disable + * 'R' - Motor resistance + * 'S' - Sensor offsets + * sub-commands: + * 'M' - sensor offset + * 'E' - sensor electrical zero + * 'M' - Monitoring control + * sub-commands: + * 'D' - downsample monitoring + * 'C' - clear monitor + * 'S' - set monitoring variables + * 'G' - get variable value + * '' - Target setting interface + * Depends of the motion control mode: + * - torque : torque (ex. M2.5) + * - velocity (open and closed loop) : velocity torque (ex.M10 2.5 or M10 to only chanage the target witout limits) + * - angle (open and closed loop) : angle velocity torque (ex.M3.5 10 2.5 or M3.5 to only chanage the target witout limits) + * + * - Each of them can be get by sening the command letter -(ex. 'R' - to get the phase resistance) + * - Each of them can be set by sending 'IdSubidValue' - (ex. SM1.5 for setting sensor zero offset to 1.5f) + * + */ + void motor(FOCMotor* motor, char* user_cmd); + + /** + * Low pass fileter command interface + * @param lpf - LowPassFilter instance + * @param user_cmd - the string command + * + * - It only has one property - filtering time constant Tf + * - It can be get by sending 'F' + * - It can be set by sending 'Fvalue' - (ex. F0.01 for settin Tf=0.01) + */ + void lpf(LowPassFilter* lpf, char* user_cmd); + /** + * PID controller command interface + * @param pid - PIDController instance + * @param user_cmd - the string command + * + * - It has several paramters (the letters can be changed in the commands.h file) + * - P gain - 'P' + * - I gain - 'I' + * - D gain - 'D' + * - output ramp - 'R' + * - output limit - 'L' + * - Each of them can be get by sening the command letter -(ex. 'D' - to get the D gain) + * - Each of them can be set by sending 'IDvalue' - (ex. I1.5 for setting I=1.5) + */ + void pid(PIDController* pid, char* user_cmd); + /** + * Float variable scalar command interface + * @param value - float variable pointer + * @param user_cmd - the string command + * + * - It only has one property - one float value + * - It can be get by sending an empty string '\n' + * - It can be set by sending 'value' - (ex. 0.01f for settin *value=0.01) + */ + void scalar(float* value, char* user_cmd); + /** + * Target setting interface, enables setting the target and limiting variables at once. + * The values are sent separated by a separator specified as the third argument. The default separator is the space. + * + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * @param separator - the string separator in between target and limit values, default is space - " " + * + * Example: P2.34 70 2 + * `P` is the user defined command, `2.34` is the target angle `70` is the target + * velocity and `2` is the desired max current. + * + * It depends of the motion control mode: + * - torque : torque (ex. P2.5) + * - velocity : velocity torque (ex.P10 2.5 or P10 to only chanage the target witout limits) + * - angle : angle velocity torque (ex.P3.5 10 2.5 or P3.5 to only chanage the target witout limits) + */ + void target(FOCMotor* motor, char* user_cmd, char* separator = (char *)" "); + + /** + * FOC motor (StepperMotor and BLDCMotor) motion control interfaces + * @param motor - FOCMotor (BLDCMotor or StepperMotor) instance + * @param user_cmd - the string command + * @param separator - the string separator in between target and limit values, default is space - " " + * + * Commands: + * 'C' - Motion control type config + * sub-commands: + * 'D' - downsample motiron loop + * '0' - torque + * '1' - velocity + * '2' - angle + * 'T' - Torque control type + * sub-commands: + * '0' - voltage + * '1' - current + * '2' - foc_current + * 'E' - Motor status (enable/disable) + * sub-commands: + * '0' - enable + * '1' - disable + * '' - Target setting interface + * Depends of the motion control mode: + * - torque : torque (ex. M2.5) + * - velocity (open and closed loop) : velocity torque (ex.M10 2.5 or M10 to only chanage the target witout limits) + * - angle (open and closed loop) : angle velocity torque (ex.M3.5 10 2.5 or M3.5 to only chanage the target witout limits) + */ + void motion(FOCMotor* motor, char* user_cmd, char* separator = (char *)" "); + + bool isSentinel(char ch); + private: + // Subscribed command callback variables + CommandCallback call_list[20];//!< array of command callback pointers - 20 is an arbitrary number + char call_ids[20]; //!< added callback commands + char* call_label[20]; //!< added callback labels + int call_count = 0;//!< number callbacks that are subscribed + + // helping variable for serial communication reading + char received_chars[MAX_COMMAND_LENGTH] = {0}; //!< so far received user message - waiting for newline + int rec_cnt = 0; //!< number of characters receives + + // serial printing functions + /** + * print the string message only if verbose mode on + * @param message - message to be printed + */ + void printVerbose(const char* message); + /** + * Print the string message only if verbose mode on + * - Function handling the case for strings defined by F macro + * @param message - message to be printed + */ + void printVerbose(const __FlashStringHelper *message); + /** + * print the numbers to the serial with desired decimal point number + * @param message - number to be printed + * @param newline - if needs lewline (1) otherwise (0) + */ + + void print(const float number); + void print(const int number); + void print(const char* message); + void print(const __FlashStringHelper *message); + void print(const char message); + void println(const float number); + void println(const int number); + void println(const char* message); + void println(const __FlashStringHelper *message); + void println(const char message); + + void printMachineReadable(const float number); + void printMachineReadable(const int number); + void printMachineReadable(const char* message); + void printMachineReadable(const __FlashStringHelper *message); + void printMachineReadable(const char message); + + void printlnMachineReadable(const float number); + void printlnMachineReadable(const int number); + void printlnMachineReadable(const char* message); + void printlnMachineReadable(const __FlashStringHelper *message); + void printlnMachineReadable(const char message); + + + void printError(); +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.cpp new file mode 100644 index 0000000..e969d8a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.cpp @@ -0,0 +1,104 @@ + +#include "SimpleFOCDebug.h" + +#ifndef SIMPLEFOC_DISABLE_DEBUG + + +Print* SimpleFOCDebug::_debugPrint = NULL; + + +void SimpleFOCDebug::enable(Print* debugPrint) { + _debugPrint = debugPrint; +} + + +void SimpleFOCDebug::println(int val) { + if (_debugPrint != NULL) { + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(float val) { + if (_debugPrint != NULL) { + _debugPrint->println(val); + } +} + + + +void SimpleFOCDebug::println(const char* str) { + if (_debugPrint != NULL) { + _debugPrint->println(str); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str) { + if (_debugPrint != NULL) { + _debugPrint->println(str); + } +} + +void SimpleFOCDebug::println(const char* str, float val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str, float val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const char* str, int val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + +void SimpleFOCDebug::println(const __FlashStringHelper* str, int val) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + _debugPrint->println(val); + } +} + + +void SimpleFOCDebug::print(const char* str) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + } +} + + +void SimpleFOCDebug::print(const __FlashStringHelper* str) { + if (_debugPrint != NULL) { + _debugPrint->print(str); + } +} + + +void SimpleFOCDebug::print(int val) { + if (_debugPrint != NULL) { + _debugPrint->print(val); + } +} + + +void SimpleFOCDebug::print(float val) { + if (_debugPrint != NULL) { + _debugPrint->print(val); + } +} + + +void SimpleFOCDebug::println() { + if (_debugPrint != NULL) { + _debugPrint->println(); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.h new file mode 100644 index 0000000..614e637 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/SimpleFOCDebug.h @@ -0,0 +1,79 @@ + +#ifndef __SIMPLEFOCDEBUG_H__ +#define __SIMPLEFOCDEBUG_H__ + +#include "Arduino.h" + + +/** + * SimpleFOCDebug class + * + * This class is used to print debug messages to a chosen output. + * Currently, Print instances are supported as targets, e.g. serial port. + * + * Activate debug output globally by calling enable(), optionally passing + * in a Print instance. If none is provided "Serial" is used by default. + * + * To produce debug output, use the macro SIMPLEFOC_DEBUG: + * SIMPLEFOC_DEBUG("Debug message!"); + * SIMPLEFOC_DEBUG("a float value:", 123.456f); + * SIMPLEFOC_DEBUG("an integer value: ", 123); + * + * Keep debugging output short and simple. Some of our MCUs have limited + * RAM and limited serial output capabilities. + * + * By default, the SIMPLEFOC_DEBUG macro uses the flash string helper to + * help preserve memory on Arduino boards. + * + * You can also disable debug output completely. In this case all debug output + * and the SimpleFOCDebug class is removed from the compiled code. + * Add -DSIMPLEFOC_DISABLE_DEBUG to your compiler flags to disable debug in + * this way. + * + **/ + + +#ifndef SIMPLEFOC_DISABLE_DEBUG + +class SimpleFOCDebug { +public: + static void enable(Print* debugPrint = &Serial); + + static void println(const __FlashStringHelper* msg); + static void println(const char* msg); + static void println(const __FlashStringHelper* msg, float val); + static void println(const char* msg, float val); + static void println(const __FlashStringHelper* msg, int val); + static void println(const char* msg, int val); + static void println(); + static void println(int val); + static void println(float val); + + static void print(const char* msg); + static void print(const __FlashStringHelper* msg); + static void print(int val); + static void print(float val); + +protected: + static Print* _debugPrint; +}; + + +#define SIMPLEFOC_DEBUG(msg, ...) \ + SimpleFOCDebug::println(F(msg), ##__VA_ARGS__) + + + + + +#else //ifndef SIMPLEFOC_DISABLE_DEBUG + + + +#define SIMPLEFOC_DEBUG(msg, ...) + + + +#endif //ifndef SIMPLEFOC_DISABLE_DEBUG +#endif + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.cpp new file mode 100644 index 0000000..ee4f69e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.cpp @@ -0,0 +1,40 @@ +#include "StepDirListener.h" + +StepDirListener::StepDirListener(int _pinStep, int _pinDir, float _counter_to_value){ + pin_step = _pinStep; + pin_dir = _pinDir; + counter_to_value = _counter_to_value; +} + +void StepDirListener::init(){ + pinMode(pin_dir, INPUT); + pinMode(pin_step, INPUT_PULLUP); + count = 0; +} + +void StepDirListener::enableInterrupt(void (*doA)()){ + attachInterrupt(digitalPinToInterrupt(pin_step), doA, polarity); +} + +void StepDirListener::attach(float* variable){ + attached_variable = variable; +} + +void StepDirListener::handle(){ + // read step status + //bool step = digitalRead(pin_step); + // update counter only on rising edge + //if(step && step != step_active){ + if(digitalRead(pin_dir)) + count++; + else + count--; + //} + //step_active = step; + // if attached variable update it + if(attached_variable) *attached_variable = getValue(); +} +// calculate the position from counter +float StepDirListener::getValue(){ + return (float) count * counter_to_value; +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.h new file mode 100644 index 0000000..f9691fd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/StepDirListener.h @@ -0,0 +1,65 @@ +#ifndef STEPDIR_H +#define STEPDIR_H + +#include "Arduino.h" +#include "../common/foc_utils.h" + + +#if !defined(TARGET_RP2040) && !defined(_SAMD21_) && !defined(_SAMD51_) && !defined(_SAME51_) && !defined(ARDUINO_UNOR4_WIFI) && !defined(ARDUINO_UNOR4_MINIMA) && !defined(NRF52_SERIES) && !defined(ARDUINO_ARCH_MEGAAVR) +#define PinStatus int +#endif + + +/** + * Step/Dir listenner class for easier interraction with this communication interface. + */ +class StepDirListener +{ + public: + + /** + * Constructor for step/direction interface + * @param step - pin + * @param direction - pin + * @param counter_to_value - step counter to value + */ + StepDirListener(int pinStep, int pinDir, float counter_to_value = 1); + /** + * Start listenning for step commands + * + * @param handleStep - on step received handler + */ + void enableInterrupt(void (*handleStep)()); + + /** + * Initialise dir and step commands + */ + void init(); + /** + * step handler + */ + void handle(); + /** + * Get so far received valued + */ + float getValue(); + /** + * Attach the value to be updated on each step receive + * - no need to call getValue function + */ + void attach(float* variable); + + // variables + int pin_step; //!< step pin + int pin_dir; //!< direction pin + long count; //!< current counter value - should be set to 0 for homing + PinStatus polarity = RISING; //!< polarity of the step pin + + private: + float* attached_variable = nullptr; //!< pointer to the attached variable + float counter_to_value; //!< step counter to value + //bool step_active = 0; //!< current step pin status (HIGH/LOW) - debouncing variable + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/commands.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/commands.h new file mode 100644 index 0000000..323a8e7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/communication/commands.h @@ -0,0 +1,52 @@ +#ifndef COMMANDS_h +#define COMMANDS_h + +// see docs.simplefoc.com for in depth explanation of the commands + +// list of commands + #define CMD_C_D_PID 'D' //!< current d PID & LPF + #define CMD_C_Q_PID 'Q' //!< current d PID & LPF + #define CMD_V_PID 'V' //!< velocity PID & LPF + #define CMD_A_PID 'A' //!< angle PID & LPF + #define CMD_STATUS 'E' //!< motor status enable/disable + #define CMD_LIMITS 'L' //!< limits current/voltage/velocity + #define CMD_MOTION_TYPE 'C' //!< motion control type + #define CMD_TORQUE_TYPE 'T' //!< torque control type + #define CMD_SENSOR 'S' //!< sensor offsets + #define CMD_MONITOR 'M' //!< monitoring + #define CMD_RESIST 'R' //!< motor phase resistance + #define CMD_INDUCTANCE 'I' //!< motor phase inductance + #define CMD_KV_RATING 'K' //!< motor kv rating + #define CMD_PWMMOD 'W' //!< pwm modulation + + // commander configuration + #define CMD_SCAN '?' //!< command scaning the network - only for commander + #define CMD_VERBOSE '@' //!< command setting output mode - only for commander + #define CMD_DECIMAL '#' //!< command setting decimal places - only for commander + + // subcomands + //pid - lpf + #define SCMD_PID_P 'P' //!< PID gain P + #define SCMD_PID_I 'I' //!< PID gain I + #define SCMD_PID_D 'D' //!< PID gain D + #define SCMD_PID_RAMP 'R' //!< PID ramp + #define SCMD_PID_LIM 'L' //!< PID limit + #define SCMD_LPF_TF 'F' //!< LPF time constant + // limits + #define SCMD_LIM_CURR 'C' //!< Limit current + #define SCMD_LIM_VOLT 'U' //!< Limit voltage + #define SCMD_LIM_VEL 'V' //!< Limit velocity + //sensor + #define SCMD_SENS_MECH_OFFSET 'M' //!< Sensor offset + #define SCMD_SENS_ELEC_OFFSET 'E' //!< Sensor electrical zero offset + // monitoring + #define SCMD_DOWNSAMPLE 'D' //!< Monitoring downsample value + #define SCMD_CLEAR 'C' //!< Clear all monitored variables + #define SCMD_GET 'G' //!< Get variable only one value + #define SCMD_SET 'S' //!< Set variables to be monitored + + #define SCMD_PWMMOD_TYPE 'T' //!<< Pwm modulation type + #define SCMD_PWMMOD_CENTER 'C' //!<< Pwm modulation center flag + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.cpp new file mode 100644 index 0000000..635535f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.cpp @@ -0,0 +1,163 @@ +#include "GenericCurrentSense.h" + +// GenericCurrentSense constructor +GenericCurrentSense::GenericCurrentSense(PhaseCurrent_s (*readCallback)(), void (*initCallback)()){ + // if function provided add it to the + if(readCallback != nullptr) this->readCallback = readCallback; + if(initCallback != nullptr) this->initCallback = initCallback; +} + +// Inline sensor init function +int GenericCurrentSense::init(){ + // configure ADC variables + if(initCallback != nullptr) initCallback(); + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void GenericCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 1000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + PhaseCurrent_s current = readCallback(); + offset_ia += current.a; + offset_ib += current.b; + offset_ic += current.c; + _delay(1); + } + // calculate the mean offsets + offset_ia = offset_ia / calibration_rounds; + offset_ib = offset_ib / calibration_rounds; + offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s GenericCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current = readCallback(); + current.a = (current.a - offset_ia); // amps + current.b = (current.b - offset_ib); // amps + current.c = (current.c - offset_ic); // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int GenericCurrentSense::driverAlign(float voltage){ + _UNUSED(voltage) ; // remove unused parameter warning + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (!initialized) return 0; + + // // set phase A active and phases B and C down + // driver->setPwm(voltage, 0, 0); + // _delay(200); + // PhaseCurrent_s c = getPhaseCurrents(); + // // read the current 100 times ( arbitrary number ) + // for (int i = 0; i < 100; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.a = c.a*0.6f + 0.4f*c1.a; + // c.b = c.b*0.6f + 0.4f*c1.b; + // c.c = c.c*0.6f + 0.4f*c1.c; + // _delay(3); + // } + // driver->setPwm(0, 0, 0); + // // align phase A + // float ab_ratio = fabs(c.a / c.b); + // float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + // if( ab_ratio > 1.5f ){ // should be ~2 + // gain_a *= _sign(c.a); + // }else if( ab_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and B + // int tmp_pinA = pinA; + // pinA = pinB; + // pinB = tmp_pinA; + // gain_a *= _sign(c.b); + // exit_flag = 2; // signal that pins have been switched + // }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and C + // int tmp_pinA = pinA; + // pinA = pinC; + // pinC= tmp_pinA; + // gain_a *= _sign(c.c); + // exit_flag = 2;// signal that pins have been switched + // }else{ + // // error in current sense - phase either not measured or bad connection + // return 0; + // } + + // // set phase B active and phases A and C down + // driver->setPwm(0, voltage, 0); + // _delay(200); + // c = getPhaseCurrents(); + // // read the current 50 times + // for (int i = 0; i < 100; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.a = c.a*0.6f + 0.4f*c1.a; + // c.b = c.b*0.6f + 0.4f*c1.b; + // c.c = c.c*0.6f + 0.4f*c1.c; + // _delay(3); + // } + // driver->setPwm(0, 0, 0); + // float ba_ratio = fabs(c.b/c.a); + // float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + // if( ba_ratio > 1.5f ){ // should be ~2 + // gain_b *= _sign(c.b); + // }else if( ba_ratio < 0.7f ){ // it should be ~0.5 + // // switch phase A and B + // int tmp_pinB = pinB; + // pinB = pinA; + // pinA = tmp_pinB; + // gain_b *= _sign(c.a); + // exit_flag = 2; // signal that pins have been switched + // }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // // switch phase A and C + // int tmp_pinB = pinB; + // pinB = pinC; + // pinC = tmp_pinB; + // gain_b *= _sign(c.c); + // exit_flag = 2; // signal that pins have been switched + // }else{ + // // error in current sense - phase either not measured or bad connection + // return 0; + // } + + // // if phase C measured + // if(_isset(pinC)){ + // // set phase B active and phases A and C down + // driver->setPwm(0, 0, voltage); + // _delay(200); + // c = getPhaseCurrents(); + // // read the adc voltage 500 times ( arbitrary number ) + // for (int i = 0; i < 50; i++) { + // PhaseCurrent_s c1 = getPhaseCurrents(); + // c.c = (c.c+c1.c)/50.0f; + // } + // driver->setPwm(0, 0, 0); + // gain_c *= _sign(c.c); + // } + + // if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + return exit_flag; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.h new file mode 100644 index 0000000..a63df49 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/GenericCurrentSense.h @@ -0,0 +1,40 @@ +#ifndef GENERIC_CS_LIB_H +#define GENERIC_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class GenericCurrentSense: public CurrentSense{ + public: + /** + GenericCurrentSense class constructor + */ + GenericCurrentSense(PhaseCurrent_s (*readCallback)() = nullptr, void (*initCallback)() = nullptr); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + + PhaseCurrent_s (*readCallback)() = nullptr; //!< function pointer to sensor reading + void (*initCallback)() = nullptr; //!< function pointer to sensor initialisation + + private: + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.cpp new file mode 100644 index 0000000..492b3ac --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.cpp @@ -0,0 +1,253 @@ +#include "InlineCurrentSense.h" +#include "communication/SimpleFOCDebug.h" +// InlineCurrentSensor constructor +// - shunt_resistor - shunt resistor value +// - gain - current-sense op-amp gain +// - phA - A phase adc pin +// - phB - B phase adc pin +// - phC - C phase adc pin (optional) +InlineCurrentSense::InlineCurrentSense(float _shunt_resistor, float _gain, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + shunt_resistor = _shunt_resistor; + amp_gain = _gain; + volts_to_amps_ratio = 1.0f /_shunt_resistor / _gain; // volts to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +}; + + +InlineCurrentSense::InlineCurrentSense(float _mVpA, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + volts_to_amps_ratio = 1000.0f / _mVpA; // mV to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +}; + + + +// Inline sensor init function +int InlineCurrentSense::init(){ + // if no linked driver its fine in this case + // at least for init() + void* drv_params = driver ? driver->params : nullptr; + // configure ADC variables + params = _configureADCInline(drv_params,pinA,pinB,pinC); + // if init failed return fail + if (params == SIMPLEFOC_CURRENT_SENSE_INIT_FAILED) return 0; + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void InlineCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 1000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + if(_isset(pinA)) offset_ia += _readADCVoltageInline(pinA, params); + if(_isset(pinB)) offset_ib += _readADCVoltageInline(pinB, params); + if(_isset(pinC)) offset_ic += _readADCVoltageInline(pinC, params); + _delay(1); + } + // calculate the mean offsets + if(_isset(pinA)) offset_ia = offset_ia / calibration_rounds; + if(_isset(pinB)) offset_ib = offset_ib / calibration_rounds; + if(_isset(pinC)) offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s InlineCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current; + current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageInline(pinA, params) - offset_ia)*gain_a;// amps + current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageInline(pinB, params) - offset_ib)*gain_b;// amps + current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageInline(pinC, params) - offset_ic)*gain_c; // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int InlineCurrentSense::driverAlign(float voltage){ + + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (driver==nullptr) { + SIMPLEFOC_DEBUG("CUR: No driver linked!"); + return 0; + } + + if (!initialized) return 0; + + if(_isset(pinA)){ + // set phase A active and phases B and C down + driver->setPwm(voltage, 0, 0); + _delay(2000); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 100 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6f + 0.4f*c1.a; + c.b = c.b*0.6f + 0.4f*c1.b; + c.c = c.c*0.6f + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + // align phase A + float ab_ratio = c.b ? fabs(c.a / c.b) : 0; + float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + if(_isset(pinB) && ab_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinC) && ac_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinB) && ab_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and B + int tmp_pinA = pinA; + pinA = pinB; + pinB = tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ib; + offset_ib = tmp_offsetA; + gain_a *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinA = pinA; + pinA = pinC; + pinC= tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ic; + offset_ic = tmp_offsetA; + gain_a *= _sign(c.c); + exit_flag = 2;// signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(_isset(pinB)){ + // set phase B active and phases A and C down + driver->setPwm(0, voltage, 0); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 50 times + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ba_ratio = c.a ? fabs(c.b / c.a) : 0; + float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + if(_isset(pinA) && ba_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinC) && bc_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinA) && ba_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and B + int tmp_pinB = pinB; + pinB = pinA; + pinA = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ia; + offset_ia = tmp_offsetB; + gain_b *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinB = pinB; + pinB = pinC; + pinC = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ic; + offset_ic = tmp_offsetB; + gain_b *= _sign(c.c); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + // if phase C measured + if(_isset(pinC)){ + // set phase C active and phases A and B down + driver->setPwm(0, 0, voltage); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the adc voltage 500 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ca_ratio = c.a ? fabs(c.c / c.a) : 0; + float cb_ratio = c.b ? fabs(c.c / c.b) : 0; + if(_isset(pinA) && ca_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinB) && cb_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinA) && ca_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and C + int tmp_pinC = pinC; + pinC = pinA; + pinA = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ia; + offset_ia = tmp_offsetC; + gain_c *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinB) && cb_ratio < 0.7f ){ // should be ~0.5 + // switch phase B and C + int tmp_pinC = pinC; + pinC = pinB; + pinB = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ib; + offset_ib = tmp_offsetC; + gain_c *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + + return exit_flag; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.h new file mode 100644 index 0000000..5fdca7d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/InlineCurrentSense.h @@ -0,0 +1,73 @@ +#ifndef INLINE_CS_LIB_H +#define INLINE_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class InlineCurrentSense: public CurrentSense{ + public: + /** + InlineCurrentSense class constructor + @param shunt_resistor shunt resistor value + @param gain current-sense op-amp gain + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + InlineCurrentSense(float shunt_resistor, float gain, int pinA, int pinB, int pinC = NOT_SET); + /** + InlineCurrentSense class constructor + @param mVpA mV per Amp ratio + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + InlineCurrentSense(float mVpA, int pinA, int pinB, int pinC = NOT_SET); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + // ADC measuremnet gain for each phase + // support for different gains for different phases of more commonly - inverted phase currents + // this should be automated later + float gain_a; //!< phase A gain + float gain_b; //!< phase B gain + float gain_c; //!< phase C gain + + // // per phase low pass fileters + // LowPassFilter lpf_a{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current A low pass filter + // LowPassFilter lpf_b{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current B low pass filter + // LowPassFilter lpf_c{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current C low pass filter + + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + + private: + + // hardware variables + int pinA; //!< pin A analog pin for current measurement + int pinB; //!< pin B analog pin for current measurement + int pinC; //!< pin C analog pin for current measurement + + // gain variables + float shunt_resistor; //!< Shunt resistor value + float amp_gain; //!< amp gain value + float volts_to_amps_ratio; //!< Volts to amps ratio + + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.cpp new file mode 100644 index 0000000..a706bee --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.cpp @@ -0,0 +1,254 @@ +#include "LowsideCurrentSense.h" +#include "communication/SimpleFOCDebug.h" +// LowsideCurrentSensor constructor +// - shunt_resistor - shunt resistor value +// - gain - current-sense op-amp gain +// - phA - A phase adc pin +// - phB - B phase adc pin +// - phC - C phase adc pin (optional) +LowsideCurrentSense::LowsideCurrentSense(float _shunt_resistor, float _gain, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + shunt_resistor = _shunt_resistor; + amp_gain = _gain; + volts_to_amps_ratio = 1.0f /_shunt_resistor / _gain; // volts to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +} + + +LowsideCurrentSense::LowsideCurrentSense(float _mVpA, int _pinA, int _pinB, int _pinC){ + pinA = _pinA; + pinB = _pinB; + pinC = _pinC; + + volts_to_amps_ratio = 1000.0f / _mVpA; // mV to amps + // gains for each phase + gain_a = volts_to_amps_ratio; + gain_b = volts_to_amps_ratio; + gain_c = volts_to_amps_ratio; +} + + +// Lowside sensor init function +int LowsideCurrentSense::init(){ + + if (driver==nullptr) { + SIMPLEFOC_DEBUG("CUR: Driver not linked!"); + return 0; + } + + // configure ADC variables + params = _configureADCLowSide(driver->params,pinA,pinB,pinC); + // if init failed return fail + if (params == SIMPLEFOC_CURRENT_SENSE_INIT_FAILED) return 0; + // sync the driver + _driverSyncLowSide(driver->params, params); + // calibrate zero offsets + calibrateOffsets(); + // set the initialized flag + initialized = (params!=SIMPLEFOC_CURRENT_SENSE_INIT_FAILED); + // return success + return 1; +} +// Function finding zero offsets of the ADC +void LowsideCurrentSense::calibrateOffsets(){ + const int calibration_rounds = 2000; + + // find adc offset = zero current voltage + offset_ia = 0; + offset_ib = 0; + offset_ic = 0; + // read the adc voltage 1000 times ( arbitrary number ) + for (int i = 0; i < calibration_rounds; i++) { + _startADC3PinConversionLowSide(); + if(_isset(pinA)) offset_ia += (_readADCVoltageLowSide(pinA, params)); + if(_isset(pinB)) offset_ib += (_readADCVoltageLowSide(pinB, params)); + if(_isset(pinC)) offset_ic += (_readADCVoltageLowSide(pinC, params)); + _delay(1); + } + // calculate the mean offsets + if(_isset(pinA)) offset_ia = offset_ia / calibration_rounds; + if(_isset(pinB)) offset_ib = offset_ib / calibration_rounds; + if(_isset(pinC)) offset_ic = offset_ic / calibration_rounds; +} + +// read all three phase currents (if possible 2 or 3) +PhaseCurrent_s LowsideCurrentSense::getPhaseCurrents(){ + PhaseCurrent_s current; + _startADC3PinConversionLowSide(); + current.a = (!_isset(pinA)) ? 0 : (_readADCVoltageLowSide(pinA, params) - offset_ia)*gain_a;// amps + current.b = (!_isset(pinB)) ? 0 : (_readADCVoltageLowSide(pinB, params) - offset_ib)*gain_b;// amps + current.c = (!_isset(pinC)) ? 0 : (_readADCVoltageLowSide(pinC, params) - offset_ic)*gain_c; // amps + return current; +} + +// Function aligning the current sense with motor driver +// if all pins are connected well none of this is really necessary! - can be avoided +// returns flag +// 0 - fail +// 1 - success and nothing changed +// 2 - success but pins reconfigured +// 3 - success but gains inverted +// 4 - success but pins reconfigured and gains inverted +int LowsideCurrentSense::driverAlign(float voltage){ + + int exit_flag = 1; + if(skip_align) return exit_flag; + + if (!initialized) return 0; + + if(_isset(pinA)){ + // set phase A active and phases B and C down + driver->setPwm(voltage, 0, 0); + _delay(2000); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 100 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6f + 0.4f*c1.a; + c.b = c.b*0.6f + 0.4f*c1.b; + c.c = c.c*0.6f + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + // align phase A + float ab_ratio = c.b ? fabs(c.a / c.b) : 0; + float ac_ratio = c.c ? fabs(c.a / c.c) : 0; + if(_isset(pinB) && ab_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinC) && ac_ratio > 1.5f ){ // should be ~2 + gain_a *= _sign(c.a); + }else if(_isset(pinB) && ab_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and B + int tmp_pinA = pinA; + pinA = pinB; + pinB = tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ib; + offset_ib = tmp_offsetA; + gain_a *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && ac_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinA = pinA; + pinA = pinC; + pinC= tmp_pinA; + float tmp_offsetA = offset_ia; + offset_ia = offset_ic; + offset_ic = tmp_offsetA; + gain_a *= _sign(c.c); + exit_flag = 2;// signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(_isset(pinB)){ + // set phase B active and phases A and C down + driver->setPwm(0, voltage, 0); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the current 50 times + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ba_ratio = c.a ? fabs(c.b / c.a) : 0; + float bc_ratio = c.c ? fabs(c.b / c.c) : 0; + if(_isset(pinA) && ba_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinC) && bc_ratio > 1.5f ){ // should be ~2 + gain_b *= _sign(c.b); + }else if(_isset(pinA) && ba_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and B + int tmp_pinB = pinB; + pinB = pinA; + pinA = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ia; + offset_ia = tmp_offsetB; + gain_b *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinC) && bc_ratio < 0.7f ){ // should be ~0.5 + // switch phase A and C + int tmp_pinB = pinB; + pinB = pinC; + pinC = tmp_pinB; + float tmp_offsetB = offset_ib; + offset_ib = offset_ic; + offset_ic = tmp_offsetB; + gain_b *= _sign(c.c); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + // if phase C measured + if(_isset(pinC)){ + // set phase C active and phases A and B down + driver->setPwm(0, 0, voltage); + _delay(200); + PhaseCurrent_s c = getPhaseCurrents(); + // read the adc voltage 500 times ( arbitrary number ) + for (int i = 0; i < 100; i++) { + PhaseCurrent_s c1 = getPhaseCurrents(); + c.a = c.a*0.6 + 0.4f*c1.a; + c.b = c.b*0.6 + 0.4f*c1.b; + c.c = c.c*0.6 + 0.4f*c1.c; + _delay(3); + } + driver->setPwm(0, 0, 0); + float ca_ratio = c.a ? fabs(c.c / c.a) : 0; + float cb_ratio = c.b ? fabs(c.c / c.b) : 0; + if(_isset(pinA) && ca_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinB) && cb_ratio > 1.5f ){ // should be ~2 + gain_c *= _sign(c.c); + }else if(_isset(pinA) && ca_ratio < 0.7f ){ // it should be ~0.5 + // switch phase A and C + int tmp_pinC = pinC; + pinC = pinA; + pinA = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ia; + offset_ia = tmp_offsetC; + gain_c *= _sign(c.a); + exit_flag = 2; // signal that pins have been switched + }else if(_isset(pinB) && cb_ratio < 0.7f ){ // should be ~0.5 + // switch phase B and C + int tmp_pinC = pinC; + pinC = pinB; + pinB = tmp_pinC; + float tmp_offsetC = offset_ic; + offset_ic = offset_ib; + offset_ib = tmp_offsetC; + gain_c *= _sign(c.b); + exit_flag = 2; // signal that pins have been switched + }else{ + // error in current sense - phase either not measured or bad connection + return 0; + } + } + + if(gain_a < 0 || gain_b < 0 || gain_c < 0) exit_flag +=2; + // exit flag is either + // 0 - fail + // 1 - success and nothing changed + // 2 - success but pins reconfigured + // 3 - success but gains inverted + // 4 - success but pins reconfigured and gains inverted + + return exit_flag; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.h new file mode 100644 index 0000000..1652b33 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/LowsideCurrentSense.h @@ -0,0 +1,73 @@ +#ifndef LOWSIDE_CS_LIB_H +#define LOWSIDE_CS_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "../common/base_classes/CurrentSense.h" +#include "../common/base_classes/FOCMotor.h" +#include "../common/lowpass_filter.h" +#include "hardware_api.h" + + +class LowsideCurrentSense: public CurrentSense{ + public: + /** + LowsideCurrentSense class constructor + @param shunt_resistor shunt resistor value + @param gain current-sense op-amp gain + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + LowsideCurrentSense(float shunt_resistor, float gain, int pinA, int pinB, int pinC = _NC); + /** + LowsideCurrentSense class constructor + @param mVpA mV per Amp ratio + @param phA A phase adc pin + @param phB B phase adc pin + @param phC C phase adc pin (optional) + */ + LowsideCurrentSense(float mVpA, int pinA, int pinB, int pinC = _NC); + + // CurrentSense interface implementing functions + int init() override; + PhaseCurrent_s getPhaseCurrents() override; + int driverAlign(float align_voltage) override; + + // ADC measuremnet gain for each phase + // support for different gains for different phases of more commonly - inverted phase currents + // this should be automated later + float gain_a; //!< phase A gain + float gain_b; //!< phase B gain + float gain_c; //!< phase C gain + + // // per phase low pass fileters + // LowPassFilter lpf_a{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current A low pass filter + // LowPassFilter lpf_b{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current B low pass filter + // LowPassFilter lpf_c{DEF_LPF_PER_PHASE_CURRENT_SENSE_Tf}; //!< current C low pass filter + + float offset_ia; //!< zero current A voltage value (center of the adc reading) + float offset_ib; //!< zero current B voltage value (center of the adc reading) + float offset_ic; //!< zero current C voltage value (center of the adc reading) + private: + + // hardware variables + int pinA; //!< pin A analog pin for current measurement + int pinB; //!< pin B analog pin for current measurement + int pinC; //!< pin C analog pin for current measurement + + // gain variables + float shunt_resistor; //!< Shunt resistor value + float amp_gain; //!< amp gain value + float volts_to_amps_ratio; //!< Volts to amps ratio + + /** + * Function finding zero offsets of the ADC + */ + void calibrateOffsets(); + +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_api.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_api.h new file mode 100644 index 0000000..7862b70 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_api.h @@ -0,0 +1,65 @@ +#ifndef HARDWARE_UTILS_CURRENT_H +#define HARDWARE_UTILS_CURRENT_H + +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// flag returned if current sense init fails +#define SIMPLEFOC_CURRENT_SENSE_INIT_FAILED ((void*)-1) + +// generic implementation of the hardware specific structure +// containing all the necessary current sense parameters +// will be returned as a void pointer from the _configureADCx functions +// will be provided to the _readADCVoltageX() as a void pointer +typedef struct GenericCurrentSenseParams { + int pins[3]; + float adc_voltage_conv; +} GenericCurrentSenseParams; + + +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + * @param cs_params -current sense parameter structure - hardware specific + */ +float _readADCVoltageInline(const int pinA, const void* cs_params); + +/** + * function reading an ADC value and returning the read voltage + * + * @param driver_params - driver parameter structure - hardware specific + * @param pinA - adc pin A + * @param pinB - adc pin B + * @param pinC - adc pin C + */ +void* _configureADCInline(const void *driver_params, const int pinA,const int pinB,const int pinC = NOT_SET); + +/** + * function reading an ADC value and returning the read voltage + * + * @param driver_params - driver parameter structure - hardware specific + * @param pinA - adc pin A + * @param pinB - adc pin B + * @param pinC - adc pin C + */ +void* _configureADCLowSide(const void *driver_params, const int pinA,const int pinB,const int pinC = NOT_SET); + +void _startADC3PinConversionLowSide(); + +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + * @param cs_params -current sense parameter structure - hardware specific + */ +float _readADCVoltageLowSide(const int pinA, const void* cs_params); + +/** + * function syncing the Driver with the ADC for the LowSide Sensing + * @param driver_params - driver parameter structure - hardware specific + * @param cs_params - current sense parameter structure - hardware specific + */ +void _driverSyncLowSide(void* driver_params, void* cs_params); + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp new file mode 100644 index 0000000..50ae596 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/atmega_mcu.cpp @@ -0,0 +1,40 @@ +#include "../hardware_api.h" + +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) + +#define _ADC_VOLTAGE 5.0f +#define _ADC_RESOLUTION 1024.0f + +#ifndef cbi + #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) +#endif +#ifndef sbi + #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) +#endif + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + // set hight frequency adc - ADPS2,ADPS1,ADPS0 | 001 (16mhz/2) | 010 ( 16mhz/4 ) | 011 (16mhz/8) | 100(16mhz/16) | 101 (16mhz/32) | 110 (16mhz/64) | 111 (16mhz/128 default) + // set divisor to 8 - adc frequency 16mhz/8 = 2 mhz + // arduino takes 25 conversions per sample so - 2mhz/25 = 80k samples per second - 12.5us per sample + cbi(ADCSRA, ADPS2); + sbi(ADCSRA, ADPS1); + sbi(ADCSRA, ADPS0); + + return params; +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp new file mode 100644 index 0000000..ce58cd9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/due_mcu.cpp @@ -0,0 +1,27 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(__SAM3X8E__) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp new file mode 100644 index 0000000..807c387 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.cpp @@ -0,0 +1,259 @@ +#include "esp32_adc_driver.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "rom/ets_sys.h" +#include "esp_attr.h" +#include "esp_intr.h" +#include "soc/rtc_io_reg.h" +#include "soc/rtc_cntl_reg.h" +#include "soc/sens_reg.h" + +static uint8_t __analogAttenuation = 3;//11db +static uint8_t __analogWidth = 3;//12 bits +static uint8_t __analogCycles = 8; +static uint8_t __analogSamples = 0;//1 sample +static uint8_t __analogClockDiv = 1; + +// Width of returned answer () +static uint8_t __analogReturnedWidth = 12; + +void __analogSetWidth(uint8_t bits){ + if(bits < 9){ + bits = 9; + } else if(bits > 12){ + bits = 12; + } + __analogReturnedWidth = bits; + __analogWidth = bits - 9; + SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR1_BIT_WIDTH, __analogWidth, SENS_SAR1_BIT_WIDTH_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_BIT, __analogWidth, SENS_SAR1_SAMPLE_BIT_S); + + SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR2_BIT_WIDTH, __analogWidth, SENS_SAR2_BIT_WIDTH_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_BIT, __analogWidth, SENS_SAR2_SAMPLE_BIT_S); +} + +void __analogSetCycles(uint8_t cycles){ + __analogCycles = cycles; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_CYCLE, __analogCycles, SENS_SAR1_SAMPLE_CYCLE_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_CYCLE, __analogCycles, SENS_SAR2_SAMPLE_CYCLE_S); +} + +void __analogSetSamples(uint8_t samples){ + if(!samples){ + return; + } + __analogSamples = samples - 1; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_SAMPLE_NUM, __analogSamples, SENS_SAR1_SAMPLE_NUM_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_SAMPLE_NUM, __analogSamples, SENS_SAR2_SAMPLE_NUM_S); +} + +void __analogSetClockDiv(uint8_t clockDiv){ + if(!clockDiv){ + return; + } + __analogClockDiv = clockDiv; + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL_REG, SENS_SAR1_CLK_DIV, __analogClockDiv, SENS_SAR1_CLK_DIV_S); + SET_PERI_REG_BITS(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_CLK_DIV, __analogClockDiv, SENS_SAR2_CLK_DIV_S); +} + +void __analogSetAttenuation(uint8_t attenuation) +{ + __analogAttenuation = attenuation & 3; + uint32_t att_data = 0; + int i = 10; + while(i--){ + att_data |= __analogAttenuation << (i * 2); + } + WRITE_PERI_REG(SENS_SAR_ATTEN1_REG, att_data & 0xFFFF);//ADC1 has 8 channels + WRITE_PERI_REG(SENS_SAR_ATTEN2_REG, att_data); +} + +void IRAM_ATTR __analogInit(){ + static bool initialized = false; + if(initialized){ + return; + } + + __analogSetAttenuation(__analogAttenuation); + __analogSetCycles(__analogCycles); + __analogSetSamples(__analogSamples + 1);//in samples + __analogSetClockDiv(__analogClockDiv); + __analogSetWidth(__analogWidth + 9);//in bits + + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL_REG, SENS_SAR1_DATA_INV); + SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV); + + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_FORCE_M); //SAR ADC1 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD_FORCE_M); //SAR ADC1 pad enable bitmap is controlled by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_FORCE_M); //SAR ADC2 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD_FORCE_M); //SAR ADC2 pad enable bitmap is controlled by SW + + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR_M); //force XPD_SAR=0, use XPD_FSM + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_AMP, 0x2, SENS_FORCE_XPD_AMP_S); //force XPD_AMP=0 + + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_CTRL_REG, 0xfff << SENS_AMP_RST_FB_FSM_S); //clear FSM + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT1_REG, SENS_SAR_AMP_WAIT1, 0x1, SENS_SAR_AMP_WAIT1_S); + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT1_REG, SENS_SAR_AMP_WAIT2, 0x1, SENS_SAR_AMP_WAIT2_S); + SET_PERI_REG_BITS(SENS_SAR_MEAS_WAIT2_REG, SENS_SAR_AMP_WAIT3, 0x1, SENS_SAR_AMP_WAIT3_S); + while (GET_PERI_REG_BITS2(SENS_SAR_SLAVE_ADDR1_REG, 0x7, SENS_MEAS_STATUS_S) != 0); //wait det_fsm== + + initialized = true; +} + +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0 || attenuation > 3){ + return ; + } + __analogInit(); + if(channel > 7){ + SET_PERI_REG_BITS(SENS_SAR_ATTEN2_REG, 3, attenuation, ((channel - 10) * 2)); + } else { + SET_PERI_REG_BITS(SENS_SAR_ATTEN1_REG, 3, attenuation, (channel * 2)); + } +} + +bool IRAM_ATTR __adcAttachPin(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + int8_t pad = digitalPinToTouchChannel(pin); + if(pad >= 0){ + uint32_t touch = READ_PERI_REG(SENS_SAR_TOUCH_ENABLE_REG); + if(touch & (1 << pad)){ + touch &= ~((1 << (pad + SENS_TOUCH_PAD_OUTEN2_S)) + | (1 << (pad + SENS_TOUCH_PAD_OUTEN1_S)) + | (1 << (pad + SENS_TOUCH_PAD_WORKEN_S))); + WRITE_PERI_REG(SENS_SAR_TOUCH_ENABLE_REG, touch); + } + } else if(pin == 25){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC | RTC_IO_PDAC1_DAC_XPD_FORCE); //stop dac1 + } else if(pin == 26){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC2_REG, RTC_IO_PDAC2_XPD_DAC | RTC_IO_PDAC2_DAC_XPD_FORCE); //stop dac2 + } + + pinMode(pin, ANALOG); + + __analogInit(); + return true; +} + +bool IRAM_ATTR __adcStart(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + } + return true; +} + +bool IRAM_ATTR __adcBusy(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 7){ + return (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); + } + return (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); +} + +uint16_t IRAM_ATTR __adcEnd(uint8_t pin) +{ + + uint16_t value = 0; + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return 0;//not adc pin + } + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + +void __analogReadResolution(uint8_t bits) +{ + if(!bits || bits > 16){ + return; + } + __analogSetWidth(bits); // hadware from 9 to 12 + __analogReturnedWidth = bits; // software from 1 to 16 +} + +uint16_t IRAM_ATTR adcRead(uint8_t pin) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + __analogInit(); + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS_START1_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_START_SAR_M); + } + + uint16_t value = 0; + + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS_START1_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h new file mode 100644 index 0000000..869c4dd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_adc_driver.h @@ -0,0 +1,88 @@ + + +#ifndef SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ +#define SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ + +#include "Arduino.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) +/* + * Get ADC value for pin + * */ +uint16_t adcRead(uint8_t pin); + +/* + * Set the resolution of analogRead return values. Default is 12 bits (range from 0 to 4096). + * If between 9 and 12, it will equal the set hardware resolution, else value will be shifted. + * Range is 1 - 16 + * + * Note: compatibility with Arduino SAM + */ +void __analogReadResolution(uint8_t bits); + +/* + * Sets the sample bits and read resolution + * Default is 12bit (0 - 4095) + * Range is 9 - 12 + * */ +void __analogSetWidth(uint8_t bits); + +/* + * Set number of cycles per sample + * Default is 8 and seems to do well + * Range is 1 - 255 + * */ +void __analogSetCycles(uint8_t cycles); + +/* + * Set number of samples in the range. + * Default is 1 + * Range is 1 - 255 + * This setting splits the range into + * "samples" pieces, which could look + * like the sensitivity has been multiplied + * that many times + * */ +void __analogSetSamples(uint8_t samples); + +/* + * Set the divider for the ADC clock. + * Default is 1 + * Range is 1 - 255 + * */ +void __analogSetClockDiv(uint8_t clockDiv); + +/* + * Set the attenuation for all channels + * Default is 11db + * */ +void __analogSetAttenuation(uint8_t attenuation); + +/* + * Set the attenuation for particular pin + * Default is 11db + * */ +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation); + +/* + * Attach pin to ADC (will also clear any other analog mode that could be on) + * */ +bool __adcAttachPin(uint8_t pin); + +/* + * Start ADC conversion on attached pin's bus + * */ +bool __adcStart(uint8_t pin); + +/* + * Check if conversion on the pin's ADC bus is currently running + * */ +bool __adcBusy(uint8_t pin); + +/* + * Get the result of the conversion (will wait if it have not finished) + * */ +uint16_t __adcEnd(uint8_t pin); + +#endif /* SIMPLEFOC_ESP32_HAL_ADC_DRIVER_H_ */ +#endif /* ESP32 */ \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp new file mode 100644 index 0000000..f2d65f8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_ledc_mcu.cpp @@ -0,0 +1,27 @@ +#include "../../hardware_api.h" +#include "../../../drivers/hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && !defined(SOC_MCPWM_SUPPORTED) + +#include "esp32_adc_driver.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4095.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp new file mode 100644 index 0000000..3df9dff --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32_mcu.cpp @@ -0,0 +1,162 @@ +#include "../../hardware_api.h" +#include "../../../drivers/hardware_api.h" +#include "../../../drivers/hardware_specific/esp32/esp32_driver_mcpwm.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + +#include "esp32_adc_driver.h" + +#include "driver/mcpwm.h" +#include "soc/mcpwm_reg.h" +#include "soc/mcpwm_struct.h" + +#include +#include + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4095.0f + + +typedef struct ESP32MCPWMCurrentSenseParams { + int pins[3]; + float adc_voltage_conv; + mcpwm_unit_t mcpwm_unit; + int buffer_index; +} ESP32MCPWMCurrentSenseParams; + + +/** + * Inline adc reading implementation +*/ +// function reading an ADC value and returning the read voltage +float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = adcRead(pinA); + return raw_adc * ((ESP32MCPWMCurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA, const int pinB, const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + ESP32MCPWMCurrentSenseParams* params = new ESP32MCPWMCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + + + +/** + * Low side adc reading implementation +*/ + +static void IRAM_ATTR mcpwm0_isr_handler(void*); +static void IRAM_ATTR mcpwm1_isr_handler(void*); +byte currentState = 1; +// two mcpwm units +// - max 2 motors per mcpwm unit (6 adc channels) +int adc_pins[2][6]={0}; +int adc_pin_count[2]={0}; +uint32_t adc_buffer[2][6]={0}; +int adc_read_index[2]={0}; + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + mcpwm_unit_t unit = ((ESP32MCPWMCurrentSenseParams*)cs_params)->mcpwm_unit; + int buffer_index = ((ESP32MCPWMCurrentSenseParams*)cs_params)->buffer_index; + float adc_voltage_conv = ((ESP32MCPWMCurrentSenseParams*)cs_params)->adc_voltage_conv; + + for(int i=0; i < adc_pin_count[unit]; i++){ + if( pin == ((ESP32MCPWMCurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_buffer[unit][buffer_index + i] * adc_voltage_conv; + } + // not found + return 0; +} + +// function configuring low-side current sensing +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + + mcpwm_unit_t unit = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_unit; + int index_start = adc_pin_count[unit]; + if( _isset(pinA) ) adc_pins[unit][adc_pin_count[unit]++] = pinA; + if( _isset(pinB) ) adc_pins[unit][adc_pin_count[unit]++] = pinB; + if( _isset(pinC) ) adc_pins[unit][adc_pin_count[unit]++] = pinC; + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + ESP32MCPWMCurrentSenseParams* params = new ESP32MCPWMCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION), + .mcpwm_unit = unit, + .buffer_index = index_start + }; + + return params; +} + + +void _driverSyncLowSide(void* driver_params, void* cs_params){ + + mcpwm_dev_t* mcpwm_dev = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_dev; + mcpwm_unit_t mcpwm_unit = ((ESP32MCPWMDriverParams*)driver_params)->mcpwm_unit; + + // low-side register enable interrupt + mcpwm_dev->int_ena.timer0_tep_int_ena = true;//A PWM timer 0 TEP event will trigger this interrupt + // high side registers enable interrupt + //mcpwm_dev->int_ena.timer0_tep_int_ena = true;//A PWM timer 0 TEZ event will trigger this interrupt + + // register interrupts (mcpwm number, interrupt handler, handler argument = NULL, interrupt signal/flag, return handler = NULL) + if(mcpwm_unit == MCPWM_UNIT_0) + mcpwm_isr_register(mcpwm_unit, mcpwm0_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL); //Set ISR Handler + else + mcpwm_isr_register(mcpwm_unit, mcpwm1_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL); //Set ISR Handler +} + +// Read currents when interrupt is triggered +static void IRAM_ATTR mcpwm0_isr_handler(void*){ + // // high side + // uint32_t mcpwm_intr_status = MCPWM0.int_st.timer0_tez_int_st; + + // low side + uint32_t mcpwm_intr_status = MCPWM0.int_st.timer0_tep_int_st; + if(mcpwm_intr_status){ + adc_buffer[0][adc_read_index[0]] = adcRead(adc_pins[0][adc_read_index[0]]); + adc_read_index[0]++; + if(adc_read_index[0] == adc_pin_count[0]) adc_read_index[0] = 0; + } + // low side + MCPWM0.int_clr.timer0_tep_int_clr = mcpwm_intr_status; + // high side + // MCPWM0.int_clr.timer0_tez_int_clr = mcpwm_intr_status_0; +} + + +// Read currents when interrupt is triggered +static void IRAM_ATTR mcpwm1_isr_handler(void*){ + // // high side + // uint32_t mcpwm_intr_status = MCPWM1.int_st.timer0_tez_int_st; + + // low side + uint32_t mcpwm_intr_status = MCPWM1.int_st.timer0_tep_int_st; + if(mcpwm_intr_status){ + adc_buffer[1][adc_read_index[1]] = adcRead(adc_pins[1][adc_read_index[1]]); + adc_read_index[1]++; + if(adc_read_index[1] == adc_pin_count[1]) adc_read_index[1] = 0; + } + // low side + MCPWM1.int_clr.timer0_tep_int_clr = mcpwm_intr_status; + // high side + // MCPWM1.int_clr.timer0_tez_int_clr = mcpwm_intr_status_0; +} + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp new file mode 100644 index 0000000..a2f58ac --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/esp32/esp32s_adc_driver.cpp @@ -0,0 +1,258 @@ +#include "esp32_adc_driver.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "rom/ets_sys.h" +#include "esp_attr.h" +//#include "esp_intr.h" // deprecated +#include "esp_intr_alloc.h" +#include "soc/rtc_io_reg.h" +#include "soc/rtc_cntl_reg.h" +#include "soc/sens_reg.h" + +static uint8_t __analogAttenuation = 3;//11db +static uint8_t __analogWidth = 3;//12 bits +static uint8_t __analogCycles = 8; +static uint8_t __analogSamples = 0;//1 sample +static uint8_t __analogClockDiv = 1; + +// Width of returned answer () +static uint8_t __analogReturnedWidth = 12; + +void __analogSetWidth(uint8_t bits){ + if(bits < 9){ + bits = 9; + } else if(bits > 12){ + bits = 12; + } + __analogReturnedWidth = bits; + __analogWidth = bits - 9; + // SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR1_BIT_WIDTH, __analogWidth, SENS_SAR1_BIT_WIDTH_S); + // SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_BIT, __analogWidth, SENS_SAR1_SAMPLE_BIT_S); + + // SET_PERI_REG_BITS(SENS_SAR_START_FORCE_REG, SENS_SAR2_BIT_WIDTH, __analogWidth, SENS_SAR2_BIT_WIDTH_S); + // SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_BIT, __analogWidth, SENS_SAR2_SAMPLE_BIT_S); +} + +void __analogSetCycles(uint8_t cycles){ + __analogCycles = cycles; + // SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_CYCLE, __analogCycles, SENS_SAR1_SAMPLE_CYCLE_S); + // SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_CYCLE, __analogCycles, SENS_SAR2_SAMPLE_CYCLE_S); +} + +void __analogSetSamples(uint8_t samples){ + if(!samples){ + return; + } + __analogSamples = samples - 1; + SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_SAMPLE_NUM, __analogSamples, SENS_SAR1_SAMPLE_NUM_S); + SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_SAMPLE_NUM, __analogSamples, SENS_SAR2_SAMPLE_NUM_S); +} + +void __analogSetClockDiv(uint8_t clockDiv){ + if(!clockDiv){ + return; + } + __analogClockDiv = clockDiv; + SET_PERI_REG_BITS(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_CLK_DIV, __analogClockDiv, SENS_SAR1_CLK_DIV_S); + SET_PERI_REG_BITS(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_CLK_DIV, __analogClockDiv, SENS_SAR2_CLK_DIV_S); +} + +void __analogSetAttenuation(uint8_t attenuation) +{ + __analogAttenuation = attenuation & 3; + uint32_t att_data = 0; + int i = 10; + while(i--){ + att_data |= __analogAttenuation << (i * 2); + } + WRITE_PERI_REG(SENS_SAR_ATTEN1_REG, att_data & 0xFFFF);//ADC1 has 8 channels + WRITE_PERI_REG(SENS_SAR_ATTEN2_REG, att_data); +} + +void IRAM_ATTR __analogInit(){ + static bool initialized = false; + if(initialized){ + return; + } + + __analogSetAttenuation(__analogAttenuation); + __analogSetCycles(__analogCycles); + __analogSetSamples(__analogSamples + 1);//in samples + __analogSetClockDiv(__analogClockDiv); + __analogSetWidth(__analogWidth + 9);//in bits + + SET_PERI_REG_MASK(SENS_SAR_READER1_CTRL_REG, SENS_SAR1_DATA_INV); + SET_PERI_REG_MASK(SENS_SAR_READER2_CTRL_REG, SENS_SAR2_DATA_INV); + + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_FORCE_M); //SAR ADC1 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD_FORCE_M); //SAR ADC1 pad enable bitmap is controlled by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_FORCE_M); //SAR ADC2 controller (in RTC) is started by SW + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD_FORCE_M); //SAR ADC2 pad enable bitmap is controlled by SW + + CLEAR_PERI_REG_MASK(SENS_SAR_POWER_XPD_SAR_REG, SENS_FORCE_XPD_SAR_M); //force XPD_SAR=0, use XPD_FSM + SET_PERI_REG_BITS(SENS_SAR_POWER_XPD_SAR_REG, SENS_FORCE_XPD_AMP, 0x2, SENS_FORCE_XPD_AMP_S); //force XPD_AMP=0 + + CLEAR_PERI_REG_MASK(SENS_SAR_AMP_CTRL3_REG, 0xfff << SENS_AMP_RST_FB_FSM_S); //clear FSM + SET_PERI_REG_BITS(SENS_SAR_AMP_CTRL1_REG, SENS_SAR_AMP_WAIT1, 0x1, SENS_SAR_AMP_WAIT1_S); + SET_PERI_REG_BITS(SENS_SAR_AMP_CTRL1_REG, SENS_SAR_AMP_WAIT2, 0x1, SENS_SAR_AMP_WAIT2_S); + SET_PERI_REG_BITS(SENS_SAR_POWER_XPD_SAR_REG, SENS_SAR_AMP_WAIT3, 0x1, SENS_SAR_AMP_WAIT3_S); + while (GET_PERI_REG_BITS2(SENS_SAR_SLAVE_ADDR1_REG, 0x7, SENS_SARADC_MEAS_STATUS_S) != 0); //wait det_fsm== + + initialized = true; +} + +void __analogSetPinAttenuation(uint8_t pin, uint8_t attenuation) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0 || attenuation > 3){ + return ; + } + __analogInit(); + if(channel > 7){ + SET_PERI_REG_BITS(SENS_SAR_ATTEN2_REG, 3, attenuation, ((channel - 10) * 2)); + } else { + SET_PERI_REG_BITS(SENS_SAR_ATTEN1_REG, 3, attenuation, (channel * 2)); + } +} + +bool IRAM_ATTR __adcAttachPin(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + int8_t pad = digitalPinToTouchChannel(pin); + if(pad >= 0){ + uint32_t touch = READ_PERI_REG(SENS_SAR_TOUCH_CONF_REG); + if(touch & (1 << pad)){ + touch &= ~((1 << (pad + SENS_TOUCH_OUTEN_S))); + WRITE_PERI_REG(SENS_SAR_TOUCH_CONF_REG, touch); + } + } else if(pin == 25){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC1_REG, RTC_IO_PDAC1_XPD_DAC | RTC_IO_PDAC1_DAC_XPD_FORCE); //stop dac1 + } else if(pin == 26){ + CLEAR_PERI_REG_MASK(RTC_IO_PAD_DAC2_REG, RTC_IO_PDAC2_XPD_DAC | RTC_IO_PDAC2_DAC_XPD_FORCE); //stop dac2 + } + + pinMode(pin, ANALOG); + + __analogInit(); + return true; +} + +bool IRAM_ATTR __adcStart(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + } + return true; +} + +bool IRAM_ATTR __adcBusy(uint8_t pin){ + + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + if(channel > 7){ + return (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); + } + return (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); +} + +uint16_t IRAM_ATTR __adcEnd(uint8_t pin) +{ + + uint16_t value = 0; + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return 0;//not adc pin + } + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + +void __analogReadResolution(uint8_t bits) +{ + if(!bits || bits > 16){ + return; + } + __analogSetWidth(bits); // hadware from 9 to 12 + __analogReturnedWidth = bits; // software from 1 to 16 +} + +uint16_t IRAM_ATTR adcRead(uint8_t pin) +{ + int8_t channel = digitalPinToAnalogChannel(pin); + if(channel < 0){ + return false;//not adc pin + } + + __analogInit(); + + if(channel > 9){ + channel -= 10; + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS2_CTRL2_REG, SENS_SAR2_EN_PAD, (1 << channel), SENS_SAR2_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_START_SAR_M); + } else { + CLEAR_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + SET_PERI_REG_BITS(SENS_SAR_MEAS1_CTRL2_REG, SENS_SAR1_EN_PAD, (1 << channel), SENS_SAR1_EN_PAD_S); + SET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_START_SAR_M); + } + + uint16_t value = 0; + + if(channel > 7){ + while (GET_PERI_REG_MASK(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS2_CTRL2_REG, SENS_MEAS2_DATA_SAR, SENS_MEAS2_DATA_SAR_S); + } else { + while (GET_PERI_REG_MASK(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DONE_SAR) == 0); //wait for conversion + value = GET_PERI_REG_BITS2(SENS_SAR_MEAS1_CTRL2_REG, SENS_MEAS1_DATA_SAR, SENS_MEAS1_DATA_SAR_S); + } + + // Shift result if necessary + uint8_t from = __analogWidth + 9; + if (from == __analogReturnedWidth) { + return value; + } + if (from > __analogReturnedWidth) { + return value >> (from - __analogReturnedWidth); + } + return value << (__analogReturnedWidth - from); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp new file mode 100644 index 0000000..dec7205 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/generic_mcu.cpp @@ -0,0 +1,41 @@ +#include "../hardware_api.h" + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = analogRead(pinA); + return raw_adc * ((GenericCurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (5.0f)/(1024.0f) + }; + + return params; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageLowSide(const int pinA, const void* cs_params){ + return _readADCVoltageInline(pinA, cs_params); +} + +// Configure low side for generic mcu +// cannot do much but +__attribute__((weak)) void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + return _configureADCInline(driver_params, pinA, pinB, pinC); +} + +// sync driver and the adc +__attribute__((weak)) void _driverSyncLowSide(void* driver_params, void* cs_params){ + _UNUSED(driver_params); + _UNUSED(cs_params); +} +__attribute__((weak)) void _startADC3PinConversionLowSide(){ } diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp new file mode 100644 index 0000000..d84c2fd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.cpp @@ -0,0 +1,265 @@ + +#if defined(TARGET_RP2040) + + +#include "../../hardware_api.h" +#include "./rp2040_mcu.h" +#include "../../../drivers/hardware_specific/rp2040/rp2040_mcu.h" +#include "communication/SimpleFOCDebug.h" + +#include "hardware/dma.h" +#include "hardware/irq.h" +#include "hardware/pwm.h" +#include "hardware/adc.h" + + +/* Singleton instance of the ADC engine */ +RP2040ADCEngine engine; + +alignas(32) const uint32_t trigger_value = ADC_CS_START_ONCE_BITS; // start once + +/* Hardware API implementation */ + +float _readADCVoltageInline(const int pinA, const void* cs_params) { + // not super-happy with this. Here we have to return 1 phase current at a time, when actually we want to + // return readings from the same ADC conversion run. The ADC on RP2040 is anyway in round robin mode :-( + // like this we either have to block interrupts, or of course have the chance of reading across + // new ADC conversions, which probably won't improve the accuracy. + _UNUSED(cs_params); + + if (pinA>=26 && pinA<=29 && engine.channelsEnabled[pinA-26]) { + return engine.lastResults.raw[pinA-26]*engine.adc_conv; + } + + // otherwise return NaN + return NAN; +}; + + +void* _configureADCInline(const void *driver_params, const int pinA, const int pinB, const int pinC) { + _UNUSED(driver_params); + + if( _isset(pinA) ) + engine.addPin(pinA); + if( _isset(pinB) ) + engine.addPin(pinB); + if( _isset(pinC) ) + engine.addPin(pinC); + engine.init(); // TODO this has to happen later if we want to support more than one motor... + engine.start(); + return &engine; +}; + + +// not supported at the moment +// void* _configureADCLowSide(const void *driver_params, const int pinA, const int pinB, const int pinC) { +// if( _isset(pinA) ) +// engine.addPin(pinA); +// if( _isset(pinB) ) +// engine.addPin(pinB); +// if( _isset(pinC) ) +// engine.addPin(pinC); +// engine.setPWMTrigger(((RP2040DriverParams*)driver_params)->slice[0]); +// engine.init(); +// engine.start(); +// return &engine; +// }; + + +// void _startADC3PinConversionLowSide() { +// // what is this for? +// }; + + +// float _readADCVoltageLowSide(const int pinA, const void* cs_params) { +// // not super-happy with this. Here we have to return 1 phase current at a time, when actually we want to +// // return readings from the same ADC conversion run. The ADC on RP2040 is anyway in round robin mode :-( +// // like this we have block interrupts 3x instead of just once, and of course have the chance of reading across +// // new ADC conversions, which probably won't improve the accuracy. + +// if (pinA>=26 && pinA<=29 && engine.channelsEnabled[pinA-26]) { +// return engine.lastResults[pinA-26]*engine.adc_conv; +// } + +// // otherwise return NaN +// return NAN; +// }; + + +// void _driverSyncLowSide(void* driver_params, void* cs_params) { +// // nothing to do +// }; + + + +volatile int rp2040_intcount = 0; + +void _adcConversionFinishedHandler() { + // conversion of all channels finished. copy results. + volatile uint8_t* from = engine.samples; + if (engine.channelsEnabled[0]) + engine.lastResults.raw[0] = (*from++); + if (engine.channelsEnabled[1]) + engine.lastResults.raw[1] = (*from++); + if (engine.channelsEnabled[2]) + engine.lastResults.raw[2] = (*from++); + if (engine.channelsEnabled[3]) + engine.lastResults.raw[3] = (*from++); + //dma_channel_acknowledge_irq0(engine.readDMAChannel); + dma_hw->ints0 = 1u << engine.readDMAChannel; + //dma_start_channel_mask( (1u << engine.readDMAChannel) ); + dma_channel_set_write_addr(engine.readDMAChannel, engine.samples, true); + // if (engine.triggerPWMSlice>=0) + // dma_channel_set_trans_count(engine.triggerDMAChannel, 1, true); + rp2040_intcount++; +}; + + + +/* ADC engine implementation */ + + +RP2040ADCEngine::RP2040ADCEngine() { + channelsEnabled[0] = false; + channelsEnabled[1] = false; + channelsEnabled[2] = false; + channelsEnabled[3] = false; + initialized = false; +}; + + + +void RP2040ADCEngine::addPin(int pin) { + if (pin>=26 && pin<=29) + channelsEnabled[pin-26] = true; + else + SIMPLEFOC_DEBUG("RP2040-CUR: ERR: Not an ADC pin: ", pin); +}; + + + +// void RP2040ADCEngine::setPWMTrigger(uint slice){ +// triggerPWMSlice = slice; +// }; + + + + +bool RP2040ADCEngine::init() { + if (initialized) + return true; + + adc_init(); + int enableMask = 0x00; + int channelCount = 0; + for (int i = 3; i>=0; i--) { + if (channelsEnabled[i]){ + adc_gpio_init(i+26); + enableMask |= (0x01<=500000) { + samples_per_second = 0; + adc_set_clkdiv(0); + } + else + adc_set_clkdiv(48000000/samples_per_second); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC init"); + + readDMAChannel = dma_claim_unused_channel(true); + dma_channel_config cc1 = dma_channel_get_default_config(readDMAChannel); + channel_config_set_transfer_data_size(&cc1, DMA_SIZE_8); + channel_config_set_read_increment(&cc1, false); + channel_config_set_write_increment(&cc1, true); + channel_config_set_dreq(&cc1, DREQ_ADC); + channel_config_set_irq_quiet(&cc1, false); + dma_channel_configure(readDMAChannel, + &cc1, + samples, // dest + &adc_hw->fifo, // source + channelCount, // count + false // defer start + ); + dma_channel_set_irq0_enabled(readDMAChannel, true); + irq_add_shared_handler(DMA_IRQ_0, _adcConversionFinishedHandler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); + + SIMPLEFOC_DEBUG("RP2040-CUR: DMA init"); + + // if (triggerPWMSlice>=0) { // if we have a trigger + // triggerDMAChannel = dma_claim_unused_channel(true); + // dma_channel_config cc3 = dma_channel_get_default_config(triggerDMAChannel); + // channel_config_set_transfer_data_size(&cc3, DMA_SIZE_32); + // channel_config_set_read_increment(&cc3, false); + // channel_config_set_write_increment(&cc3, false); + // channel_config_set_irq_quiet(&cc3, true); + // channel_config_set_dreq(&cc3, DREQ_PWM_WRAP0+triggerPWMSlice); //pwm_get_dreq(triggerPWMSlice)); + // pwm_set_irq_enabled(triggerPWMSlice, true); + // dma_channel_configure(triggerDMAChannel, + // &cc3, + // hw_set_alias_untyped(&adc_hw->cs), // dest + // &trigger_value, // source + // 1, // count + // true // defer start + // ); + // SIMPLEFOC_DEBUG("RP2040-CUR: PWM trigger init slice ", triggerPWMSlice); + // } + + initialized = true; + return initialized; +}; + + + + +void RP2040ADCEngine::start() { + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine starting"); + irq_set_enabled(DMA_IRQ_0, true); + dma_start_channel_mask( (1u << readDMAChannel) ); + for (int i=0;i<4;i++) { + if (channelsEnabled[i]) { + adc_select_input(i); // set input to first enabled channel + break; + } + } + // if (triggerPWMSlice>=0) { + // dma_start_channel_mask( (1u << triggerDMAChannel) ); + // //hw_set_bits(&adc_hw->cs, trigger_value); + // } + // else + adc_run(true); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine started"); +}; + + + + +void RP2040ADCEngine::stop() { + adc_run(false); + irq_set_enabled(DMA_IRQ_0, false); + dma_channel_abort(readDMAChannel); + // if (triggerPWMSlice>=0) + // dma_channel_abort(triggerDMAChannel); + adc_fifo_drain(); + SIMPLEFOC_DEBUG("RP2040-CUR: ADC engine stopped"); +}; + + + +ADCResults RP2040ADCEngine::getLastResults() { + ADCResults r; + r.value = lastResults.value; + return r; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h new file mode 100644 index 0000000..ae28bb2 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/rp2040/rp2040_mcu.h @@ -0,0 +1,93 @@ + + +#pragma once + +/* + * RP2040 ADC features are very weak :-( + * - only 4 inputs + * - only 9 bit effective resolution + * - read only 1 input at a time + * - 2 microseconds conversion time! + * - no triggers from PWM / events, only DMA + * + * So to read 3 phases takes 6 microseconds. :-( + * + * The RP2040 ADC engine takes over the control of the MCU's ADC. Parallel ADC access is not permitted, as this would + * cause conflicts with the engine's DMA based access and cause crashes. + * To use the other ADC channels, use them via this engine. Use addPin() to add them to the conversion, and getLastResult() + * to retrieve their value at any time. + * + * For motor current sensing, the engine supports inline sensing only. + * + * Inline sensing is supported by offering a user-selectable fixed ADC sampling rate, which can be set between 500kHz and 1Hz. + * After starting the engine it will continuously sample and provide new values at the configured rate. + * + * The default sampling rate is 20kHz, which is suitable for 2 channels assuming you a 5kHz main loop speed (a new measurement is used per + * main loop iteration). + * + * Low-side sensing is currently not supported. + * + * The SimpleFOC PWM driver for RP2040 syncs all the slices, so the PWM trigger is applied to the first used slice. For current + * sensing to work correctly, all PWM slices have to be set to the same PWM frequency. + * In theory, two motors could be sensed using 2 shunts on each motor. + * + * Note that if using other ADC channels along with the motor current sensing, those channels will be subject to the same conversion schedule as the motor's ADC channels, i.e. convert at the same fixed rate in case + * of inline sensing. + * + * Solution to trigger ADC conversion from PWM via DMA: + * use the PWM wrap as a DREQ to a DMA channel, and have the DMA channel write to the ADC's CS register to trigger an ADC sample. + * Unfortunately, I could not get this to work, so no low side sensing for the moment. + * + * Solution for ADC conversion: + * ADC converts all channels in round-robin mode, and writes to FIFO. FIFO is emptied by a DMA which triggers after N conversions, + * where N is the number of ADC channels used. So this DMA copies all the values from one round-robin conversion. + * + * + */ + + +#define SIMPLEFOC_RP2040_ADC_RESOLUTION 256 +#ifndef SIMPLEFOC_RP2040_ADC_VDDA +#define SIMPLEFOC_RP2040_ADC_VDDA 3.3f +#endif + + +union ADCResults { + uint32_t value; + uint8_t raw[4]; + struct { + uint8_t ch0; + uint8_t ch1; + uint8_t ch2; + uint8_t ch3; + }; +}; + + +class RP2040ADCEngine { + +public: + RP2040ADCEngine(); + void addPin(int pin); + //void setPWMTrigger(uint slice); + + bool init(); + void start(); + void stop(); + + ADCResults getLastResults(); // TODO find a better API and representation for this + + int samples_per_second = 20000; // 20kHz default (assuming 2 shunts and 5kHz loop speed), set to 0 to convert in tight loop + float adc_conv = (SIMPLEFOC_RP2040_ADC_VDDA / SIMPLEFOC_RP2040_ADC_RESOLUTION); // conversion from raw ADC to float + + //int triggerPWMSlice = -1; + bool initialized; + uint readDMAChannel; + //uint copyDMAChannel; + //uint triggerDMAChannel; + + bool channelsEnabled[4]; + volatile uint8_t samples[4]; + volatile ADCResults lastResults; + //alignas(32) volatile uint8_t nextResults[4]; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp new file mode 100644 index 0000000..da5ba85 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.cpp @@ -0,0 +1,318 @@ +#ifdef _SAMD21_ + +#include "samd21_mcu.h" +#include "../../hardware_api.h" + + +static bool freeRunning = false; +static int _pinA, _pinB, _pinC; +static uint16_t a = 0xFFFF, b = 0xFFFF, c = 0xFFFF; // updated by adcStopWithDMA when configured in freerunning mode +static SAMDCurrentSenseADCDMA instance; + +// function configuring low-side current sensing +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC) +{ + _UNUSED(driver_params); + + _pinA = pinA; + _pinB = pinB; + _pinC = pinC; + freeRunning = true; + instance.init(pinA, pinB, pinC); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC } + }; + + return params; +} +void _startADC3PinConversionLowSide() +{ + instance.startADCScan(); +} +/** + * function reading an ADC value and returning the read voltage + * + * @param pinA - the arduino pin to be read (it has to be ADC pin) + */ +float _readADCVoltageLowSide(const int pinA, const void* cs_params) +{ + _UNUSED(cs_params); + + instance.readResults(a, b, c); + + if(pinA == _pinA) + return instance.toVolts(a); + if(pinA == _pinB) + return instance.toVolts(b); + if(pinA == _pinC) + return instance.toVolts(c); + + return NAN; +} + +/** + * function syncing the Driver with the ADC for the LowSide Sensing + */ +void _driverSyncLowSide(void* driver_params, void* cs_params) +{ + _UNUSED(driver_params); + _UNUSED(cs_params); + + SIMPLEFOC_SAMD_DEBUG_SERIAL.println(F("TODO! _driverSyncLowSide() is not implemented")); + instance.startADCScan(); + //TODO: hook with PWM interrupts +} + + + + + + + + + + + // Credit: significant portions of this code were pulled from Paul Gould's git https://github.com/gouldpa/FOC-Arduino-Brushless + +static void adcStopWithDMA(void); +static void adcStartWithDMA(void); + +/** + * @brief ADC sync wait + * @retval void + */ +static __inline__ void ADCsync() __attribute__((always_inline, unused)); +static void ADCsync() { + while (ADC->STATUS.bit.SYNCBUSY == 1); //Just wait till the ADC is free +} + +// ADC DMA sequential free running (6) with Interrupts ///////////////// + +SAMDCurrentSenseADCDMA * SAMDCurrentSenseADCDMA::getHardwareAPIInstance() +{ + + return &instance; +} + +SAMDCurrentSenseADCDMA::SAMDCurrentSenseADCDMA() +{ +} + +void SAMDCurrentSenseADCDMA::init(int pinA, int pinB, int pinC, int pinAREF, float voltageAREF, uint32_t adcBits, uint32_t channelDMA) +{ + this->pinA = pinA; + this->pinB = pinB; + this->pinC = pinC; + this->pinAREF = pinAREF; + this->channelDMA = channelDMA; + this->voltageAREF = voltageAREF; + this->maxCountsADC = 1 << adcBits; + countsToVolts = ( voltageAREF / maxCountsADC ); + + initPins(); + initADC(); + initDMA(); + startADCScan(); //so we have something to read next time we call readResults() +} + + +void SAMDCurrentSenseADCDMA::startADCScan(){ + adcToDMATransfer(adcBuffer + oneBeforeFirstAIN, BufferSize); + adcStartWithDMA(); +} + +bool SAMDCurrentSenseADCDMA::readResults(uint16_t & a, uint16_t & b, uint16_t & c){ + if(ADC->CTRLA.bit.ENABLE) + return false; + uint32_t ainA = g_APinDescription[pinA].ulADCChannelNumber; + uint32_t ainB = g_APinDescription[pinB].ulADCChannelNumber; + a = adcBuffer[ainA]; + b = adcBuffer[ainB]; + if(_isset(pinC)) + { + uint32_t ainC = g_APinDescription[pinC].ulADCChannelNumber; + c = adcBuffer[ainC]; + } + return true; +} + + +float SAMDCurrentSenseADCDMA::toVolts(uint16_t counts) { + return counts * countsToVolts; +} + +void SAMDCurrentSenseADCDMA::initPins(){ + + if (pinAREF>=0) + pinMode(pinAREF, INPUT); + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + + uint32_t ainA = g_APinDescription[pinA].ulADCChannelNumber; + uint32_t ainB = g_APinDescription[pinB].ulADCChannelNumber; + firstAIN = min(ainA, ainB); + lastAIN = max(ainA, ainB); + if( _isset(pinC) ) + { + uint32_t ainC = g_APinDescription[pinC].ulADCChannelNumber; + pinMode(pinC, INPUT); + firstAIN = min(firstAIN, ainC); + lastAIN = max(lastAIN, ainC); + } + + oneBeforeFirstAIN = firstAIN - 1; //hack to discard noisy first readout + BufferSize = lastAIN - oneBeforeFirstAIN + 1; + +} + +void SAMDCurrentSenseADCDMA::initADC(){ + + analogRead(pinA); // do some pin init pinPeripheral() + analogRead(pinB); // do some pin init pinPeripheral() + analogRead(pinC); // do some pin init pinPeripheral() + + ADC->CTRLA.bit.ENABLE = 0x00; // Disable ADC + ADCsync(); + //ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC0_Val; // 2.2297 V Supply VDDANA + ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_1X_Val; // Gain select as 1X + // ADC->INPUTCTRL.bit.GAIN = ADC_INPUTCTRL_GAIN_DIV2_Val; // default + if (pinAREF>=0) + ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_AREFA; + else + ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC0; + ADCsync(); // ref 31.6.16 + + /* + Bits 19:16 – INPUTSCAN[3:0]: Number of Input Channels Included in Scan + This register gives the number of input sources included in the pin scan. The number of input sources included is + INPUTSCAN + 1. The input channels included are in the range from MUXPOS + INPUTOFFSET to MUXPOS + + INPUTOFFSET + INPUTSCAN. + The range of the scan mode must not exceed the number of input channels available on the device. + Bits 4:0 – MUXPOS[4:0]: Positive Mux Input Selection + These bits define the Mux selection for the positive ADC input. Table 32-14 shows the possible input selections. If + the internal bandgap voltage or temperature sensor input channel is selected, then the Sampling Time Length bit + group in the SamplingControl register must be written. + Table 32-14. Positive Mux Input Selection + MUXPOS[4:0] Group configuration Description + 0x00 PIN0 ADC AIN0 pin + 0x01 PIN1 ADC AIN1 pin + 0x02 PIN2 ADC AIN2 pin + 0x03 PIN3 ADC AIN3 pin + 0x04 PIN4 ADC AIN4 pin + 0x05 PIN5 ADC AIN5 pin + 0x06 PIN6 ADC AIN6 pin + 0x07 PIN7 ADC AIN7 pin + 0x08 PIN8 ADC AIN8 pin + 0x09 PIN9 ADC AIN9 pin + 0x0A PIN10 ADC AIN10 pin + 0x0B PIN11 ADC AIN11 pin + 0x0C PIN12 ADC AIN12 pin + 0x0D PIN13 ADC AIN13 pin + 0x0E PIN14 ADC AIN14 pin + 0x0F PIN15 ADC AIN15 pin + 0x10 PIN16 ADC AIN16 pin + 0x11 PIN17 ADC AIN17 pin + 0x12 PIN18 ADC AIN18 pin + 0x13 PIN19 ADC AIN19 pin + 0x14-0x17 Reserved + 0x18 TEMP Temperature reference + 0x19 BANDGAP Bandgap voltage + 0x1A SCALEDCOREVCC 1/4 scaled core supply + 0x1B SCALEDIOVCC 1/4 scaled I/O supply + 0x1C DAC DAC output + 0x1D-0x1F Reserved + */ + ADC->INPUTCTRL.bit.MUXPOS = oneBeforeFirstAIN; + ADCsync(); + ADC->INPUTCTRL.bit.INPUTSCAN = lastAIN; // so the adc will scan from oneBeforeFirstAIN to lastAIN (inclusive) + ADCsync(); + ADC->INPUTCTRL.bit.INPUTOFFSET = 0; //input scan cursor + ADCsync(); + ADC->AVGCTRL.reg = 0x00 ; //no averaging + ADC->SAMPCTRL.reg = 0x05; ; //sample length in 1/2 CLK_ADC cycles, see GCLK_ADC and ADC_CTRLB_PRESCALER_DIV16 + // according to the specsheet: f_GCLK_ADC ADC input clock frequency 48 MHz, so same as fCPU + ADCsync(); + ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV16 | ADC_CTRLB_FREERUN | ADC_CTRLB_RESSEL_12BIT; + ADCsync(); +} + +volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16))); + +void SAMDCurrentSenseADCDMA::initDMA() { + // probably on by default + PM->AHBMASK.reg |= PM_AHBMASK_DMAC ; + PM->APBBMASK.reg |= PM_APBBMASK_DMAC ; + NVIC_EnableIRQ( DMAC_IRQn ) ; + DMAC->BASEADDR.reg = (uint32_t)descriptor_section; + DMAC->WRBADDR.reg = (uint32_t)wrb; + DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); +} + + +void SAMDCurrentSenseADCDMA::adcToDMATransfer(void *rxdata, uint32_t hwords) { + + DMAC->CHID.reg = DMAC_CHID_ID(channelDMA); + DMAC->CHCTRLA.reg &= ~DMAC_CHCTRLA_ENABLE; + DMAC->CHCTRLA.reg = DMAC_CHCTRLA_SWRST; + DMAC->SWTRIGCTRL.reg &= (uint32_t)(~(1 << channelDMA)); + + DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) + | DMAC_CHCTRLB_TRIGSRC(ADC_DMAC_ID_RESRDY) + | DMAC_CHCTRLB_TRIGACT_BEAT; + DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK ; // enable all 3 interrupts + descriptor.descaddr = 0; + descriptor.srcaddr = (uint32_t) &ADC->RESULT.reg; + descriptor.btcnt = hwords; + descriptor.dstaddr = (uint32_t)rxdata + hwords*2; // end address + descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID; + memcpy(&descriptor_section[channelDMA],&descriptor, sizeof(dmacdescriptor)); + + // start channel + DMAC->CHID.reg = DMAC_CHID_ID(channelDMA); + DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE; +} + + +int iii = 0; + +void adcStopWithDMA(void){ + ADCsync(); + ADC->CTRLA.bit.ENABLE = 0x00; + // ADCsync(); + // if(iii++ % 1000 == 0) + // { + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(a); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(" :: "); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(b); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(" :: "); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.print(c); + // SIMPLEFOC_SAMD_DEBUG_SERIAL.println("yo!"); + // } + + +} + +void adcStartWithDMA(void){ + ADCsync(); + ADC->INPUTCTRL.bit.INPUTOFFSET = 0; + ADCsync(); + ADC->SWTRIG.bit.FLUSH = 1; + ADCsync(); + ADC->CTRLA.bit.ENABLE = 0x01; + ADCsync(); +} + +void DMAC_Handler() { + uint8_t active_channel; + active_channel = DMAC->INTPEND.reg & DMAC_INTPEND_ID_Msk; // get channel number + DMAC->CHID.reg = DMAC_CHID_ID(active_channel); + adcStopWithDMA(); + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL; // clear + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR; + DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP; + +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h new file mode 100644 index 0000000..e7d7442 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd21_mcu.h @@ -0,0 +1,69 @@ +#ifdef _SAMD21_ + +#ifndef CURRENT_SENSE_SAMD21_H +#define CURRENT_SENSE_SAMD21_H + +#define SIMPLEFOC_SAMD_DEBUG +#if !defined(SIMPLEFOC_SAMD_DEBUG_SERIAL) +#define SIMPLEFOC_SAMD_DEBUG_SERIAL Serial +#endif + +#include + typedef struct { + uint16_t btctrl; + uint16_t btcnt; + uint32_t srcaddr; + uint32_t dstaddr; + uint32_t descaddr; + } dmacdescriptor ; + + +// AREF pin is 42 + +class SAMDCurrentSenseADCDMA +{ + +public: + static SAMDCurrentSenseADCDMA * getHardwareAPIInstance(); + SAMDCurrentSenseADCDMA(); + void init(int pinA, int pinB, int pinC, int pinAREF = 42, float voltageAREF = 3.3, uint32_t adcBits = 12, uint32_t channelDMA = 3); + void startADCScan(); + bool readResults(uint16_t & a, uint16_t & b, uint16_t & c); + float toVolts(uint16_t counts); +private: + + void adcToDMATransfer(void *rxdata, uint32_t hwords); + + void initPins(); + void initADC(); + void initDMA(); + + uint32_t oneBeforeFirstAIN; // hack to discard first noisy readout + uint32_t firstAIN; + uint32_t lastAIN; + uint32_t BufferSize = 0; + + uint16_t adcBuffer[20]; + + + uint32_t pinA; + uint32_t pinB; + uint32_t pinC; + uint32_t pinAREF; + uint32_t channelDMA; // DMA channel + bool freeRunning; + + float voltageAREF; + float maxCountsADC; + float countsToVolts; + + dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16))); + dmacdescriptor descriptor __attribute__ ((aligned (16))); + +}; + +#endif + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp new file mode 100644 index 0000000..2ec47c0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/samd/samd_mcu.cpp @@ -0,0 +1,23 @@ +#include "../../hardware_api.h" + +#if defined(_SAMD21_)|| defined(_SAMD51_) || defined(_SAME51_) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp new file mode 100644 index 0000000..dc505d6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.cpp @@ -0,0 +1,377 @@ +#include "../../../hardware_api.h" +#if defined(ARDUINO_B_G431B_ESC1) + +#include "communication/SimpleFOCDebug.h" + +#include "stm32g4xx_hal.h" +#include "stm32g4xx_ll_pwr.h" +#include "stm32g4xx_hal_adc.h" +#include "b_g431_hal.h" + +// From STM32 cube IDE +/** + ****************************************************************************** + * @attention + * + *

© Copyright (c) 2020 STMicroelectronics. + * All rights reserved.

+ * + * This software component is licensed by ST under BSD 3-Clause license, + * the "License"; You may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * opensource.org/licenses/BSD-3-Clause + * + ****************************************************************************** + */ + + +/** + * @brief GPIO Initialization Function + * @param None + * @retval None + */ +void MX_GPIO_Init(void) +{ + /* GPIO Ports Clock Enable */ + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOF_CLK_ENABLE(); + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + + __HAL_RCC_ADC12_CLK_ENABLE(); +} + +/** + * Enable DMA controller clock + */ +void MX_DMA_Init(void) +{ + /* DMA controller clock enable */ + __HAL_RCC_DMAMUX1_CLK_ENABLE(); + __HAL_RCC_DMA1_CLK_ENABLE(); + __HAL_RCC_DMA2_CLK_ENABLE(); + + /* DMA interrupt init */ + /* DMA1_Channel1_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); + /* DMA1_Channel2_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn); + + // Enable external clock for ADC + RCC_PeriphCLKInitTypeDef PeriphClkInit; + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC12; + PeriphClkInit.Adc12ClockSelection = RCC_ADC12CLKSOURCE_PLL; + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); +} + + +/** + * @brief ADC1 Initialization Function + * @param None + * @retval None + */ +void MX_ADC1_Init(ADC_HandleTypeDef* hadc1) +{ + /* USER CODE BEGIN ADC1_Init 0 */ + + /* USER CODE END ADC1_Init 0 */ + + ADC_MultiModeTypeDef multimode = {0}; + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC1_Init 1 */ + + /* USER CODE END ADC1_Init 1 */ + /** Common config + */ + hadc1->Instance = ADC1; + hadc1->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV16; + hadc1->Init.Resolution = ADC_RESOLUTION_12B; + hadc1->Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc1->Init.GainCompensation = 0; + hadc1->Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc1->Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc1->Init.LowPowerAutoWait = DISABLE; + hadc1->Init.ContinuousConvMode = DISABLE; + hadc1->Init.NbrOfConversion = 5; + hadc1->Init.DiscontinuousConvMode = DISABLE; + hadc1->Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO; + hadc1->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; + hadc1->Init.DMAContinuousRequests = ENABLE; + hadc1->Init.Overrun = ADC_OVR_DATA_PRESERVED; + + if (HAL_ADC_Init(hadc1) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_Init failed!"); + } + + /** Configure the ADC multi-mode + */ + multimode.Mode = ADC_MODE_INDEPENDENT; + if (HAL_ADCEx_MultiModeConfigChannel(hadc1, &multimode) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADCEx_MultiModeConfigChannel failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_12; // ADC1_IN12 = PB1 = OP3_OUT + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_3; // ADC1_IN3 = PA2 = OP1_OUT + sConfig.Rank = ADC_REGULAR_RANK_2; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + //****************************************************************** + // Temp, Poti .... + /* Configure Regular Channel (PB12, Potentiometer) + */ + sConfig.Channel = ADC_CHANNEL_11; + sConfig.Rank = ADC_REGULAR_RANK_3; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + /** Configure Regular Channel (PB14, Temperature) + */ + sConfig.Channel = ADC_CHANNEL_5; + sConfig.Rank = ADC_REGULAR_RANK_4; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + + /** Configure Regular Channel (PB14, Temperature) + */ + sConfig.Channel = ADC_CHANNEL_1; + sConfig.Rank = ADC_REGULAR_RANK_5; + sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc1, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /* USER CODE BEGIN ADC1_Init 2 */ + + /* USER CODE END ADC1_Init 2 */ + +} + +/** + * @brief ADC2 Initialization Function + * @param None + * @retval None + */ +void MX_ADC2_Init(ADC_HandleTypeDef* hadc2) +{ + /* USER CODE BEGIN ADC2_Init 0 */ + + /* USER CODE END ADC2_Init 0 */ + + ADC_ChannelConfTypeDef sConfig = {0}; + + /* USER CODE BEGIN ADC2_Init 1 */ + + /* USER CODE END ADC2_Init 1 */ + /** Common config + */ + hadc2->Instance = ADC2; + hadc2->Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV16; + hadc2->Init.Resolution = ADC_RESOLUTION_12B; + hadc2->Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc2->Init.GainCompensation = 0; + hadc2->Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc2->Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc2->Init.LowPowerAutoWait = DISABLE; + hadc2->Init.ContinuousConvMode = DISABLE; + hadc2->Init.NbrOfConversion = 1; + hadc2->Init.DiscontinuousConvMode = DISABLE; + hadc2->Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO; + hadc2->Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; + hadc2->Init.DMAContinuousRequests = ENABLE; + hadc2->Init.Overrun = ADC_OVR_DATA_PRESERVED; + + if (HAL_ADC_Init(hadc2) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_Init failed!"); + } + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_3; // ADC2_IN3 = PA6 + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + if (HAL_ADC_ConfigChannel(hadc2, &sConfig) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_ADC_ConfigChannel failed!"); + } + /* USER CODE BEGIN ADC2_Init 2 */ + + /* USER CODE END ADC2_Init 2 */ + +} + +/** +* @brief OPAMP MSP Initialization +* This function configures the hardware resources used in this example +* @param hopamp-> OPAMP handle pointer +* @retval None +*/ +void HAL_OPAMP_MspInit(OPAMP_HandleTypeDef* hopamp) +{ + GPIO_InitTypeDef GPIO_InitStruct = {0}; + if(hopamp->Instance==OPAMP1) + { + /* USER CODE BEGIN OPAMP1_MspInit 0 */ + + /* USER CODE END OPAMP1_MspInit 0 */ + + __HAL_RCC_GPIOA_CLK_ENABLE(); + /**OPAMP1 GPIO Configuration + PA1 ------> OPAMP1_VINP + PA2 ------> OPAMP1_VOUT + PA3 ------> OPAMP1_VINM + */ + GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP1_MspInit 1 */ + + /* USER CODE END OPAMP1_MspInit 1 */ + } + else if(hopamp->Instance==OPAMP2) + { + /* USER CODE BEGIN OPAMP2_MspInit 0 */ + + /* USER CODE END OPAMP2_MspInit 0 */ + + __HAL_RCC_GPIOA_CLK_ENABLE(); + /**OPAMP2 GPIO Configuration + PA5 ------> OPAMP2_VINM + PA6 ------> OPAMP2_VOUT + PA7 ------> OPAMP2_VINP + */ + GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP2_MspInit 1 */ + + /* USER CODE END OPAMP2_MspInit 1 */ + } + else if(hopamp->Instance==OPAMP3) + { + /* USER CODE BEGIN OPAMP3_MspInit 0 */ + + /* USER CODE END OPAMP3_MspInit 0 */ + + __HAL_RCC_GPIOB_CLK_ENABLE(); + /**OPAMP3 GPIO Configuration + PB0 ------> OPAMP3_VINP + PB1 ------> OPAMP3_VOUT + PB2 ------> OPAMP3_VINM + */ + GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + /* USER CODE BEGIN OPAMP3_MspInit 1 */ + + /* USER CODE END OPAMP3_MspInit 1 */ + } + +} + +/** +* @brief OPAMP MSP De-Initialization +* This function freeze the hardware resources used in this example +* @param hopamp-> OPAMP handle pointer +* @retval None +*/ +void HAL_OPAMP_MspDeInit(OPAMP_HandleTypeDef* hopamp) +{ + if(hopamp->Instance==OPAMP1) + { + /* USER CODE BEGIN OPAMP1_MspDeInit 0 */ + + /* USER CODE END OPAMP1_MspDeInit 0 */ + + /**OPAMP1 GPIO Configuration + PA1 ------> OPAMP1_VINP + PA2 ------> OPAMP1_VOUT + PA3 ------> OPAMP1_VINM + */ + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3); + + /* USER CODE BEGIN OPAMP1_MspDeInit 1 */ + + /* USER CODE END OPAMP1_MspDeInit 1 */ + } + else if(hopamp->Instance==OPAMP2) + { + /* USER CODE BEGIN OPAMP2_MspDeInit 0 */ + + /* USER CODE END OPAMP2_MspDeInit 0 */ + + /**OPAMP2 GPIO Configuration + PA5 ------> OPAMP2_VINM + PA6 ------> OPAMP2_VOUT + PA7 ------> OPAMP2_VINP + */ + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7); + + /* USER CODE BEGIN OPAMP2_MspDeInit 1 */ + + /* USER CODE END OPAMP2_MspDeInit 1 */ + } + else if(hopamp->Instance==OPAMP3) + { + /* USER CODE BEGIN OPAMP3_MspDeInit 0 */ + + /* USER CODE END OPAMP3_MspDeInit 0 */ + + /**OPAMP3 GPIO Configuration + PB0 ------> OPAMP3_VINP + PB1 ------> OPAMP3_VOUT + PB2 ------> OPAMP3_VINM + */ + HAL_GPIO_DeInit(GPIOB, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2); + + /* USER CODE BEGIN OPAMP3_MspDeInit 1 */ + + /* USER CODE END OPAMP3_MspDeInit 1 */ + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h new file mode 100644 index 0000000..2d6a1f0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_hal.h @@ -0,0 +1,18 @@ +#ifndef B_G431_ESC1_HAL +#define B_G431_ESC1_HAL + +#if defined(ARDUINO_B_G431B_ESC1) + +#include +#include + +void MX_GPIO_Init(void); +void MX_DMA_Init(void); +void MX_ADC1_Init(ADC_HandleTypeDef* hadc1); +void MX_ADC2_Init(ADC_HandleTypeDef* hadc2); +void MX_OPAMP1_Init(OPAMP_HandleTypeDef* hopamp); +void MX_OPAMP2_Init(OPAMP_HandleTypeDef* hopamp); +void MX_OPAMP3_Init(OPAMP_HandleTypeDef* hopamp); +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp new file mode 100644 index 0000000..3e10bbc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/b_g431/b_g431_mcu.cpp @@ -0,0 +1,171 @@ +#include "../../../hardware_api.h" + +#if defined(ARDUINO_B_G431B_ESC1) + +#include "b_g431_hal.h" +#include "Arduino.h" +#include "../stm32_mcu.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "communication/SimpleFOCDebug.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 4096.0f +#define ADC_BUF_LEN_1 5 +#define ADC_BUF_LEN_2 1 + +static ADC_HandleTypeDef hadc1; +static ADC_HandleTypeDef hadc2; +static OPAMP_HandleTypeDef hopamp1; +static OPAMP_HandleTypeDef hopamp2; +static OPAMP_HandleTypeDef hopamp3; + +static DMA_HandleTypeDef hdma_adc1; +static DMA_HandleTypeDef hdma_adc2; + +volatile uint16_t adcBuffer1[ADC_BUF_LEN_1] = {0}; // Buffer for store the results of the ADC conversion +volatile uint16_t adcBuffer2[ADC_BUF_LEN_2] = {0}; // Buffer for store the results of the ADC conversion + +// function reading an ADC value and returning the read voltage +// As DMA is being used just return the DMA result +float _readADCVoltageInline(const int pin, const void* cs_params){ + uint32_t raw_adc = 0; + if(pin == PA2) // = ADC1_IN3 = phase U (OP1_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer1[1]; + else if(pin == PA6) // = ADC2_IN3 = phase V (OP2_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer2[0]; +#ifdef PB1 + else if(pin == PB1) // = ADC1_IN12 = phase W (OP3_OUT) on B-G431B-ESC1 + raw_adc = adcBuffer1[0]; +#endif + + else if (pin == A_POTENTIOMETER) + raw_adc = adcBuffer1[2]; + else if (pin == A_TEMPERATURE) + raw_adc = adcBuffer1[3]; + else if (pin == A_VBUS) + raw_adc = adcBuffer1[4]; + + return raw_adc * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +void _configureOPAMP(OPAMP_HandleTypeDef *hopamp, OPAMP_TypeDef *OPAMPx_Def){ + // could this be replaced with LL_OPAMP calls?? + hopamp->Instance = OPAMPx_Def; + hopamp->Init.PowerMode = OPAMP_POWERMODE_HIGHSPEED; + hopamp->Init.Mode = OPAMP_PGA_MODE; + hopamp->Init.NonInvertingInput = OPAMP_NONINVERTINGINPUT_IO0; + hopamp->Init.InternalOutput = DISABLE; + hopamp->Init.TimerControlledMuxmode = OPAMP_TIMERCONTROLLEDMUXMODE_DISABLE; + hopamp->Init.PgaConnect = OPAMP_PGA_CONNECT_INVERTINGINPUT_IO0_BIAS; + hopamp->Init.PgaGain = OPAMP_PGA_GAIN_16_OR_MINUS_15; + hopamp->Init.UserTrimming = OPAMP_TRIMMING_FACTORY; + if (HAL_OPAMP_Init(hopamp) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_OPAMP_Init failed!"); + } +} +void _configureOPAMPs(OPAMP_HandleTypeDef *OPAMPA, OPAMP_HandleTypeDef *OPAMPB, OPAMP_HandleTypeDef *OPAMPC){ + // Configure the opamps + _configureOPAMP(OPAMPA, OPAMP1); + _configureOPAMP(OPAMPB, OPAMP2); + _configureOPAMP(OPAMPC, OPAMP3); +} + +void MX_DMA1_Init(ADC_HandleTypeDef *hadc, DMA_HandleTypeDef *hdma_adc, DMA_Channel_TypeDef* channel, uint32_t request) { + hdma_adc->Instance = channel; + hdma_adc->Init.Request = request; + hdma_adc->Init.Direction = DMA_PERIPH_TO_MEMORY; + hdma_adc->Init.PeriphInc = DMA_PINC_DISABLE; + hdma_adc->Init.MemInc = DMA_MINC_ENABLE; + hdma_adc->Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_adc->Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + hdma_adc->Init.Mode = DMA_CIRCULAR; + hdma_adc->Init.Priority = DMA_PRIORITY_LOW; + HAL_DMA_DeInit(hdma_adc); + if (HAL_DMA_Init(hdma_adc) != HAL_OK) + { + SIMPLEFOC_DEBUG("HAL_DMA_Init failed!"); + } + __HAL_LINKDMA(hadc, DMA_Handle, *hdma_adc); +} + +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + _UNUSED(pinA); + _UNUSED(pinB); + _UNUSED(pinC); + + SIMPLEFOC_DEBUG("B-G431B does not implement inline current sense. Use low-side current sense instead."); + return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; +} + + +void* _configureADCLowSide(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + HAL_Init(); + MX_GPIO_Init(); + MX_DMA_Init(); + _configureOPAMPs(&hopamp1, &hopamp3, &hopamp2); + MX_ADC1_Init(&hadc1); + MX_ADC2_Init(&hadc2); + + MX_DMA1_Init(&hadc1, &hdma_adc1, DMA1_Channel1, DMA_REQUEST_ADC1); + MX_DMA1_Init(&hadc2, &hdma_adc2, DMA1_Channel2, DMA_REQUEST_ADC2); + + if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer1, ADC_BUF_LEN_1) != HAL_OK) + { + SIMPLEFOC_DEBUG("DMA read init failed"); + } + if (HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adcBuffer2, ADC_BUF_LEN_2) != HAL_OK) + { + SIMPLEFOC_DEBUG("DMA read init failed"); + } + + HAL_OPAMP_Start(&hopamp1); + HAL_OPAMP_Start(&hopamp2); + HAL_OPAMP_Start(&hopamp3); + + Stm32CurrentSenseParams* params = new Stm32CurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE) / (_ADC_RESOLUTION), + .timer_handle = (HardwareTimer *)(HardwareTimer_Handle[get_timer_index(TIM1)]->__this) + }; + + return params; +} + +extern "C" { +void DMA1_Channel1_IRQHandler(void) { + HAL_DMA_IRQHandler(&hdma_adc1); +} + +void DMA1_Channel2_IRQHandler(void) { + HAL_DMA_IRQHandler(&hdma_adc2); +} +} + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger for DMA transfer aligns with the pwm peaks instead of throughs. + // only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp new file mode 100644 index 0000000..94253d7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.cpp @@ -0,0 +1,33 @@ + +#include "../../hardware_api.h" + +#if defined(_STM32_DEF_) and !defined(ARDUINO_B_G431B_ESC1) + +#include "stm32_mcu.h" + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + Stm32CurrentSenseParams* params = new Stm32CurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +// function reading an ADC value and returning the read voltage +__attribute__((weak)) float _readADCVoltageInline(const int pinA, const void* cs_params){ + uint32_t raw_adc = analogRead(pinA); + return raw_adc * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h new file mode 100644 index 0000000..6e23817 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32_mcu.h @@ -0,0 +1,21 @@ + +#ifndef STM32_CURRENTSENSE_MCU_DEF +#define STM32_CURRENTSENSE_MCU_DEF +#include "../../hardware_api.h" +#include "../../../common/foc_utils.h" + +#if defined(_STM32_DEF_) + +// generic implementation of the hardware specific structure +// containing all the necessary current sense parameters +// will be returned as a void pointer from the _configureADCx functions +// will be provided to the _readADCVoltageX() as a void pointer +typedef struct Stm32CurrentSenseParams { + int pins[3] = {(int)NOT_SET}; + float adc_voltage_conv; + ADC_HandleTypeDef* adc_handle = NP; + HardwareTimer* timer_handle = NP; +} Stm32CurrentSenseParams; + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp new file mode 100644 index 0000000..27e1195 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.cpp @@ -0,0 +1,162 @@ +#include "stm32f1_hal.h" + +#if defined(STM32F1xx) + +#include "../../../../communication/SimpleFOCDebug.h" +#define _TRGO_NOT_AVAILABLE 12345 + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_adc_ex.h#L215 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJECCONV_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJECCONV_T2_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJECCONV_T4_TRGO; +#endif +#ifdef TIM5 // if defined timer 5 + else if(timer->getHandle()->Instance == TIM5) + return ADC_EXTERNALTRIGINJECCONV_T5_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_adc_ex.h#L215 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGCONV_T3_TRGO; +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGCONV_T8_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + if(hadc.Instance == ADC1) __HAL_RCC_ADC1_CLK_ENABLE(); +#ifdef ADC2 // if defined ADC2 + else if(hadc.Instance == ADC2) __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef ADC3 // if defined ADC3 + else if(hadc.Instance == ADC3) __HAL_RCC_ADC3_CLK_ENABLE(); +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = ENABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 0; + HAL_ADC_Init(&hadc); + /**Configure for the selected ADC regular channel to be converted. + */ + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_1CYCLE_5; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffset = 0; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); + #endif + return -1; + } + // display which timer is being used + #ifdef SIMPLEFOC_STM32_DEBUG + // it would be better to use the getTimerNumber from driver + SIMPLEFOC_DEBUG("STM32-CS: injected trigger for timer index: ", get_timer_index(cs_params->timer_handle->getHandle()->Instance) + 1); + #endif + + // first channel + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_1; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[0]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + // second channel + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_2; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[1]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_REGULAR_RANK_3; + sConfigInjected.InjectedChannel = STM_PIN_CHANNEL(pinmap_function(analogInputToPinName(cs_params->pins[2]), PinMap_ADC)); + HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected); + } + + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + + cs_params->adc_handle = &hadc; + + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } + +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h new file mode 100644 index 0000000..b0f4f83 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_hal.h @@ -0,0 +1,17 @@ +#ifndef STM32F1_LOWSIDE_HAL +#define STM32F1_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32F1xx) +#include "stm32f1xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp new file mode 100644 index 0000000..8daa7ee --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f1/stm32f1_mcu.cpp @@ -0,0 +1,104 @@ +#include "../../../hardware_api.h" + +#if defined(STM32F1xx) +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32f1_hal.h" +#include "Arduino.h" + +#define _ADC_VOLTAGE_F1 3.3f +#define _ADC_RESOLUTION_F1 4096.0f + +// array of values of 4 injected channels per adc instance (3) +uint32_t adc_val[3][4]={0}; +// does adc interrupt need a downsample - per adc (3) +bool needs_downsample[3] = {1}; +// downsampling variable - per adc (3) +uint8_t tim_downsample[3] = {0}; + +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + if(AdcHandle->Instance == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle->Instance == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle->Instance == ADC3) return 2; +#endif + return 0; +} + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET,(int)NOT_SET,(int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_F1) / (_ADC_RESOLUTION_F1) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp new file mode 100644 index 0000000..68a9d09 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.cpp @@ -0,0 +1,170 @@ +#include "stm32f4_hal.h" + +#if defined(STM32F4xx) + +//#define SIMPLEFOC_STM32_DEBUG + +#include "../../../../communication/SimpleFOCDebug.h" +#define _TRGO_NOT_AVAILABLE 12345 + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) __HAL_RCC_ADC1_CLK_ENABLE(); +#ifdef ADC2 // if defined ADC2 + else if(hadc.Instance == ADC2) __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef ADC3 // if defined ADC3 + else if(hadc.Instance == ADC3) __HAL_RCC_ADC3_CLK_ENABLE(); +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ENABLE; + hadc.Init.ContinuousConvMode = ENABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 1; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_3CYCLES; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONVEDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffset = 0; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); + #endif + return -1; + } + // display which timer is being used + #ifdef SIMPLEFOC_STM32_DEBUG + // it would be better to use the getTimerNumber from driver + SIMPLEFOC_DEBUG("STM32-CS: injected trigger for timer index: ", get_timer_index(cs_params->timer_handle->getHandle()->Instance) + 1); + #endif + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + // enable interrupt + HAL_NVIC_SetPriority(ADC_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC_IRQn); + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h new file mode 100644 index 0000000..71071a5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_hal.h @@ -0,0 +1,18 @@ +#ifndef STM32F4_LOWSIDE_HAL +#define STM32F4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32F4xx) +#include "stm32f4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32f4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp new file mode 100644 index 0000000..6034478 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_mcu.cpp @@ -0,0 +1,96 @@ +#include "../../../hardware_api.h" + +#if defined(STM32F4xx) +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32f4_hal.h" +#include "stm32f4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_F4 3.3f +#define _ADC_RESOLUTION_F4 4096.0f + + +// array of values of 4 injected channels per adc instance (3) +uint32_t adc_val[3][4]={0}; +// does adc interrupt need a downsample - per adc (3) +bool needs_downsample[3] = {1}; +// downsampling variable - per adc (3) +uint8_t tim_downsample[3] = {0}; + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET,(int)NOT_SET,(int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_F4) / (_ADC_RESOLUTION_F4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp new file mode 100644 index 0000000..20793d8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.cpp @@ -0,0 +1,193 @@ +#include "stm32f4_utils.h" + +#if defined(STM32F4xx) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc_ex.h#L179 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJECCONV_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJECCONV_T2_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJECCONV_T4_TRGO; +#endif +#ifdef TIM5 // if defined timer 5 + else if(timer->getHandle()->Instance == TIM5) + return ADC_EXTERNALTRIGINJECCONV_T5_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h#L331 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGCONV_T2_TRGO; +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGCONV_T3_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGCONV_T8_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h new file mode 100644 index 0000000..b4549ba --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32f4/stm32f4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32F4_UTILS_HAL +#define STM32F4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32F4xx) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc_ex.h#L179 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h#L331 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp new file mode 100644 index 0000000..9132211 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.cpp @@ -0,0 +1,267 @@ +#include "stm32g4_hal.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "../../../../communication/SimpleFOCDebug.h" + +#define SIMPLEFOC_STM32_DEBUG + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) { +#ifdef __HAL_RCC_ADC1_CLK_ENABLE + __HAL_RCC_ADC1_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { +#ifdef __HAL_RCC_ADC2_CLK_ENABLE + __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { +#ifdef __HAL_RCC_ADC3_CLK_ENABLE + __HAL_RCC_ADC3_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { +#ifdef __HAL_RCC_ADC4_CLK_ENABLE + __HAL_RCC_ADC4_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = DISABLE; + hadc.Init.LowPowerAutoWait = DISABLE; + hadc.Init.GainCompensation = 0; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 2; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONV_EDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedSingleDiff = ADC_SINGLE_ENDED; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffsetNumber = ADC_OFFSET_NONE; + sConfigInjected.InjectedOffset = 0; + sConfigInjected.InjecOversamplingMode = DISABLE; + sConfigInjected.QueueInjectedContext = DISABLE; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); +#endif + return -1; + } + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + + + if(hadc.Instance == ADC1) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { + // enable interrupt + HAL_NVIC_SetPriority(ADC3_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC3_IRQn); + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { + // enable interrupt + HAL_NVIC_SetPriority(ADC4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC4_IRQn); + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { + // enable interrupt + HAL_NVIC_SetPriority(ADC5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC5_IRQn); + } +#endif + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#ifdef ADC3 + void ADC3_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC4 + void ADC4_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC5 + void ADC5_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h new file mode 100644 index 0000000..2298b17 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_hal.h @@ -0,0 +1,19 @@ +#ifndef STM32G4_LOWSIDE_HAL +#define STM32G4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "stm32g4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32g4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp new file mode 100644 index 0000000..ae96bad --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_mcu.cpp @@ -0,0 +1,98 @@ +#include "../../../hardware_api.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32g4_hal.h" +#include "stm32g4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_G4 3.3f +#define _ADC_RESOLUTION_G4 4096.0f + + +// array of values of 4 injected channels per adc instance (5) +uint32_t adc_val[5][4]={0}; +// does adc interrupt need a downsample - per adc (5) +bool needs_downsample[5] = {1}; +// downsampling variable - per adc (5) +uint8_t tim_downsample[5] = {0}; + + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET, (int)NOT_SET, (int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_G4) / (_ADC_RESOLUTION_G4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp new file mode 100644 index 0000000..89a9bc3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.cpp @@ -0,0 +1,237 @@ +#include "stm32g4_utils.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJEC_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJEC_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGINJEC_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJEC_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIGINJEC_T6_TRGO; +#endif +#ifdef TIM7 // if defined timer 7 + else if(timer->getHandle()->Instance == TIM7) + return ADC_EXTERNALTRIGINJEC_T7_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGINJEC_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIGINJEC_T15_TRGO; +#endif +#ifdef TIM20 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM20) + return ADC_EXTERNALTRIGINJEC_T20_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIG_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIG_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIG_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIG_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIG_T6_TRGO; +#endif +#ifdef TIM7 // if defined timer 7 + else if(timer->getHandle()->Instance == TIM7) + return ADC_EXTERNALTRIG_T7_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIG_T7_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIG_T15_TRGO; +#endif +#ifdef TIM20 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM20) + return ADC_EXTERNALTRIG_T20_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h new file mode 100644 index 0000000..fa857bd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32g4/stm32g4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32G4_UTILS_HAL +#define STM32G4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32G4xx) && !defined(ARDUINO_B_G431B_ESC1) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp new file mode 100644 index 0000000..bfec217 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.cpp @@ -0,0 +1,266 @@ +#include "stm32l4_hal.h" + +#if defined(STM32L4xx) + +#include "../../../../communication/SimpleFOCDebug.h" + +#define SIMPLEFOC_STM32_DEBUG + +ADC_HandleTypeDef hadc; + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params) +{ + ADC_InjectionConfTypeDef sConfigInjected; + + // check if all pins belong to the same ADC + ADC_TypeDef* adc_pin1 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + ADC_TypeDef* adc_pin2 = (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[1]), PinMap_ADC); + ADC_TypeDef* adc_pin3 = _isset(cs_params->pins[2]) ? (ADC_TypeDef*)pinmap_peripheral(analogInputToPinName(cs_params->pins[2]), PinMap_ADC) : nullptr; + if ( (adc_pin1 != adc_pin2) || ( (adc_pin3) && (adc_pin1 != adc_pin3) )){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Analog pins dont belong to the same ADC!"); +#endif + return -1; + } + + + /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + */ + hadc.Instance = (ADC_TypeDef *)pinmap_peripheral(analogInputToPinName(cs_params->pins[0]), PinMap_ADC); + + if(hadc.Instance == ADC1) { +#ifdef __HAL_RCC_ADC1_CLK_ENABLE + __HAL_RCC_ADC1_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { +#ifdef __HAL_RCC_ADC2_CLK_ENABLE + __HAL_RCC_ADC2_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC12_CLK_ENABLE + __HAL_RCC_ADC12_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { +#ifdef __HAL_RCC_ADC3_CLK_ENABLE + __HAL_RCC_ADC3_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { +#ifdef __HAL_RCC_ADC4_CLK_ENABLE + __HAL_RCC_ADC4_CLK_ENABLE(); +#endif +#ifdef __HAL_RCC_ADC34_CLK_ENABLE + __HAL_RCC_ADC34_CLK_ENABLE(); +#endif +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { +#if defined(ADC345_COMMON) + __HAL_RCC_ADC345_CLK_ENABLE(); +#endif + } +#endif + else{ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: Pin does not belong to any ADC!"); +#endif + return -1; // error not a valid ADC instance + } + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: Using ADC: ", _adcToIndex(&hadc)+1); +#endif + + hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc.Init.Resolution = ADC_RESOLUTION_12B; + hadc.Init.ScanConvMode = ADC_SCAN_ENABLE; + hadc.Init.ContinuousConvMode = DISABLE; + hadc.Init.LowPowerAutoWait = DISABLE; + hadc.Init.DiscontinuousConvMode = DISABLE; + hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; // for now + hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc.Init.NbrOfConversion = 2; + hadc.Init.DMAContinuousRequests = DISABLE; + hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED; + if ( HAL_ADC_Init(&hadc) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init ADC!"); +#endif + return -1; + } + + /**Configures for the selected ADC injected channel its corresponding rank in the sequencer and its sample time + */ + sConfigInjected.InjectedNbrOfConversion = _isset(cs_params->pins[2]) ? 3 : 2; + sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_2CYCLES_5; + sConfigInjected.ExternalTrigInjecConvEdge = ADC_EXTERNALTRIGINJECCONV_EDGE_RISING; + sConfigInjected.AutoInjectedConv = DISABLE; + sConfigInjected.InjectedSingleDiff = ADC_SINGLE_ENDED; + sConfigInjected.InjectedDiscontinuousConvMode = DISABLE; + sConfigInjected.InjectedOffsetNumber = ADC_OFFSET_NONE; + sConfigInjected.InjectedOffset = 0; + sConfigInjected.InjecOversamplingMode = DISABLE; + sConfigInjected.QueueInjectedContext = DISABLE; + + // automating TRGO flag finding - hardware specific + uint8_t tim_num = 0; + while(driver_params->timers[tim_num] != NP && tim_num < 6){ + uint32_t trigger_flag = _timerToInjectedTRGO(driver_params->timers[tim_num++]); + if(trigger_flag == _TRGO_NOT_AVAILABLE) continue; // timer does not have valid trgo for injected channels + + // if the code comes here, it has found the timer available + // timer does have trgo flag for injected channels + sConfigInjected.ExternalTrigInjecConv = trigger_flag; + + // this will be the timer with which the ADC will sync + cs_params->timer_handle = driver_params->timers[tim_num-1]; + // done + break; + } + if( cs_params->timer_handle == NP ){ + // not possible to use these timers for low-side current sense +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot sync any timer to injected channels!"); +#endif + return -1; + } + + + // first channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[0])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[0])) ); +#endif + return -1; + } + + // second channel + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_2; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[1])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[1]))) ; +#endif + return -1; + } + + // third channel - if exists + if(_isset(cs_params->pins[2])){ + sConfigInjected.InjectedRank = ADC_INJECTED_RANK_3; + sConfigInjected.InjectedChannel = _getADCChannel(analogInputToPinName(cs_params->pins[2])); + if (HAL_ADCEx_InjectedConfigChannel(&hadc, &sConfigInjected) != HAL_OK){ +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-CS: ERR: cannot init injected channel: ", (int)_getADCChannel(analogInputToPinName(cs_params->pins[2]))) ; +#endif + return -1; + } + } + + + + if(hadc.Instance == ADC1) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#ifdef ADC2 + else if (hadc.Instance == ADC2) { + // enable interrupt + HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC1_2_IRQn); + } +#endif +#ifdef ADC3 + else if (hadc.Instance == ADC3) { + // enable interrupt + HAL_NVIC_SetPriority(ADC3_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC3_IRQn); + } +#endif +#ifdef ADC4 + else if (hadc.Instance == ADC4) { + // enable interrupt + HAL_NVIC_SetPriority(ADC4_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC4_IRQn); + } +#endif +#ifdef ADC5 + else if (hadc.Instance == ADC5) { + // enable interrupt + HAL_NVIC_SetPriority(ADC5_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(ADC5_IRQn); + } +#endif + + cs_params->adc_handle = &hadc; + return 0; +} + +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC) +{ + uint8_t cnt = 0; + if(_isset(pinA)){ + pinmap_pinout(analogInputToPinName(pinA), PinMap_ADC); + cs_params->pins[cnt++] = pinA; + } + if(_isset(pinB)){ + pinmap_pinout(analogInputToPinName(pinB), PinMap_ADC); + cs_params->pins[cnt++] = pinB; + } + if(_isset(pinC)){ + pinmap_pinout(analogInputToPinName(pinC), PinMap_ADC); + cs_params->pins[cnt] = pinC; + } +} + +extern "C" { + void ADC1_2_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#ifdef ADC3 + void ADC3_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC4 + void ADC4_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif + +#ifdef ADC5 + void ADC5_IRQHandler(void) + { + HAL_ADC_IRQHandler(&hadc); + } +#endif +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h new file mode 100644 index 0000000..0317b74 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_hal.h @@ -0,0 +1,19 @@ +#ifndef STM32L4_LOWSIDE_HAL +#define STM32L4_LOWSIDE_HAL + +#include "Arduino.h" + +#if defined(STM32L4xx) + +#include "stm32l4xx_hal.h" +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../stm32_mcu.h" +#include "stm32l4_utils.h" + +int _adc_init(Stm32CurrentSenseParams* cs_params, const STM32DriverParams* driver_params); +void _adc_gpio_init(Stm32CurrentSenseParams* cs_params, const int pinA, const int pinB, const int pinC); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp new file mode 100644 index 0000000..edac641 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_mcu.cpp @@ -0,0 +1,98 @@ +#include "../../../hardware_api.h" + +#if defined(STM32L4xx) + +#include "../../../../common/foc_utils.h" +#include "../../../../drivers/hardware_api.h" +#include "../../../../drivers/hardware_specific/stm32/stm32_mcu.h" +#include "../../../hardware_api.h" +#include "../stm32_mcu.h" +#include "stm32l4_hal.h" +#include "stm32l4_utils.h" +#include "Arduino.h" + + +#define _ADC_VOLTAGE_L4 3.3f +#define _ADC_RESOLUTION_L4 4096.0f + + +// array of values of 4 injected channels per adc instance (5) +uint32_t adc_val[5][4]={0}; +// does adc interrupt need a downsample - per adc (5) +bool needs_downsample[5] = {1}; +// downsampling variable - per adc (5) +uint8_t tim_downsample[5] = {0}; + + +void* _configureADCLowSide(const void* driver_params, const int pinA, const int pinB, const int pinC){ + + Stm32CurrentSenseParams* cs_params= new Stm32CurrentSenseParams { + .pins={(int)NOT_SET, (int)NOT_SET, (int)NOT_SET}, + .adc_voltage_conv = (_ADC_VOLTAGE_L4) / (_ADC_RESOLUTION_L4) + }; + _adc_gpio_init(cs_params, pinA,pinB,pinC); + if(_adc_init(cs_params, (STM32DriverParams*)driver_params) != 0) return SIMPLEFOC_CURRENT_SENSE_INIT_FAILED; + return cs_params; +} + + +void _driverSyncLowSide(void* _driver_params, void* _cs_params){ + STM32DriverParams* driver_params = (STM32DriverParams*)_driver_params; + Stm32CurrentSenseParams* cs_params = (Stm32CurrentSenseParams*)_cs_params; + + // if compatible timer has not been found + if (cs_params->timer_handle == NULL) return; + + // stop all the timers for the driver + _stopTimers(driver_params->timers, 6); + + // if timer has repetition counter - it will downsample using it + // and it does not need the software downsample + if( IS_TIM_REPETITION_COUNTER_INSTANCE(cs_params->timer_handle->getHandle()->Instance) ){ + // adjust the initial timer state such that the trigger + // - for DMA transfer aligns with the pwm peaks instead of throughs. + // - for interrupt based ADC transfer + // - only necessary for the timers that have repetition counters + cs_params->timer_handle->getHandle()->Instance->CR1 |= TIM_CR1_DIR; + cs_params->timer_handle->getHandle()->Instance->CNT = cs_params->timer_handle->getHandle()->Instance->ARR; + // remember that this timer has repetition counter - no need to downasmple + needs_downsample[_adcToIndex(cs_params->adc_handle)] = 0; + } + + // set the trigger output event + LL_TIM_SetTriggerOutput(cs_params->timer_handle->getHandle()->Instance, LL_TIM_TRGO_UPDATE); + // start the adc + HAL_ADCEx_InjectedStart_IT(cs_params->adc_handle); + // restart all the timers of the driver + _startTimers(driver_params->timers, 6); +} + + +// function reading an ADC value and returning the read voltage +float _readADCVoltageLowSide(const int pin, const void* cs_params){ + for(int i=0; i < 3; i++){ + if( pin == ((Stm32CurrentSenseParams*)cs_params)->pins[i]) // found in the buffer + return adc_val[_adcToIndex(((Stm32CurrentSenseParams*)cs_params)->adc_handle)][i] * ((Stm32CurrentSenseParams*)cs_params)->adc_voltage_conv; + } + return 0; +} + + +extern "C" { + void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ + // calculate the instance + int adc_index = _adcToIndex(AdcHandle); + + // if the timer han't repetition counter - downsample two times + if( needs_downsample[adc_index] && tim_downsample[adc_index]++ > 0) { + tim_downsample[adc_index] = 0; + return; + } + + adc_val[adc_index][0]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_1); + adc_val[adc_index][1]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_2); + adc_val[adc_index][2]=HAL_ADCEx_InjectedGetValue(AdcHandle, ADC_INJECTED_RANK_3); + } +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp new file mode 100644 index 0000000..376d9d6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.cpp @@ -0,0 +1,221 @@ +#include "stm32l4_utils.h" + +#if defined(STM32L4xx) + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin) +{ + uint32_t function = pinmap_function(pin, PinMap_ADC); + uint32_t channel = 0; + switch (STM_PIN_CHANNEL(function)) { +#ifdef ADC_CHANNEL_0 + case 0: + channel = ADC_CHANNEL_0; + break; +#endif + case 1: + channel = ADC_CHANNEL_1; + break; + case 2: + channel = ADC_CHANNEL_2; + break; + case 3: + channel = ADC_CHANNEL_3; + break; + case 4: + channel = ADC_CHANNEL_4; + break; + case 5: + channel = ADC_CHANNEL_5; + break; + case 6: + channel = ADC_CHANNEL_6; + break; + case 7: + channel = ADC_CHANNEL_7; + break; + case 8: + channel = ADC_CHANNEL_8; + break; + case 9: + channel = ADC_CHANNEL_9; + break; + case 10: + channel = ADC_CHANNEL_10; + break; + case 11: + channel = ADC_CHANNEL_11; + break; + case 12: + channel = ADC_CHANNEL_12; + break; + case 13: + channel = ADC_CHANNEL_13; + break; + case 14: + channel = ADC_CHANNEL_14; + break; + case 15: + channel = ADC_CHANNEL_15; + break; +#ifdef ADC_CHANNEL_16 + case 16: + channel = ADC_CHANNEL_16; + break; +#endif + case 17: + channel = ADC_CHANNEL_17; + break; +#ifdef ADC_CHANNEL_18 + case 18: + channel = ADC_CHANNEL_18; + break; +#endif +#ifdef ADC_CHANNEL_19 + case 19: + channel = ADC_CHANNEL_19; + break; +#endif +#ifdef ADC_CHANNEL_20 + case 20: + channel = ADC_CHANNEL_20; + break; + case 21: + channel = ADC_CHANNEL_21; + break; + case 22: + channel = ADC_CHANNEL_22; + break; + case 23: + channel = ADC_CHANNEL_23; + break; +#ifdef ADC_CHANNEL_24 + case 24: + channel = ADC_CHANNEL_24; + break; + case 25: + channel = ADC_CHANNEL_25; + break; + case 26: + channel = ADC_CHANNEL_26; + break; +#ifdef ADC_CHANNEL_27 + case 27: + channel = ADC_CHANNEL_27; + break; + case 28: + channel = ADC_CHANNEL_28; + break; + case 29: + channel = ADC_CHANNEL_29; + break; + case 30: + channel = ADC_CHANNEL_30; + break; + case 31: + channel = ADC_CHANNEL_31; + break; +#endif +#endif +#endif + default: + _Error_Handler("ADC: Unknown adc channel", (int)(STM_PIN_CHANNEL(function))); + break; + } + return channel; +} + + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/e156c32db24d69cb4818208ccc28894e2f427cfa/system/Drivers/STM32L4xx_HAL_Driver/Inc/stm32l4xx_hal_adc_ex.h#L210 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIGINJEC_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIGINJEC_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIGINJEC_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIGINJEC_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIGINJEC_T6_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIGINJEC_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIGINJEC_T15_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer){ + if(timer->getHandle()->Instance == TIM1) + return ADC_EXTERNALTRIG_T1_TRGO; +#ifdef TIM2 // if defined timer 2 + else if(timer->getHandle()->Instance == TIM2) + return ADC_EXTERNALTRIG_T2_TRGO; +#endif +#ifdef TIM3 // if defined timer 3 + else if(timer->getHandle()->Instance == TIM3) + return ADC_EXTERNALTRIG_T3_TRGO; +#endif +#ifdef TIM4 // if defined timer 4 + else if(timer->getHandle()->Instance == TIM4) + return ADC_EXTERNALTRIG_T4_TRGO; +#endif +#ifdef TIM6 // if defined timer 6 + else if(timer->getHandle()->Instance == TIM6) + return ADC_EXTERNALTRIG_T6_TRGO; +#endif +#ifdef TIM8 // if defined timer 8 + else if(timer->getHandle()->Instance == TIM8) + return ADC_EXTERNALTRIG_T8_TRGO; +#endif +#ifdef TIM15 // if defined timer 15 + else if(timer->getHandle()->Instance == TIM15) + return ADC_EXTERNALTRIG_T15_TRGO; +#endif + else + return _TRGO_NOT_AVAILABLE; +} + + +int _adcToIndex(ADC_TypeDef *AdcHandle){ + if(AdcHandle == ADC1) return 0; +#ifdef ADC2 // if ADC2 exists + else if(AdcHandle == ADC2) return 1; +#endif +#ifdef ADC3 // if ADC3 exists + else if(AdcHandle == ADC3) return 2; +#endif +#ifdef ADC4 // if ADC4 exists + else if(AdcHandle == ADC4) return 3; +#endif +#ifdef ADC5 // if ADC5 exists + else if(AdcHandle == ADC5) return 4; +#endif + return 0; +} +int _adcToIndex(ADC_HandleTypeDef *AdcHandle){ + return _adcToIndex(AdcHandle->Instance); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h new file mode 100644 index 0000000..ceef9be --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/stm32/stm32l4/stm32l4_utils.h @@ -0,0 +1,34 @@ + +#ifndef STM32L4_UTILS_HAL +#define STM32L4_UTILS_HAL + +#include "Arduino.h" + +#if defined(STM32L4xx) + +#define _TRGO_NOT_AVAILABLE 12345 + + +/* Exported Functions */ +/** + * @brief Return ADC HAL channel linked to a PinName + * @param pin: PinName + * @retval Valid HAL channel + */ +uint32_t _getADCChannel(PinName pin); + +// timer to injected TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc_ex.h#L217 +uint32_t _timerToInjectedTRGO(HardwareTimer* timer); + +// timer to regular TRGO +// https://github.com/stm32duino/Arduino_Core_STM32/blob/6588dee03382e73ed42c4a5e473900ab3b79d6e4/system/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_adc.h#L519 +uint32_t _timerToRegularTRGO(HardwareTimer* timer); + +// function returning index of the ADC instance +int _adcToIndex(ADC_HandleTypeDef *AdcHandle); +int _adcToIndex(ADC_TypeDef *AdcHandle); + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp new file mode 100644 index 0000000..7ab370a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/current_sense/hardware_specific/teensy_mcu.cpp @@ -0,0 +1,24 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +#define _ADC_VOLTAGE 3.3f +#define _ADC_RESOLUTION 1024.0f + +// function reading an ADC value and returning the read voltage +void* _configureADCInline(const void* driver_params, const int pinA,const int pinB,const int pinC){ + _UNUSED(driver_params); + + if( _isset(pinA) ) pinMode(pinA, INPUT); + if( _isset(pinB) ) pinMode(pinB, INPUT); + if( _isset(pinC) ) pinMode(pinC, INPUT); + + GenericCurrentSenseParams* params = new GenericCurrentSenseParams { + .pins = { pinA, pinB, pinC }, + .adc_voltage_conv = (_ADC_VOLTAGE)/(_ADC_RESOLUTION) + }; + + return params; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.cpp new file mode 100644 index 0000000..637c8db --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.cpp @@ -0,0 +1,92 @@ +#include "BLDCDriver3PWM.h" + +BLDCDriver3PWM::BLDCDriver3PWM(int phA, int phB, int phC, int en1, int en2, int en3){ + // Pin initialization + pwmA = phA; + pwmB = phB; + pwmC = phC; + + // enable_pin pin + enableA_pin = en1; + enableB_pin = en2; + enableC_pin = en3; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void BLDCDriver3PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enableA_pin) ) digitalWrite(enableA_pin, enable_active_high); + if ( _isset(enableB_pin) ) digitalWrite(enableB_pin, enable_active_high); + if ( _isset(enableC_pin) ) digitalWrite(enableC_pin, enable_active_high); + // set zero to PWM + setPwm(0,0,0); +} + +// disable motor driver +void BLDCDriver3PWM::disable() +{ + // set zero to PWM + setPwm(0, 0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enableA_pin) ) digitalWrite(enableA_pin, !enable_active_high); + if ( _isset(enableB_pin) ) digitalWrite(enableB_pin, !enable_active_high); + if ( _isset(enableC_pin) ) digitalWrite(enableC_pin, !enable_active_high); + +} + +// init hardware pins +int BLDCDriver3PWM::init() { + // PWM pins + pinMode(pwmA, OUTPUT); + pinMode(pwmB, OUTPUT); + pinMode(pwmC, OUTPUT); + if( _isset(enableA_pin)) pinMode(enableA_pin, OUTPUT); + if( _isset(enableB_pin)) pinMode(enableB_pin, OUTPUT); + if( _isset(enableC_pin)) pinMode(enableC_pin, OUTPUT); + + + // sanity check for the voltage limit configuration + if(!_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure3PWM(pwm_frequency, pwmA, pwmB, pwmC); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + + +// Set voltage to the pwm pin +void BLDCDriver3PWM::setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) { + // disable if needed + if( _isset(enableA_pin) && _isset(enableB_pin) && _isset(enableC_pin) ){ + digitalWrite(enableA_pin, sa == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + digitalWrite(enableB_pin, sb == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + digitalWrite(enableC_pin, sc == PhaseState::PHASE_ON ? enable_active_high:!enable_active_high); + } +} + +// Set voltage to the pwm pin +void BLDCDriver3PWM::setPwm(float Ua, float Ub, float Uc) { + + // limit the voltage in driver + Ua = _constrain(Ua, 0.0f, voltage_limit); + Ub = _constrain(Ub, 0.0f, voltage_limit); + Uc = _constrain(Uc, 0.0f, voltage_limit); + // calculate duty cycle + // limited in [0,1] + dc_a = _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); + dc_b = _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); + dc_c = _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); + + // hardware specific writing + // hardware specific function - depending on driver and mcu + _writeDutyCycle3PWM(dc_a, dc_b, dc_c, params); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.h new file mode 100644 index 0000000..1942f60 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver3PWM.h @@ -0,0 +1,64 @@ +#ifndef BLDCDriver3PWM_h +#define BLDCDriver3PWM_h + +#include "../common/base_classes/BLDCDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 3 pwm bldc driver class +*/ +class BLDCDriver3PWM: public BLDCDriver +{ + public: + /** + BLDCDriver class constructor + @param phA A phase pwm pin + @param phB B phase pwm pin + @param phC C phase pwm pin + @param en1 enable pin (optional input) + @param en2 enable pin (optional input) + @param en3 enable pin (optional input) + */ + BLDCDriver3PWM(int phA,int phB,int phC, int en1 = NOT_SET, int en2 = NOT_SET, int en3 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwmA; //!< phase A pwm pin number + int pwmB; //!< phase B pwm pin number + int pwmC; //!< phase C pwm pin number + int enableA_pin; //!< enable pin number + int enableB_pin; //!< enable pin number + int enableC_pin; //!< enable pin number + bool enable_active_high = true; + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + void setPwm(float Ua, float Ub, float Uc) override; + + /** + * Set phase voltages to the harware + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) override; + private: +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.cpp new file mode 100644 index 0000000..4981858 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.cpp @@ -0,0 +1,103 @@ +#include "BLDCDriver6PWM.h" + +BLDCDriver6PWM::BLDCDriver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int en){ + // Pin initialization + pwmA_h = phA_h; + pwmB_h = phB_h; + pwmC_h = phC_h; + pwmA_l = phA_l; + pwmB_l = phB_l; + pwmC_l = phC_l; + + // enable_pin pin + enable_pin = en; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + + // dead zone initial - 2% + dead_zone = 0.02f; + +} + +// enable motor driver +void BLDCDriver6PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin) ) digitalWrite(enable_pin, enable_active_high); + // set phase state enabled + setPhaseState(PhaseState::PHASE_ON, PhaseState::PHASE_ON, PhaseState::PHASE_ON); + // set zero to PWM + setPwm(0, 0, 0); +} + +// disable motor driver +void BLDCDriver6PWM::disable() +{ + // set phase state to disabled + setPhaseState(PhaseState::PHASE_OFF, PhaseState::PHASE_OFF, PhaseState::PHASE_OFF); + // set zero to PWM + setPwm(0, 0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin) ) digitalWrite(enable_pin, !enable_active_high); + +} + +// init hardware pins +int BLDCDriver6PWM::init() { + + // PWM pins + pinMode(pwmA_h, OUTPUT); + pinMode(pwmB_h, OUTPUT); + pinMode(pwmC_h, OUTPUT); + pinMode(pwmA_l, OUTPUT); + pinMode(pwmB_l, OUTPUT); + pinMode(pwmC_l, OUTPUT); + if(_isset(enable_pin)) pinMode(enable_pin, OUTPUT); + + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // set phase state to disabled + phase_state[0] = PhaseState::PHASE_OFF; + phase_state[1] = PhaseState::PHASE_OFF; + phase_state[2] = PhaseState::PHASE_OFF; + + // set zero to PWM + dc_a = dc_b = dc_c = 0; + + // configure 6pwm + // hardware specific function - depending on driver and mcu + params = _configure6PWM(pwm_frequency, dead_zone, pwmA_h,pwmA_l, pwmB_h,pwmB_l, pwmC_h,pwmC_l); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void BLDCDriver6PWM::setPwm(float Ua, float Ub, float Uc) { + // limit the voltage in driver + Ua = _constrain(Ua, 0, voltage_limit); + Ub = _constrain(Ub, 0, voltage_limit); + Uc = _constrain(Uc, 0, voltage_limit); + // calculate duty cycle + // limited in [0,1] + dc_a = _constrain(Ua / voltage_power_supply, 0.0f , 1.0f ); + dc_b = _constrain(Ub / voltage_power_supply, 0.0f , 1.0f ); + dc_c = _constrain(Uc / voltage_power_supply, 0.0f , 1.0f ); + // hardware specific writing + // hardware specific function - depending on driver and mcu + _writeDutyCycle6PWM(dc_a, dc_b, dc_c, phase_state, params); +} + + +// Set the phase state +// actually changing the state is only done on the next call to setPwm, and depends +// on the hardware capabilities of the driver and MCU. +void BLDCDriver6PWM::setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) { + phase_state[0] = sa; + phase_state[1] = sb; + phase_state[2] = sc; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.h new file mode 100644 index 0000000..e8643cc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/BLDCDriver6PWM.h @@ -0,0 +1,70 @@ +#ifndef BLDCDriver6PWM_h +#define BLDCDriver6PWM_h + +#include "../common/base_classes/BLDCDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 6 pwm bldc driver class +*/ +class BLDCDriver6PWM: public BLDCDriver +{ + public: + /** + BLDCDriver class constructor + @param phA_h A phase pwm pin + @param phA_l A phase pwm pin + @param phB_h B phase pwm pin + @param phB_l A phase pwm pin + @param phC_h C phase pwm pin + @param phC_l A phase pwm pin + @param en enable pin (optional input) + */ + BLDCDriver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int en = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwmA_h,pwmA_l; //!< phase A pwm pin number + int pwmB_h,pwmB_l; //!< phase B pwm pin number + int pwmC_h,pwmC_l; //!< phase C pwm pin number + int enable_pin; //!< enable pin number + bool enable_active_high = true; + + float dead_zone; //!< a percentage of dead-time(zone) (both high and low side in low) for each pwm cycle [0,1] + + PhaseState phase_state[3]; //!< phase state (active / disabled) + + + /** + * Set phase voltages to the harware + * + * @param Ua - phase A voltage + * @param Ub - phase B voltage + * @param Uc - phase C voltage + */ + void setPwm(float Ua, float Ub, float Uc) override; + + /** + * Set phase voltages to the harware + * + * @param sc - phase A state : active / disabled ( high impedance ) + * @param sb - phase B state : active / disabled ( high impedance ) + * @param sa - phase C state : active / disabled ( high impedance ) + */ + virtual void setPhaseState(PhaseState sa, PhaseState sb, PhaseState sc) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.cpp new file mode 100644 index 0000000..dbbf5b8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.cpp @@ -0,0 +1,107 @@ +#include "StepperDriver2PWM.h" + +StepperDriver2PWM::StepperDriver2PWM(int _pwm1, int* _in1, int _pwm2, int* _in2, int en1, int en2){ + // Pin initialization + pwm1 = _pwm1; // phase 1 pwm pin number + dir1a = _in1[0]; // phase 1 INA pin number + dir1b = _in1[1]; // phase 1 INB pin number + pwm2 = _pwm2; // phase 2 pwm pin number + dir2a = _in2[0]; // phase 2 INA pin number + dir2b = _in2[1]; // phase 2 INB pin number + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +StepperDriver2PWM::StepperDriver2PWM(int _pwm1, int _dir1, int _pwm2, int _dir2, int en1, int en2){ + // Pin initialization + pwm1 = _pwm1; // phase 1 pwm pin number + dir1a = _dir1; // phase 1 direction pin + pwm2 = _pwm2; // phase 2 pwm pin number + dir2a = _dir2; // phase 2 direction pin + // these pins are not used + dir1b = NOT_SET; + dir2b = NOT_SET; + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void StepperDriver2PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, HIGH); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, HIGH); + // set zero to PWM + setPwm(0,0); +} + +// disable motor driver +void StepperDriver2PWM::disable() +{ + // set zero to PWM + setPwm(0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, LOW); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, LOW); + +} + +// init hardware pins +int StepperDriver2PWM::init() { + // PWM pins + pinMode(pwm1, OUTPUT); + pinMode(pwm2, OUTPUT); + pinMode(dir1a, OUTPUT); + pinMode(dir2a, OUTPUT); + if( _isset(dir1b) ) pinMode(dir1b, OUTPUT); + if( _isset(dir2b) ) pinMode(dir2b, OUTPUT); + + if( _isset(enable_pin1) ) pinMode(enable_pin1, OUTPUT); + if( _isset(enable_pin2) ) pinMode(enable_pin2, OUTPUT); + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure2PWM(pwm_frequency, pwm1, pwm2); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void StepperDriver2PWM::setPwm(float Ua, float Ub) { + float duty_cycle1(0.0f),duty_cycle2(0.0f); + // limit the voltage in driver + Ua = _constrain(Ua, -voltage_limit, voltage_limit); + Ub = _constrain(Ub, -voltage_limit, voltage_limit); + // hardware specific writing + duty_cycle1 = _constrain(abs(Ua)/voltage_power_supply,0.0f,1.0f); + duty_cycle2 = _constrain(abs(Ub)/voltage_power_supply,0.0f,1.0f); + + // phase 1 direction + digitalWrite(dir1a, Ua >= 0 ? LOW : HIGH); + if( _isset(dir1b) ) digitalWrite(dir1b, Ua >= 0 ? HIGH : LOW ); + // phase 2 direction + digitalWrite(dir2a, Ub >= 0 ? LOW : HIGH); + if( _isset(dir2b) ) digitalWrite(dir2b, Ub >= 0 ? HIGH : LOW ); + + // write to hardware + _writeDutyCycle2PWM(duty_cycle1, duty_cycle2, params); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.h new file mode 100644 index 0000000..b349af0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver2PWM.h @@ -0,0 +1,68 @@ +#ifndef STEPPER_DRIVER_2PWM_h +#define STEPPER_DRIVER_2PWM_h + +#include "../common/base_classes/StepperDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 2 pwm stepper driver class +*/ +class StepperDriver2PWM: public StepperDriver +{ + public: + /** + StepperMotor class constructor + @param pwm1 PWM1 phase pwm pin + @param in1 IN1A phase dir pin + @param pwm2 PWM2 phase pwm pin + @param in2 IN2A phase dir + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver2PWM(int pwm1, int* in1, int pwm2, int* in2, int en1 = NOT_SET, int en2 = NOT_SET); + + /** + StepperMotor class constructor + @param pwm1 PWM1 phase pwm pin + @param dir1 DIR1 phase dir pin + @param pwm2 PWM2 phase pwm pin + @param dir2 DIR2 phase dir pin + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver2PWM(int pwm1, int dir1, int pwm2, int dir2, int en1 = NOT_SET, int en2 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwm1; //!< phase 1 pwm pin number + int dir1a; //!< phase 1 INA pin number + int dir1b; //!< phase 1 INB pin number + int pwm2; //!< phase 2 pwm pin number + int dir2a; //!< phase 2 INA pin number + int dir2b; //!< phase 2 INB pin number + int enable_pin1; //!< enable pin number phase 1 + int enable_pin2; //!< enable pin number phase 2 + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + void setPwm(float Ua, float Ub) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.cpp new file mode 100644 index 0000000..836f547 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.cpp @@ -0,0 +1,81 @@ +#include "StepperDriver4PWM.h" + +StepperDriver4PWM::StepperDriver4PWM(int ph1A,int ph1B,int ph2A,int ph2B,int en1, int en2){ + // Pin initialization + pwm1A = ph1A; + pwm1B = ph1B; + pwm2A = ph2A; + pwm2B = ph2B; + + // enable_pin pin + enable_pin1 = en1; + enable_pin2 = en2; + + // default power-supply value + voltage_power_supply = DEF_POWER_SUPPLY; + voltage_limit = NOT_SET; + pwm_frequency = NOT_SET; + +} + +// enable motor driver +void StepperDriver4PWM::enable(){ + // enable_pin the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, HIGH); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, HIGH); + // set zero to PWM + setPwm(0,0); +} + +// disable motor driver +void StepperDriver4PWM::disable() +{ + // set zero to PWM + setPwm(0, 0); + // disable the driver - if enable_pin pin available + if ( _isset(enable_pin1) ) digitalWrite(enable_pin1, LOW); + if ( _isset(enable_pin2) ) digitalWrite(enable_pin2, LOW); + +} + +// init hardware pins +int StepperDriver4PWM::init() { + + // PWM pins + pinMode(pwm1A, OUTPUT); + pinMode(pwm1B, OUTPUT); + pinMode(pwm2A, OUTPUT); + pinMode(pwm2B, OUTPUT); + if( _isset(enable_pin1) ) pinMode(enable_pin1, OUTPUT); + if( _isset(enable_pin2) ) pinMode(enable_pin2, OUTPUT); + + // sanity check for the voltage limit configuration + if( !_isset(voltage_limit) || voltage_limit > voltage_power_supply) voltage_limit = voltage_power_supply; + + // Set the pwm frequency to the pins + // hardware specific function - depending on driver and mcu + params = _configure4PWM(pwm_frequency, pwm1A, pwm1B, pwm2A, pwm2B); + initialized = (params!=SIMPLEFOC_DRIVER_INIT_FAILED); + return params!=SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// Set voltage to the pwm pin +void StepperDriver4PWM::setPwm(float Ualpha, float Ubeta) { + float duty_cycle1A(0.0f),duty_cycle1B(0.0f),duty_cycle2A(0.0f),duty_cycle2B(0.0f); + // limit the voltage in driver + Ualpha = _constrain(Ualpha, -voltage_limit, voltage_limit); + Ubeta = _constrain(Ubeta, -voltage_limit, voltage_limit); + // hardware specific writing + if( Ualpha > 0 ) + duty_cycle1B = _constrain(abs(Ualpha)/voltage_power_supply,0.0f,1.0f); + else + duty_cycle1A = _constrain(abs(Ualpha)/voltage_power_supply,0.0f,1.0f); + + if( Ubeta > 0 ) + duty_cycle2B = _constrain(abs(Ubeta)/voltage_power_supply,0.0f,1.0f); + else + duty_cycle2A = _constrain(abs(Ubeta)/voltage_power_supply,0.0f,1.0f); + // write to hardware + _writeDutyCycle4PWM(duty_cycle1A, duty_cycle1B, duty_cycle2A, duty_cycle2B, params); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.h new file mode 100644 index 0000000..e4b2ee4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/StepperDriver4PWM.h @@ -0,0 +1,55 @@ +#ifndef STEPPER_DRIVER_4PWM_h +#define STEPPER_DRIVER_4PWM_h + +#include "../common/base_classes/StepperDriver.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/defaults.h" +#include "hardware_api.h" + +/** + 4 pwm stepper driver class +*/ +class StepperDriver4PWM: public StepperDriver +{ + public: + /** + StepperMotor class constructor + @param ph1A 1A phase pwm pin + @param ph1B 1B phase pwm pin + @param ph2A 2A phase pwm pin + @param ph2B 2B phase pwm pin + @param en1 enable pin phase 1 (optional input) + @param en2 enable pin phase 2 (optional input) + */ + StepperDriver4PWM(int ph1A,int ph1B,int ph2A,int ph2B, int en1 = NOT_SET, int en2 = NOT_SET); + + /** Motor hardware init function */ + int init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + // hardware variables + int pwm1A; //!< phase 1A pwm pin number + int pwm1B; //!< phase 1B pwm pin number + int pwm2A; //!< phase 2A pwm pin number + int pwm2B; //!< phase 2B pwm pin number + int enable_pin1; //!< enable pin number phase 1 + int enable_pin2; //!< enable pin number phase 2 + + /** + * Set phase voltages to the harware + * + * @param Ua phase A voltage + * @param Ub phase B voltage + */ + void setPwm(float Ua, float Ub) override; + + private: + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_api.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_api.h new file mode 100644 index 0000000..8b47745 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_api.h @@ -0,0 +1,180 @@ +#ifndef HARDWARE_UTILS_DRIVER_H +#define HARDWARE_UTILS_DRIVER_H + +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../communication/SimpleFOCDebug.h" +#include "../common/base_classes/BLDCDriver.h" + + +// these defines determine the polarity of the PWM output. Normally, the polarity is active-high, +// i.e. a high-level PWM output is expected to switch on the MOSFET. But should your driver design +// require inverted polarity, you can change the defines below, or set them via your build environment +// or board definition files. + +// used for 1-PWM, 2-PWM, 3-PWM, and 4-PWM modes +#ifndef SIMPLEFOC_PWM_ACTIVE_HIGH +#define SIMPLEFOC_PWM_ACTIVE_HIGH true +#endif +// used for 6-PWM mode, high-side +#ifndef SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH +#define SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH true +#endif +// used for 6-PWM mode, low-side +#ifndef SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH +#define SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH true +#endif + + + + +// flag returned if driver init fails +#define SIMPLEFOC_DRIVER_INIT_FAILED ((void*)-1) + +// generic implementation of the hardware specific structure +// containing all the necessary driver parameters +// will be returned as a void pointer from the _configurexPWM functions +// will be provided to the _writeDutyCyclexPWM() as a void pointer +typedef struct GenericDriverParams { + int pins[6]; + long pwm_frequency; + float dead_zone; +} GenericDriverParams; + + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA pwm pin + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure1PWM(long pwm_frequency, const int pinA); + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB); + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * @param pinC pinC bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC); + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pin1A pin1A stepper driver + * @param pin1B pin1B stepper driver + * @param pin2A pin2A stepper driver + * @param pin2B pin2B stepper driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B); + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 6PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low - if applicable + * @param pinA_h pinA high-side bldc driver + * @param pinA_l pinA low-side bldc driver + * @param pinB_h pinA high-side bldc driver + * @param pinB_l pinA low-side bldc driver + * @param pinC_h pinA high-side bldc driver + * @param pinC_l pinA low-side bldc driver + * + * @return -1 if failed, or pointer to internal driver parameters struct if successful + */ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle1PWM(float dc_a, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params); + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param dc_1a duty cycle phase 1A [0, 1] + * @param dc_1b duty cycle phase 1B [0, 1] + * @param dc_2a duty cycle phase 2A [0, 1] + * @param dc_2b duty cycle phase 2B [0, 1] + * @param params the driver parameters + */ +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params); + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 6PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param phase_state pointer to PhaseState[3] array + * @param params the driver parameters + * + */ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params); + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp new file mode 100644 index 0000000..8a7fbbe --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega2560_mcu.cpp @@ -0,0 +1,278 @@ +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega2560__) || defined(AVR_ATmega1280) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega2560 or Arduino/ATmega1280") +#pragma message("") + + +#define _PWM_FREQUENCY 32000 +#define _PWM_FREQUENCY_MAX 32000 +#define _PWM_FREQUENCY_MIN 4000 + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin, const long frequency){ + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + // High PWM frequency + // https://sites.google.com/site/qeewiki/books/avr-guide/timers-on-the-ATmega2560 + // https://forum.arduino.cc/index.php?topic=72092.0 + if (pin == 13 || pin == 4 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + } + else if (pin == 12 || pin == 11 ) + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 10 || pin == 9 ) + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 5 || pin == 3 || pin == 2) + if(high_fq) TCCR3B = ((TCCR3B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR3B = ((TCCR3B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 8 || pin == 7 || pin == 6) + if(high_fq) TCCR4B = ((TCCR4B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR4B = ((TCCR4B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + else if (pin == 44 || pin == 45 || pin == 46) + if(high_fq) TCCR5B = ((TCCR5B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR5B = ((TCCR5B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure1PWM(long pwm_frequency,const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + _pinHighFrequency(pinB, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA, pwm_frequency); + _pinHighFrequency(pinB, pwm_frequency); + _pinHighFrequency(pinC, pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + // _syncAllTimers(); + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware specific +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware specific +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A,pwm_frequency); + _pinHighFrequency(pin1B,pwm_frequency); + _pinHighFrequency(pin2A,pwm_frequency); + _pinHighFrequency(pin2B,pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware specific +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +// supports Arduino/ATmega2560 +// https://ww1.microchip.com/downloads/en/devicedoc/atmel-2549-8-bit-avr-microcontroller-atmega640-1280-1281-2560-2561_datasheet.pdf +// https://docs.arduino.cc/hacking/hardware/PinMapping2560 +int _configureComplementaryPair(const int pinH,const int pinL, long frequency) { + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + + // configure pin pairs + if( (pinH == 4 && pinL == 13 ) || (pinH == 13 && pinL == 4 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 13 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set frequency + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if( (pinH == 11 && pinL == 12 ) || (pinH == 12 && pinL == 11 ) ){ + // set frequency + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 11 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 10 && pinL == 9 ) || (pinH == 9 && pinL == 10 ) ){ + // set frequency + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 10 ) TCCR2A = 0b10110000 | (TCCR2A & 0b00001111) ; + else TCCR2A = 0b11100000 | (TCCR2A & 0b00001111) ; + }else if((pinH == 5 && pinL == 2 ) || (pinH == 2 && pinL == 5 ) ){ + // set frequency + if(high_fq) TCCR3B = ((TCCR3B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR3B = ((TCCR3B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 5 ) TCCR3A = 0b10110000 | (TCCR3A & 0b00001111) ; + else TCCR3A = 0b11100000 | (TCCR3A & 0b00001111) ; + }else if((pinH == 6 && pinL == 7 ) || (pinH == 7 && pinL == 6 ) ){ + // set frequency + if(high_fq) TCCR4B = ((TCCR4B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR4B = ((TCCR4B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 6 ) TCCR4A = 0b10110000 | (TCCR4A & 0b00001111) ; + else TCCR4A = 0b11100000 | (TCCR4A & 0b00001111) ; + }else if((pinH == 46 && pinL == 45 ) || (pinH == 45 && pinL == 46 ) ){ + // set frequency + if(high_fq) TCCR5B = ((TCCR5B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR5B = ((TCCR5B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 46 ) TCCR5A = 0b10110000 | (TCCR5A & 0b00001111) ; + else TCCR5A = 0b11100000 | (TCCR5A & 0b00001111) ; + }else{ + return -1; + } + return 0; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - setting +// - hardware specific +// supports Arduino/ATmega2560 +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l, pwm_frequency); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + // _syncAllTimers(); + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time, PhaseState ps) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + // determine the phase state and set the pwm accordingly + // deactivate phases if needed + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_LO)){ + digitalWrite(pinH, LOW); + }else{ + analogWrite(pinH, pwm_h); + } + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_HI)){ + digitalWrite(pinL, LOW); + }else{ + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); + } + +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[0]); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[1]); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[2]); +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp new file mode 100644 index 0000000..53fb108 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega328_mcu.cpp @@ -0,0 +1,255 @@ +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__) || defined(__AVR_ATmega328PB__) + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega328 ATmega168 ATmega328PB") +#pragma message("") + +#define _PWM_FREQUENCY 32000 +#define _PWM_FREQUENCY_MAX 32000 +#define _PWM_FREQUENCY_MIN 4000 + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin, const long frequency){ + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + // High PWM frequency + // https://www.arxterra.com/9-atmega328p-timers/ + if (pin == 5 || pin == 6 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if (pin == 9 || pin == 10 ){ + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if (pin == 3 || pin == 11){ + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + } +} + +void _syncAllTimers(){ + GTCCR = (1<pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware specific +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A,pwm_frequency); + _pinHighFrequency(pin1B,pwm_frequency); + _pinHighFrequency(pin2A,pwm_frequency); + _pinHighFrequency(pin2B,pwm_frequency); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware specific +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +int _configureComplementaryPair(const int pinH, const int pinL, long frequency) { + bool high_fq = false; + // set 32kHz frequency if requested freq is higher than the middle of the range (14kHz) + // else set the 4kHz + if(frequency >= 0.5*(_PWM_FREQUENCY_MAX-_PWM_FREQUENCY_MIN)) high_fq=true; + + // configure pins + if( (pinH == 5 && pinL == 6 ) || (pinH == 6 && pinL == 5 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 6 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set frequency + if(high_fq) TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR0B = ((TCCR0B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + }else if( (pinH == 9 && pinL == 10 ) || (pinH == 10 && pinL == 9 ) ){ + // set frequency + if(high_fq) TCCR1B = ((TCCR1B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR1B = ((TCCR1B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 9 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 3 && pinL == 11 ) || (pinH == 11 && pinL == 3 ) ){ + // set frequency + if(high_fq) TCCR2B = ((TCCR2B & 0b11110000) | 0x01); // set prescaler to 1 - 32kHz + else TCCR2B = ((TCCR2B & 0b11110000) | 0x02); // set prescaler to 2 - 4kHz + // configure complementary pwm on low side + if(pinH == 11 ) TCCR2A = 0b10110000 | (TCCR2A & 0b00001111) ; + else TCCR2A = 0b11100000 | (TCCR2A & 0b00001111) ; + }else{ + return -1; + } + return 0; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - setting +// - hardware specific +// supports Arduino/ATmega328 +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 32kHz + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); // constrain to 4-32kHz max + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l, pwm_frequency); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l, pwm_frequency); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + _syncAllTimers(); + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time, PhaseState ps) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + // determine the phase state and set the pwm accordingly + // deactivate phases if needed + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_LO)){ + digitalWrite(pinH, LOW); + }else{ + analogWrite(pinH, pwm_h); + } + if((ps == PhaseState::PHASE_OFF) || (ps == PhaseState::PHASE_HI)){ + digitalWrite(pinL, LOW); + }else{ + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); + } + + +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arduino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[0]); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[1]); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0, phase_state[2]); +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp new file mode 100644 index 0000000..b4ad310 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/atmega/atmega32u4_mcu.cpp @@ -0,0 +1,222 @@ + +#include "../../hardware_api.h" + +#if defined(__AVR_ATmega32U4__) + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/ATmega32U4") +#pragma message("") + +// set pwm frequency to 32KHz +void _pinHighFrequency(const int pin){ + // High PWM frequency + // reference: http://r6500.blogspot.com/2014/12/fast-pwm-on-arduino-leonardo.html + if (pin == 3 || pin == 11 ) { + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + TCCR0B = ((TCCR0B & 0b11110000) | 0x01); // set prescaler to 1 + } + else if (pin == 9 || pin == 10 ) + TCCR1B = ((TCCR1B & 0b11111000) | 0x01); // set prescaler to 1 + else if (pin == 5 ) + TCCR3B = ((TCCR3B & 0b11111000) | 0x01); // set prescaler to 1 + else if ( pin == 6 || pin == 13 ) { // a bit more complicated 10 bit timer + // PLL Configuration + PLLFRQ= ((PLLFRQ & 0b11001111) | 0x20); // Use 96MHz / 1.5 = 64MHz + TCCR4B = ((TCCR4B & 0b11110000) | 0xB); // configure prescaler to get 64M/2/1024 = 31.25 kHz + TCCR4D = ((TCCR4D & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + + if (pin == 6) TCCR4A = 0x82; // activate channel A - pin 13 + else if (pin == 13) TCCR4C |= 0x09; // Activate channel D - pin 6 + } + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency,const int pinA) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + _pinHighFrequency(pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pinA); + _pinHighFrequency(pinB); + _pinHighFrequency(pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, int pinA, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + // High PWM frequency + // - always max 32kHz + _pinHighFrequency(pin1A); + _pinHighFrequency(pin1B); + _pinHighFrequency(pin2A); + _pinHighFrequency(pin2B); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + + + +// function configuring pair of high-low side pwm channels, 32khz frequency and center aligned pwm +int _configureComplementaryPair(int pinH, int pinL) { + if( (pinH == 3 && pinL == 11 ) || (pinH == 11 && pinL == 3 ) ){ + // configure the pwm phase-corrected mode + TCCR0A = ((TCCR0A & 0b11111100) | 0x01); + // configure complementary pwm on low side + if(pinH == 11 ) TCCR0A = 0b10110000 | (TCCR0A & 0b00001111) ; + else TCCR0A = 0b11100000 | (TCCR0A & 0b00001111) ; + // set prescaler to 1 - 32kHz freq + TCCR0B = ((TCCR0B & 0b11110000) | 0x01); + }else if( (pinH == 9 && pinL == 10 ) || (pinH == 10 && pinL == 9 ) ){ + // set prescaler to 1 - 32kHz freq + TCCR1B = ((TCCR1B & 0b11111000) | 0x01); + // configure complementary pwm on low side + if(pinH == 9 ) TCCR1A = 0b10110000 | (TCCR1A & 0b00001111) ; + else TCCR1A = 0b11100000 | (TCCR1A & 0b00001111) ; + }else if((pinH == 6 && pinL == 13 ) || (pinH == 13 && pinL == 6 ) ){ + // PLL Configuration + PLLFRQ= ((PLLFRQ & 0b11001111) | 0x20); // Use 96MHz / 1.5 = 64MHz + TCCR4B = ((TCCR4B & 0b11110000) | 0xB); // configure prescaler to get 64M/2/1024 = 31.25 kHz + TCCR4D = ((TCCR4D & 0b11111100) | 0x01); // configure the pwm phase-corrected mode + + // configure complementary pwm on low side + if(pinH == 13 ){ + TCCR4A = 0x82; // activate channel A - pin 13 + TCCR4C |= 0x0d; // Activate complementary channel D - pin 6 + }else { + TCCR4C |= 0x09; // Activate channel D - pin 6 + TCCR4A = 0xc2; // activate complementary channel A - pin 13 + } + }else{ + return -1; + } + return 0; +} + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // High PWM frequency + // - always max 32kHz + int ret_flag = 0; + ret_flag += _configureComplementaryPair(pinA_h, pinA_l); + ret_flag += _configureComplementaryPair(pinB_h, pinB_l); + ret_flag += _configureComplementaryPair(pinC_h, pinC_l); + if (ret_flag!=0) return SIMPLEFOC_DRIVER_INIT_FAILED; + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone + }; + return params; +} + +// function setting the +void _setPwmPair(int pinH, int pinL, float val, int dead_time) +{ + int pwm_h = _constrain(val-dead_time/2,0,255); + int pwm_l = _constrain(val+dead_time/2,0,255); + + analogWrite(pinH, pwm_h); + if(pwm_l == 255 || pwm_l == 0) + digitalWrite(pinL, pwm_l ? LOW : HIGH); + else + analogWrite(pinL, pwm_l); +} + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +// supports Arudino/ATmega328 +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _setPwmPair(((GenericDriverParams*)params)->pins[0], ((GenericDriverParams*)params)->pins[1], dc_a*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + _setPwmPair(((GenericDriverParams*)params)->pins[2], ((GenericDriverParams*)params)->pins[3], dc_b*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + _setPwmPair(((GenericDriverParams*)params)->pins[4], ((GenericDriverParams*)params)->pins[5], dc_c*255.0, ((GenericDriverParams*)params)->dead_zone*255.0); + + _UNUSED(phase_state); +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp new file mode 100644 index 0000000..4397114 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/due_mcu.cpp @@ -0,0 +1,449 @@ +#include "../hardware_api.h" + +#if defined(__arm__) && defined(__SAM3X8E__) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Due") +#pragma message("") + + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +#define _PWM_RES_MIN 255 // 50khz + +// pwm frequency and max duty cycle +static unsigned long _pwm_frequency; +static int _max_pwm_value = 1023; + +// array mapping the timer values to the interrupt handlers +static IRQn_Type irq_type[] = {TC0_IRQn, TC0_IRQn, TC1_IRQn, TC1_IRQn, TC2_IRQn, TC2_IRQn, TC3_IRQn, TC3_IRQn, TC4_IRQn, TC4_IRQn, TC5_IRQn, TC5_IRQn, TC6_IRQn, TC6_IRQn, TC7_IRQn, TC7_IRQn, TC8_IRQn, TC8_IRQn}; +// current counter values +static volatile uint32_t pwm_counter_vals[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + +// variables copied from wiring_analog.cpp for arduino due +static uint8_t PWMEnabled = 0; +static uint8_t TCChanEnabled[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; +static const uint32_t channelToChNo[] = { 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2 }; +static const uint32_t channelToAB[] = { 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 }; +static Tc *channelToTC[] = { + TC0, TC0, TC0, TC0, TC0, TC0, + TC1, TC1, TC1, TC1, TC1, TC1, + TC2, TC2, TC2, TC2, TC2, TC2 }; +static const uint32_t channelToId[] = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8 }; + + +// function setting the CMR register +static void TC_SetCMR_ChannelA(Tc *tc, uint32_t chan, uint32_t v){ tc->TC_CHANNEL[chan].TC_CMR = (tc->TC_CHANNEL[chan].TC_CMR & 0xFFF0FFFF) | v;} +static void TC_SetCMR_ChannelB(Tc *tc, uint32_t chan, uint32_t v){ tc->TC_CHANNEL[chan].TC_CMR = (tc->TC_CHANNEL[chan].TC_CMR & 0xF0FFFFFF) | v; } + + +// function which starts and syncs the timers +// if the pin is the true PWM pin this function does not do anything +void syncTimers(uint32_t ulPin1,uint32_t ulPin2, uint32_t ulPin3 = -1, uint32_t ulPin4 = -1){ + uint32_t chNo1,chNo2,chNo3,chNo4; + Tc *chTC1 = nullptr,*chTC2 = nullptr,*chTC3 = nullptr,*chTC4 = nullptr; + + // configure timer channel for the first pin if it is a timer pin + uint32_t attr = g_APinDescription[ulPin1].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel1 = g_APinDescription[ulPin1].ulTCChannel; + chNo1 = channelToChNo[channel1]; + chTC1 = channelToTC[channel1]; + TCChanEnabled[channelToId[channel1]] = 1; + } + + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin2].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel2 = g_APinDescription[ulPin2].ulTCChannel; + chNo2 = channelToChNo[channel2]; + chTC2 = channelToTC[channel2]; + TCChanEnabled[channelToId[channel2]] = 1; + } + if(ulPin3 > 0 ){ + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin3].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel3 = g_APinDescription[ulPin3].ulTCChannel; + chNo3 = channelToChNo[channel3]; + chTC3 = channelToTC[channel3]; + TCChanEnabled[channelToId[channel3]] = 1; + } + } + if(ulPin4 > 0 ){ + // configure timer channel for the first pin if it is a timer pin + attr = g_APinDescription[ulPin4].ulPinAttribute; + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { + ETCChannel channel4 = g_APinDescription[ulPin4].ulTCChannel; + chNo4 = channelToChNo[channel4]; + chTC4 = channelToTC[channel4]; + TCChanEnabled[channelToId[channel4]] = 1; + } + } + // start timers and make them synced + if(chTC1){ + TC_Start(chTC1, chNo1); + chTC1->TC_BCR = TC_BCR_SYNC; + } + if(chTC2){ + TC_Start(chTC2, chNo2); + chTC2->TC_BCR = TC_BCR_SYNC; + } + if(chTC3 && ulPin3){ + TC_Start(chTC3, chNo3); + chTC3->TC_BCR = TC_BCR_SYNC; + } + if(chTC4 && ulPin4){ + TC_Start(chTC4, chNo4); + chTC4->TC_BCR = TC_BCR_SYNC; + } +} + +// function configuring the pwm frequency for given pin +// possible to supply the pwm pin and the timer pin +void initPWM(uint32_t ulPin, uint32_t pwm_freq){ + // check which pin type + uint32_t attr = g_APinDescription[ulPin].ulPinAttribute; + if ((attr & PIN_ATTR_PWM) == PIN_ATTR_PWM) { // if pwm pin + + if (!PWMEnabled) { + // PWM Startup code + pmc_enable_periph_clk(PWM_INTERFACE_ID); + // this function does not work too well - I'll rewrite it + // PWMC_ConfigureClocks(PWM_FREQUENCY * _max_pwm_value, 0, VARIANT_MCK); + + // finding the divisors an prescalers form FindClockConfiguration function + uint32_t divisors[11] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; + uint8_t divisor = 0; + uint32_t prescaler; + + /* Find prescaler and divisor values */ + prescaler = (VARIANT_MCK / divisors[divisor]) / (pwm_freq*_max_pwm_value); + while ((prescaler > 255) && (divisor < 11)) { + divisor++; + prescaler = (VARIANT_MCK / divisors[divisor]) / (pwm_freq*_max_pwm_value); + } + // update the divisor*prescaler value + prescaler = prescaler | (divisor << 8); + + // now calculate the real resolution timer period necessary (pwm resolution) + // pwm_res = bus_freq / (pwm_freq * (prescaler)) + _max_pwm_value = (double)VARIANT_MCK / (double)pwm_freq / (double)(prescaler); + // set the prescaler value + PWM->PWM_CLK = prescaler; + + PWMEnabled = 1; + } + + uint32_t chan = g_APinDescription[ulPin].ulPWMChannel; + if ((g_pinStatus[ulPin] & 0xF) != PIN_STATUS_PWM) { + // Setup PWM for this pin + PIO_Configure(g_APinDescription[ulPin].pPort, + g_APinDescription[ulPin].ulPinType, + g_APinDescription[ulPin].ulPin, + g_APinDescription[ulPin].ulPinConfiguration); + // PWM_CMR_CALG - center align + // PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKA, PWM_CMR_CALG, 0); + PWMC_ConfigureChannel(PWM_INTERFACE, chan, PWM_CMR_CPRE_CLKA, 0, 0); + PWMC_SetPeriod(PWM_INTERFACE, chan, _max_pwm_value); + PWMC_SetDutyCycle(PWM_INTERFACE, chan, 0); + PWMC_EnableChannel(PWM_INTERFACE, chan); + g_pinStatus[ulPin] = (g_pinStatus[ulPin] & 0xF0) | PIN_STATUS_PWM; + } + return; + } + + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { // if timer pin + // We use MCLK/2 as clock. + const uint32_t TC = VARIANT_MCK / 2 / pwm_freq ; + // Setup Timer for this pin + ETCChannel channel = g_APinDescription[ulPin].ulTCChannel; + uint32_t chNo = channelToChNo[channel]; + uint32_t chA = channelToAB[channel]; + Tc *chTC = channelToTC[channel]; + uint32_t interfaceID = channelToId[channel]; + + if (!TCChanEnabled[interfaceID]) { + pmc_enable_periph_clk(TC_INTERFACE_ID + interfaceID); + TC_Configure(chTC, chNo, + TC_CMR_TCCLKS_TIMER_CLOCK1 | + TC_CMR_WAVE | // Waveform mode + TC_CMR_WAVSEL_UP_RC | // Counter running up and reset when equals to RC + TC_CMR_EEVT_XC0 | // Set external events from XC0 (this setup TIOB as output) + TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_CLEAR | + TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_CLEAR); + TC_SetRC(chTC, chNo, TC); + } + + // disable the counter on start + if (chA){ + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); + }else{ + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); + } + // configure input-ouput structure + if ((g_pinStatus[ulPin] & 0xF) != PIN_STATUS_PWM) { + PIO_Configure(g_APinDescription[ulPin].pPort, + g_APinDescription[ulPin].ulPinType, + g_APinDescription[ulPin].ulPin, + g_APinDescription[ulPin].ulPinConfiguration); + g_pinStatus[ulPin] = (g_pinStatus[ulPin] & 0xF0) | PIN_STATUS_PWM; + } + // enable interrupts + chTC->TC_CHANNEL[chNo].TC_IER = TC_IER_CPAS // interrupt on RA compare match + | TC_IER_CPBS // interrupt on RB compare match + | TC_IER_CPCS; // interrupt on RC compare match + chTC->TC_CHANNEL[chNo].TC_IDR = ~TC_IER_CPAS // interrupt on RA compare match + & ~TC_IER_CPBS // interrupt on RB compare match + & ~ TC_IER_CPCS; // interrupt on RC compare match + // enable interrupts for this timer + NVIC_EnableIRQ(irq_type[channel]); + return; + } +} + +// pwm setting function +// it sets the duty cycle for pwm pin or timer pin +void setPwm(uint32_t ulPin, uint32_t ulValue) { + // check pin type + uint32_t attr = g_APinDescription[ulPin].ulPinAttribute; + if ((attr & PIN_ATTR_PWM) == PIN_ATTR_PWM) { // if pwm + uint32_t chan = g_APinDescription[ulPin].ulPWMChannel; + PWMC_SetDutyCycle(PWM_INTERFACE, chan, ulValue); + return; + } + + if ((attr & PIN_ATTR_TIMER) == PIN_ATTR_TIMER) { // if timer pin + // get the timer variables + ETCChannel channel = g_APinDescription[ulPin].ulTCChannel; + Tc *chTC = channelToTC[channel]; + uint32_t chNo = channelToChNo[channel]; + if(!ulValue) { + // if the value 0 disable counter + if (channelToAB[channel]) + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_CLEAR); + else + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_CLEAR); + }else{ + // if the value not zero + // calculate clock + const uint32_t TC = VARIANT_MCK / 2 / _pwm_frequency; + // Map value to Timer ranges 0..max_duty_cycle => 0..TC + // Setup Timer for this pin + ulValue = ulValue * TC ; + pwm_counter_vals[channel] = ulValue / _max_pwm_value; + // enable counter + if (channelToAB[channel]) + TC_SetCMR_ChannelA(chTC, chNo, TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET); + else + TC_SetCMR_ChannelB(chTC, chNo, TC_CMR_BCPB_CLEAR | TC_CMR_BCPC_SET); + } + + return; + } +} + +// interrupt handlers for seamless pwm duty-cycle setting +void TC0_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 0); + // update the counters + if(pwm_counter_vals[0]) TC_SetRA(TC0, 0, pwm_counter_vals[0]); + if(pwm_counter_vals[1]) TC_SetRB(TC0, 0, pwm_counter_vals[1]); +} + +void TC1_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 1); + // update the counters + if(pwm_counter_vals[2]) TC_SetRA(TC0, 1, pwm_counter_vals[2]); + if(pwm_counter_vals[3]) TC_SetRB(TC0, 1, pwm_counter_vals[3]); +} + +void TC2_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC0, 2); + // update the counters + if(pwm_counter_vals[4]) TC_SetRA(TC0, 2, pwm_counter_vals[4]); + if(pwm_counter_vals[5]) TC_SetRB(TC0, 2, pwm_counter_vals[5]); +} +void TC3_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 0); + // update the counters + if(pwm_counter_vals[6]) TC_SetRA(TC1, 0, pwm_counter_vals[6]); + if(pwm_counter_vals[7]) TC_SetRB(TC1, 0, pwm_counter_vals[7]); +} + +void TC4_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 1); + // update the counters + if(pwm_counter_vals[8]) TC_SetRA(TC1, 1, pwm_counter_vals[8]); + if(pwm_counter_vals[9]) TC_SetRB(TC1, 1, pwm_counter_vals[9]); +} + +void TC5_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC1, 2); + // update the counters + if(pwm_counter_vals[10]) TC_SetRA(TC1, 2, pwm_counter_vals[10]); + if(pwm_counter_vals[11]) TC_SetRB(TC1, 2, pwm_counter_vals[11]); +} +void TC6_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 0); + // update the counters + if(pwm_counter_vals[12]) TC_SetRA(TC2, 0, pwm_counter_vals[12]); + if(pwm_counter_vals[13]) TC_SetRB(TC2, 0, pwm_counter_vals[13]); +} + +void TC7_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 1); + // update the counters + if(pwm_counter_vals[14]) TC_SetRA(TC2, 1, pwm_counter_vals[14]); + if(pwm_counter_vals[15]) TC_SetRB(TC2, 1, pwm_counter_vals[15]); +} + +void TC8_Handler() +{ + // read/clear interrupt status + TC_GetStatus(TC2, 2); + // update the counters + if(pwm_counter_vals[16]) TC_SetRA(TC2, 2, pwm_counter_vals[16]); + if(pwm_counter_vals[17]) TC_SetRB(TC2, 2, pwm_counter_vals[17]); +} + + + + + +// implementation of the hardware_api.cpp +// --------------------------------------------------------------------------------------------------------------------------------- + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware specific +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + initPWM(pinC, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB, pinC); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// Configuring PWM frequency, resolution and alignment +//- Stepper driver - 2PWM setting +// - hardware specific +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency)) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency)) pwm_frequency = _PWM_FREQUENCY; // default frequency 50khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // save the pwm frequency + _pwm_frequency = pwm_frequency; + // cinfigure pwm pins + initPWM(pinA, _pwm_frequency); + initPWM(pinB, _pwm_frequency); + initPWM(pinC, _pwm_frequency); + initPWM(pinD, _pwm_frequency); + // sync the timers if possible + syncTimers(pinA, pinB, pinC, pinD); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_a); + setPwm(p->pins[1], _max_pwm_value*dc_b); + setPwm(p->pins[2], _max_pwm_value*dc_c); +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_1a); + setPwm(p->pins[1], _max_pwm_value*dc_1b); + setPwm(p->pins[2], _max_pwm_value*dc_2a); + setPwm(p->pins[3], _max_pwm_value*dc_2b); +} + + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - Stepper driver - 2PWM setting +// - hardware specific +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* param){ + // transform duty cycle from [0,1] to [0,_max_pwm_value] + GenericDriverParams* p = (GenericDriverParams*)param; + setPwm(p->pins[0], _max_pwm_value*dc_a); + setPwm(p->pins[1], _max_pwm_value*dc_b); +} + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h new file mode 100644 index 0000000..1049787 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_driver_mcpwm.h @@ -0,0 +1,96 @@ +#ifndef ESP32_DRIVER_MCPWM_H +#define ESP32_DRIVER_MCPWM_H + +#include "../../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + + + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP32 MCPWM driver") +#pragma message("") + + +#include "driver/mcpwm.h" +#include "soc/mcpwm_reg.h" +#include "soc/mcpwm_struct.h" + +// empty motor slot +#define _EMPTY_SLOT -20 +#define _TAKEN_SLOT -21 + +// ABI bus frequency - would be better to take it from somewhere +// but I did nto find a good exposed variable +#define _MCPWM_FREQ 160e6f + +// preferred pwm resolution default +#define _PWM_RES_DEF 4096 +// min resolution +#define _PWM_RES_MIN 3000 +// max resolution +#define _PWM_RES_MAX 8000 +// pwm frequency +#define _PWM_FREQUENCY 25000 // default +#define _PWM_FREQUENCY_MAX 50000 // mqx + +// structure containing motor slot configuration +// this library supports up to 4 motors +typedef struct { + int pinA; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator; + mcpwm_io_signals_t mcpwm_a; + mcpwm_io_signals_t mcpwm_b; + mcpwm_io_signals_t mcpwm_c; +} bldc_3pwm_motor_slots_t; + +typedef struct { + int pin1A; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + mcpwm_io_signals_t mcpwm_1a; + mcpwm_io_signals_t mcpwm_1b; + mcpwm_io_signals_t mcpwm_2a; + mcpwm_io_signals_t mcpwm_2b; +} stepper_4pwm_motor_slots_t; + +typedef struct { + int pin1pwm; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator; + mcpwm_io_signals_t mcpwm_a; + mcpwm_io_signals_t mcpwm_b; +} stepper_2pwm_motor_slots_t; + +typedef struct { + int pinAH; + mcpwm_dev_t* mcpwm_num; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + mcpwm_io_signals_t mcpwm_ah; + mcpwm_io_signals_t mcpwm_bh; + mcpwm_io_signals_t mcpwm_ch; + mcpwm_io_signals_t mcpwm_al; + mcpwm_io_signals_t mcpwm_bl; + mcpwm_io_signals_t mcpwm_cl; +} bldc_6pwm_motor_slots_t; + + +typedef struct ESP32MCPWMDriverParams { + long pwm_frequency; + mcpwm_dev_t* mcpwm_dev; + mcpwm_unit_t mcpwm_unit; + mcpwm_operator_t mcpwm_operator1; + mcpwm_operator_t mcpwm_operator2; + float deadtime; +} ESP32MCPWMDriverParams; + + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp new file mode 100644 index 0000000..a454c05 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_ledc_mcu.cpp @@ -0,0 +1,185 @@ +#include "../../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && ( !defined(SOC_MCPWM_SUPPORTED) || defined(SIMPLEFOC_ESP32_USELEDC) ) + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP32 LEDC driver") +#pragma message("") + +#include "driver/ledc.h" + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 38000 // 38khz max to be able to have 10 bit pwm resolution +#define _PWM_RES_BIT 10 // 10 bir resolution +#define _PWM_RES 1023 // 2^10-1 = 1023-1 + + +// empty motor slot +#define _EMPTY_SLOT -20 +#define _TAKEN_SLOT -21 + +// figure out how many ledc channels are avaible +// esp32 - 2x8=16 +// esp32s2 - 8 +// esp32c3 - 6 +#include "soc/soc_caps.h" +#ifdef SOC_LEDC_SUPPORT_HS_MODE +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1) +#else +#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM) +#endif + + +// current channel stack index +// support for multiple motors +// esp32 has 16 channels +// esp32s2 has 8 channels +// esp32c3 has 6 channels +int channel_index = 0; + + + + +typedef struct ESP32LEDCDriverParams { + int channels[6]; + long pwm_frequency; +} ESP32LEDCDriverParams; + + + + + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin, const int channel){ + ledcSetup(channel, freq, _PWM_RES_BIT ); + ledcAttachPin(pin, channel); +} + + + + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 1 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + + + + + +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 2 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 3 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + int ch3 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + _setHighFrequency(pwm_frequency, pinC, ch3); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2, ch3 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + // check if enough channels available + if ( channel_index + 4 >= LEDC_CHANNELS ) return SIMPLEFOC_DRIVER_INIT_FAILED; + + int ch1 = channel_index++; + int ch2 = channel_index++; + int ch3 = channel_index++; + int ch4 = channel_index++; + _setHighFrequency(pwm_frequency, pinA, ch1); + _setHighFrequency(pwm_frequency, pinB, ch2); + _setHighFrequency(pwm_frequency, pinC, ch3); + _setHighFrequency(pwm_frequency, pinD, ch4); + + ESP32LEDCDriverParams* params = new ESP32LEDCDriverParams { + .channels = { ch1, ch2, ch3, ch4 }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); +} + + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_b, 0, _PWM_RES)); +} + + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_b, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[2], _constrain(_PWM_RES*dc_c, 0, _PWM_RES)); +} + + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[0], _constrain(_PWM_RES*dc_1a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[1], _constrain(_PWM_RES*dc_1b, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[2], _constrain(_PWM_RES*dc_2a, 0, _PWM_RES)); + ledcWrite(((ESP32LEDCDriverParams*)params)->channels[3], _constrain(_PWM_RES*dc_2b, 0, _PWM_RES)); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp new file mode 100644 index 0000000..0dc23c1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp32/esp32_mcu.cpp @@ -0,0 +1,431 @@ +#include "esp32_driver_mcpwm.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && defined(SOC_MCPWM_SUPPORTED) && !defined(SIMPLEFOC_ESP32_USELEDC) + +#ifndef SIMPLEFOC_ESP32_HW_DEADTIME + #define SIMPLEFOC_ESP32_HW_DEADTIME true // TODO: Change to false when sw-deadtime & phase_state is approved ready for general use. +#endif + +// define bldc motor slots array +bldc_3pwm_motor_slots_t esp32_bldc_3pwm_motor_slots[4] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM0A, MCPWM1A, MCPWM2A}, // 1st motor will be MCPWM0 channel A + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_B, MCPWM0B, MCPWM1B, MCPWM2B}, // 2nd motor will be MCPWM0 channel B + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM0A, MCPWM1A, MCPWM2A}, // 3rd motor will be MCPWM1 channel A + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_B, MCPWM0B, MCPWM1B, MCPWM2B} // 4th motor will be MCPWM1 channel B + }; + +// define stepper motor slots array +bldc_6pwm_motor_slots_t esp32_bldc_6pwm_motor_slots[2] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM2A, MCPWM0B, MCPWM1B, MCPWM2B}, // 1st motor will be on MCPWM0 + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM2A, MCPWM0B, MCPWM1B, MCPWM2B}, // 1st motor will be on MCPWM0 + }; + +// define 4pwm stepper motor slots array +stepper_4pwm_motor_slots_t esp32_stepper_4pwm_motor_slots[2] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM0B, MCPWM1B}, // 1st motor will be on MCPWM0 + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM_OPR_B, MCPWM0A, MCPWM1A, MCPWM0B, MCPWM1B}, // 1st motor will be on MCPWM1 + }; + +// define 2pwm stepper motor slots array +stepper_2pwm_motor_slots_t esp32_stepper_2pwm_motor_slots[4] = { + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_A, MCPWM0A, MCPWM1A}, // 1st motor will be MCPWM0 channel A + {_EMPTY_SLOT, &MCPWM0, MCPWM_UNIT_0, MCPWM_OPR_B, MCPWM0B, MCPWM1B}, // 2nd motor will be MCPWM0 channel B + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_A, MCPWM0A, MCPWM1A}, // 3rd motor will be MCPWM1 channel A + {_EMPTY_SLOT, &MCPWM1, MCPWM_UNIT_1, MCPWM_OPR_B, MCPWM0B, MCPWM1B} // 4th motor will be MCPWM1 channel B + }; + + +// configuring high frequency pwm timer +// a lot of help from this post from Paul Gauld +// https://hackaday.io/project/169905-esp-32-bldc-robot-actuator-controller +// a short tutorial for v2.0.1+ +// https://kzhead.info/sun/q6uFktWgkYeMeZ8/esp32-arduino.html +// +void _configureTimerFrequency(long pwm_frequency, mcpwm_dev_t* mcpwm_num, mcpwm_unit_t mcpwm_unit, float dead_zone = NOT_SET){ + + mcpwm_config_t pwm_config; + pwm_config.counter_mode = MCPWM_UP_DOWN_COUNTER; // Up-down counter (triangle wave) + pwm_config.duty_mode = (_isset(dead_zone) || SIMPLEFOC_PWM_ACTIVE_HIGH == true) ? MCPWM_DUTY_MODE_0 : MCPWM_DUTY_MODE_1; // Normally Active HIGH (MCPWM_DUTY_MODE_0) + pwm_config.frequency = 2*pwm_frequency; // set the desired freq - just a placeholder for now https://github.com/simplefoc/Arduino-FOC/issues/76 + mcpwm_init(mcpwm_unit, MCPWM_TIMER_0, &pwm_config); //Configure PWM0A & PWM0B with above settings + mcpwm_init(mcpwm_unit, MCPWM_TIMER_1, &pwm_config); //Configure PWM1A & PWM1B with above settings + mcpwm_init(mcpwm_unit, MCPWM_TIMER_2, &pwm_config); //Configure PWM2A & PWM2B with above settings + + if (_isset(dead_zone)){ + // dead zone is configured + + // When using hardware deadtime, setting the phase_state parameter is not supported. + #if SIMPLEFOC_ESP32_HW_DEADTIME == true + float dead_time = (float)(_MCPWM_FREQ / (pwm_frequency)) * dead_zone; + mcpwm_deadtime_type_t pwm_mode; + if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true)) {pwm_mode = MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE;} // Normal, noninverting driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false)){pwm_mode = MCPWM_ACTIVE_HIGH_MODE;} // Inverted lowside driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true)) {pwm_mode = MCPWM_ACTIVE_LOW_MODE;} // Inverted highside driver + else if ((SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) && (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false)){pwm_mode = MCPWM_ACTIVE_LOW_COMPLIMENT_MODE;} // Inverted low- & highside driver. Caution: This may short the FETs on reset of the ESP32, as both pins get pulled low! + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_0, pwm_mode, dead_time/2.0, dead_time/2.0); + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_1, pwm_mode, dead_time/2.0, dead_time/2.0); + mcpwm_deadtime_enable(mcpwm_unit, MCPWM_TIMER_2, pwm_mode, dead_time/2.0, dead_time/2.0); + #else // Software deadtime + for (int i = 0; i < 3; i++){ + if (SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == true) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_A, MCPWM_DUTY_MODE_0);} // Normal, noninverted highside + else if (SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH == false) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_A, MCPWM_DUTY_MODE_1);} // Inverted highside driver + + if (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == true) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_B, MCPWM_DUTY_MODE_1);} // Normal, complementary lowside + else if (SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH == false) {mcpwm_set_duty_type(mcpwm_unit, (mcpwm_timer_t) i, MCPWM_GEN_B, MCPWM_DUTY_MODE_0);} // Inverted lowside driver + } + #endif + + } + _delay(100); + + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_0); + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_1); + mcpwm_stop(mcpwm_unit, MCPWM_TIMER_2); + + // manual configuration due to the lack of config flexibility in mcpwm_init() + mcpwm_num->clk_cfg.clk_prescale = 0; + // calculate prescaler and period + // step 1: calculate the prescaler using the default pwm resolution + // prescaler = bus_freq / (pwm_freq * default_pwm_res) - 1 + int prescaler = ceil((double)_MCPWM_FREQ / (double)_PWM_RES_DEF / 2.0f / (double)pwm_frequency) - 1; + // constrain prescaler + prescaler = _constrain(prescaler, 0, 128); + // now calculate the real resolution timer period necessary (pwm resolution) + // pwm_res = bus_freq / (pwm_freq * (prescaler + 1)) + int resolution_corrected = (double)_MCPWM_FREQ / 2.0f / (double)pwm_frequency / (double)(prescaler + 1); + // if pwm resolution too low lower the prescaler + if(resolution_corrected < _PWM_RES_MIN && prescaler > 0 ) + resolution_corrected = (double)_MCPWM_FREQ / 2.0f / (double)pwm_frequency / (double)(--prescaler + 1); + resolution_corrected = _constrain(resolution_corrected, _PWM_RES_MIN, _PWM_RES_MAX); + + // set prescaler + mcpwm_num->timer[0].timer_cfg0.timer_prescale = prescaler; + mcpwm_num->timer[1].timer_cfg0.timer_prescale = prescaler; + mcpwm_num->timer[2].timer_cfg0.timer_prescale = prescaler; + _delay(1); + //set period + mcpwm_num->timer[0].timer_cfg0.timer_period = resolution_corrected; + mcpwm_num->timer[1].timer_cfg0.timer_period = resolution_corrected; + mcpwm_num->timer[2].timer_cfg0.timer_period = resolution_corrected; + _delay(1); + mcpwm_num->timer[0].timer_cfg0.timer_period_upmethod = 0; + mcpwm_num->timer[1].timer_cfg0.timer_period_upmethod = 0; + mcpwm_num->timer[2].timer_cfg0.timer_period_upmethod = 0; + _delay(1); + // _delay(1); + //restart the timers + mcpwm_start(mcpwm_unit, MCPWM_TIMER_0); + mcpwm_start(mcpwm_unit, MCPWM_TIMER_1); + mcpwm_start(mcpwm_unit, MCPWM_TIMER_2); + _delay(1); + + mcpwm_sync_config_t sync_conf = { + .sync_sig = MCPWM_SELECT_TIMER0_SYNC, + .timer_val = 0, + .count_direction = MCPWM_TIMER_DIRECTION_UP + }; + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_0, &sync_conf); + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_1, &sync_conf); + mcpwm_sync_configure(mcpwm_unit, MCPWM_TIMER_2, &sync_conf); + + // Enable sync event for all timers to be the TEZ of timer0 + mcpwm_set_timer_sync_output(mcpwm_unit, MCPWM_TIMER_0, MCPWM_SWSYNC_SOURCE_TEZ); +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +// supports Arudino/ATmega328, STM32 and ESP32 +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + + stepper_2pwm_motor_slots_t m_slot = {}; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm = pinA; + m_slot = esp32_stepper_2pwm_motor_slots[slot_num]; + break; + } + } + + // disable all the slots with the same MCPWM + // disable 3pwm bldc motor which would go in the same slot + esp32_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + if( slot_num < 2 ){ + // slot 0 of the 4pwm stepper + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slot 1 of the stepper + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_b, pinB); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +// supports Arudino/ATmega328, STM32 and ESP32 +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + + bldc_3pwm_motor_slots_t m_slot = {}; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(esp32_bldc_3pwm_motor_slots[slot_num].pinA == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_bldc_3pwm_motor_slots[slot_num].pinA = pinA; + m_slot = esp32_bldc_3pwm_motor_slots[slot_num]; + break; + } + } + // disable all the slots with the same MCPWM + // disable 2pwm steppr motor which would go in the same slot + esp32_stepper_2pwm_motor_slots[slot_num].pin1pwm = _TAKEN_SLOT; + if( slot_num < 2 ){ + // slot 0 of the 4pwm stepper + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slot 1 of the stepper + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_b, pinB); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_c, pinC); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25hz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max + stepper_4pwm_motor_slots_t m_slot = {}; + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(esp32_stepper_4pwm_motor_slots[slot_num].pin1A == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_stepper_4pwm_motor_slots[slot_num].pin1A = pinA; + m_slot = esp32_stepper_4pwm_motor_slots[slot_num]; + break; + } + } + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slots 0 and 1 of the 2pwm stepper taken + esp32_stepper_2pwm_motor_slots[0].pin1pwm = _TAKEN_SLOT; + esp32_stepper_2pwm_motor_slots[1].pin1pwm = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slots 2 and 3 of the 2pwm stepper taken + esp32_stepper_2pwm_motor_slots[2].pin1pwm = _TAKEN_SLOT; + esp32_stepper_2pwm_motor_slots[3].pin1pwm = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + } + + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_1a, pinA); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_1b, pinB); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_2a, pinC); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_2b, pinD); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit); + + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator1, + .mcpwm_operator2 = m_slot.mcpwm_operator2 + }; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_b*100.0); +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_b*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_c*100.0); +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +// ESP32 uses MCPWM +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // se the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100] + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_1a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator1, dc_1b*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator2, dc_2a*100.0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, ((ESP32MCPWMDriverParams*)params)->mcpwm_operator2, dc_2b*100.0); +} + + + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = 20000; // default frequency 20khz - centered pwm has twice lower frequency + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 40kHz max - centered pwm has twice lower frequency + bldc_6pwm_motor_slots_t m_slot = {}; + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(esp32_bldc_6pwm_motor_slots[slot_num].pinAH == _EMPTY_SLOT){ // put the new motor in the first empty slot + esp32_bldc_6pwm_motor_slots[slot_num].pinAH = pinA_h; + m_slot = esp32_bldc_6pwm_motor_slots[slot_num]; + break; + } + } + // if no slots available + if(slot_num >= 2) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + esp32_stepper_4pwm_motor_slots[0].pin1A = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + esp32_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + esp32_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + esp32_stepper_4pwm_motor_slots[1].pin1A = _TAKEN_SLOT; + } + + // configure pins + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_ah, pinA_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_al, pinA_l); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_bh, pinB_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_bl, pinB_l); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_ch, pinC_h); + mcpwm_gpio_init(m_slot.mcpwm_unit, m_slot.mcpwm_cl, pinC_l); + + // configure the timer + _configureTimerFrequency(pwm_frequency, m_slot.mcpwm_num, m_slot.mcpwm_unit, dead_zone); + // return + ESP32MCPWMDriverParams* params = new ESP32MCPWMDriverParams { + .pwm_frequency = pwm_frequency, + .mcpwm_dev = m_slot.mcpwm_num, + .mcpwm_unit = m_slot.mcpwm_unit, + .mcpwm_operator1 = m_slot.mcpwm_operator1, + .mcpwm_operator2 = m_slot.mcpwm_operator2, + .deadtime = _isset(dead_zone) ? dead_zone : 0 + }; + return params; +} + + + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + // set the PWM on the slot timers + // transform duty cycle from [0,1] to [0,100.0] + #if SIMPLEFOC_ESP32_HW_DEADTIME == true + // Hardware deadtime does deadtime insertion internally + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_A, dc_a*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_B, dc_a*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_A, dc_b*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_B, dc_b*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_A, dc_c*100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_B, dc_c*100.0f); + _UNUSED(phase_state); + #else + float deadtime = 0.5f*((ESP32MCPWMDriverParams*)params)->deadtime; + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_A, (phase_state[0] == PHASE_ON || phase_state[0] == PHASE_HI) ? _constrain(dc_a-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_0, MCPWM_OPR_B, (phase_state[0] == PHASE_ON || phase_state[0] == PHASE_LO) ? _constrain(dc_a+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_A, (phase_state[1] == PHASE_ON || phase_state[1] == PHASE_HI) ? _constrain(dc_b-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_1, MCPWM_OPR_B, (phase_state[1] == PHASE_ON || phase_state[1] == PHASE_LO) ? _constrain(dc_b+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_A, (phase_state[2] == PHASE_ON || phase_state[2] == PHASE_HI) ? _constrain(dc_c-deadtime, 0.0f, 1.0f) * 100.0f : 0); + mcpwm_set_duty(((ESP32MCPWMDriverParams*)params)->mcpwm_unit, MCPWM_TIMER_2, MCPWM_OPR_B, (phase_state[2] == PHASE_ON || phase_state[2] == PHASE_LO) ? _constrain(dc_c+deadtime, 0.0f, 1.0f) * 100.0f : 100.0f); + #endif +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp new file mode 100644 index 0000000..23d9411 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/esp8266_mcu.cpp @@ -0,0 +1,121 @@ +#include "../hardware_api.h" + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP8266) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for ESP8266") +#pragma message("") + + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin){ + analogWrite(pin, 0); + analogWriteFreq(freq); +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + _setHighFrequency(pwm_frequency, pinD); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp new file mode 100644 index 0000000..b6bc2f0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/generic_mcu.cpp @@ -0,0 +1,125 @@ + +#include "../hardware_api.h" + +// if the mcu doen't have defiend analogWrite +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) && !defined(analogWrite) + __attribute__((weak)) void analogWrite(uint8_t pin, int value){ }; +#endif + +// function setting the high pwm frequency to the supplied pin +// - Stepper motor - 1PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure1PWM(long pwm_frequency, const int pinA) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +// in generic case dont do anything +__attribute__((weak)) void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + GenericDriverParams* params = new GenericDriverParams { + .pins = { pin1A, pin1B, pin2A, pin2B }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +__attribute__((weak)) void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + _UNUSED(pwm_frequency); + _UNUSED(dead_zone); + _UNUSED(pinA_h); + _UNUSED(pinA_l); + _UNUSED(pinB_h); + _UNUSED(pinB_l); + _UNUSED(pinC_h); + _UNUSED(pinC_l); + + return SIMPLEFOC_DRIVER_INIT_FAILED; +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 1PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +__attribute__((weak)) void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +__attribute__((weak)) void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _UNUSED(dc_a); + _UNUSED(dc_b); + _UNUSED(dc_c); + _UNUSED(phase_state); + _UNUSED(params); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp new file mode 100644 index 0000000..a20b828 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/nrf52_mcu.cpp @@ -0,0 +1,397 @@ + +#include "../hardware_api.h" + +#if defined(NRF52_SERIES) + +#pragma message("") +#pragma message("SimpleFOC: compiling for NRF52") +#pragma message("") + + +#define PWM_CLK (16000000) +#define PWM_FREQ (40000) +#define PWM_RESOLUTION (PWM_CLK/PWM_FREQ) +#define PWM_MAX_FREQ (62500) +#define DEAD_ZONE (250) // in ns +#define DEAD_TIME (DEAD_ZONE / (PWM_RESOLUTION * 0.25 * 62.5)) // 62.5ns resolution of PWM + +#ifdef NRF_PWM3 +#define PWM_COUNT 4 +#else +#define PWM_COUNT 3 +#endif + +// empty motor slot +#define _EMPTY_SLOT (-0xAA) +#define _TAKEN_SLOT (-0x55) + +int pwm_range; + + +static NRF_PWM_Type* pwms[PWM_COUNT] = { + NRF_PWM0, + NRF_PWM1, + NRF_PWM2, + #ifdef NRF_PWM3 + NRF_PWM3 + #endif +}; + +typedef struct { + int pinA; + NRF_PWM_Type* mcpwm; + uint16_t mcpwm_channel_sequence[4]; +} bldc_3pwm_motor_slots_t; + +typedef struct { + int pin1A; + NRF_PWM_Type* mcpwm; + uint16_t mcpwm_channel_sequence[4]; +} stepper_motor_slots_t; + +typedef struct { + int pinAH; + NRF_PWM_Type* mcpwm1; + NRF_PWM_Type* mcpwm2; + uint16_t mcpwm_channel_sequence[8]; +} bldc_6pwm_motor_slots_t; + +// define bldc motor slots array +bldc_3pwm_motor_slots_t nrf52_bldc_3pwm_motor_slots[4] = { + {_EMPTY_SLOT, pwms[0], {0,0,0,0}},// 1st motor will be PWM0 + {_EMPTY_SLOT, pwms[1], {0,0,0,0}},// 2nd motor will be PWM1 + {_EMPTY_SLOT, pwms[2], {0,0,0,0}},// 3rd motor will be PWM2 + {_EMPTY_SLOT, pwms[3], {0,0,0,0}} // 4th motor will be PWM3 + }; + +// define stepper motor slots array +stepper_motor_slots_t nrf52_stepper_motor_slots[4] = { + {_EMPTY_SLOT, pwms[0], {0,0,0,0}},// 1st motor will be on PWM0 + {_EMPTY_SLOT, pwms[1], {0,0,0,0}},// 1st motor will be on PWM1 + {_EMPTY_SLOT, pwms[2], {0,0,0,0}},// 1st motor will be on PWM2 + {_EMPTY_SLOT, pwms[3], {0,0,0,0}} // 1st motor will be on PWM3 + }; + +// define BLDC motor slots array +bldc_6pwm_motor_slots_t nrf52_bldc_6pwm_motor_slots[2] = { + {_EMPTY_SLOT, pwms[0], pwms[1], {0,0,0,0,0,0,0,0}},// 1st motor will be on PWM0 & PWM1 + {_EMPTY_SLOT, pwms[2], pwms[3], {0,0,0,0,0,0,0,0}} // 2nd motor will be on PWM1 & PWM2 + }; + + + +typedef struct NRF52DriverParams { + union { + bldc_3pwm_motor_slots_t* slot3pwm; + bldc_6pwm_motor_slots_t* slot6pwm; + stepper_motor_slots_t* slotstep; + } slot; + long pwm_frequency; + float dead_time; +} NRF52DriverParams; + + + + +// configuring high frequency pwm timer +void _configureHwPwm(NRF_PWM_Type* mcpwm1, NRF_PWM_Type* mcpwm2){ + + mcpwm1->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos); + mcpwm1->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos); + mcpwm1->MODE = (PWM_MODE_UPDOWN_UpAndDown << PWM_MODE_UPDOWN_Pos); + mcpwm1->COUNTERTOP = pwm_range; //pwm freq. + mcpwm1->LOOP = (PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos); + mcpwm1->DECODER = ((uint32_t)PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos) | ((uint32_t)PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos); + mcpwm1->SEQ[0].REFRESH = 0; + mcpwm1->SEQ[0].ENDDELAY = 0; + + if(mcpwm1 != mcpwm2){ + mcpwm2->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos); + mcpwm2->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos); + mcpwm2->MODE = (PWM_MODE_UPDOWN_UpAndDown << PWM_MODE_UPDOWN_Pos); + mcpwm2->COUNTERTOP = pwm_range; //pwm freq. + mcpwm2->LOOP = (PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos); + mcpwm2->DECODER = ((uint32_t)PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos) | ((uint32_t)PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos); + mcpwm2->SEQ[0].REFRESH = 0; + mcpwm2->SEQ[0].ENDDELAY = 0; + }else{ + mcpwm1->MODE = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos); + } +} + + + +// can we support it using the generic driver on this MCU? Commented out to fall back to generic driver for 2-pwm +// void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { +// return SIMPLEFOC_DRIVER_INIT_FAILED; // not supported +// } + + + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz for a resolution of 800 + else pwm_frequency = _constrain(pwm_frequency, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + + int pA = digitalPinToPinName(pinA); //g_ADigitalPinMap[pinA]; + int pB = digitalPinToPinName(pinB); //g_ADigitalPinMap[pinB]; + int pC = digitalPinToPinName(pinC); //g_ADigitalPinMap[pinC]; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(nrf52_bldc_3pwm_motor_slots[slot_num].pinA == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = pinA; + break; + } + } + // if no slots available + if(slot_num >= 4) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if(slot_num < 2){ + // slot 0 of the stepper + nrf52_stepper_motor_slots[slot_num].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~1UL; + }else{ + // slot 1 of the stepper + nrf52_stepper_motor_slots[slot_num].pin1A = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~2UL; + } + + // configure pwm outputs + + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[0] = pA; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[1] = pB; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->PSEL.OUT[2] = pC; + + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->SEQ[0].PTR = (uint32_t)&nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm->SEQ[0].CNT = 4; + + // configure the pwm + _configureHwPwm(nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm, nrf52_bldc_3pwm_motor_slots[slot_num].mcpwm); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slot3pwm = &(nrf52_bldc_3pwm_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + return params; +} + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC, const int pinD) { + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz for a resolution of 800 + else pwm_frequency = _constrain(pwm_frequency, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + + int pA = digitalPinToPinName(pinA); //g_ADigitalPinMap[pinA]; + int pB = digitalPinToPinName(pinB); //g_ADigitalPinMap[pinB]; + int pC = digitalPinToPinName(pinC); //g_ADigitalPinMap[pinC]; + int pD = digitalPinToPinName(pinD); //g_ADigitalPinMap[pinD]; + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 4; slot_num++){ + if(nrf52_stepper_motor_slots[slot_num].pin1A == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_stepper_motor_slots[slot_num].pin1A = pinA; + break; + } + } + // if no slots available + if (slot_num >= 4) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if (slot_num < 2){ + // slots 0 and 1 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + // slot 0 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[0].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~1UL; + }else{ + // slots 2 and 3 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[slot_num].pinA = _TAKEN_SLOT; + // slot 1 of the 6pwm bldc + nrf52_bldc_6pwm_motor_slots[1].pinAH = _TAKEN_SLOT; + //NRF_PPI->CHEN &= ~2UL; + } + + // configure pwm outputs + + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[0] = pA; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[1] = pB; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[2] = pC; + nrf52_stepper_motor_slots[slot_num].mcpwm->PSEL.OUT[3] = pD; + + nrf52_stepper_motor_slots[slot_num].mcpwm->SEQ[0].PTR = (uint32_t)&nrf52_stepper_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_stepper_motor_slots[slot_num].mcpwm->SEQ[0].CNT = 4; + + // configure the pwm + _configureHwPwm(nrf52_stepper_motor_slots[slot_num].mcpwm, nrf52_stepper_motor_slots[slot_num].mcpwm); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slotstep = &(nrf52_stepper_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + return params; +} + + + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,range] + bldc_3pwm_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slot3pwm; + p->mcpwm_channel_sequence[0] = (int)(dc_a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(dc_b * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[2] = (int)(dc_c * pwm_range) | 0x8000; + + p->mcpwm->TASKS_SEQSTART[0] = 1; +} + + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + + stepper_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slotstep; + p->mcpwm_channel_sequence[0] = (int)(dc_1a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(dc_1b * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[2] = (int)(dc_2a * pwm_range) | 0x8000; + p->mcpwm_channel_sequence[3] = (int)(dc_2b * pwm_range) | 0x8000; + + p->mcpwm->TASKS_SEQSTART[0] = 1; +} + +/* Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +*/ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + + if( !pwm_frequency || pwm_frequency == NOT_SET) pwm_frequency = PWM_FREQ; // default frequency 20khz - centered pwm has twice lower frequency for a resolution of 400 + else pwm_frequency = _constrain(pwm_frequency*2, 0, PWM_MAX_FREQ); // constrain to 62.5kHz max => 31.25kHz for a resolution of 256 + + pwm_range = (PWM_CLK / pwm_frequency); + pwm_range /= 2; // scale the frequency (centered PWM) + + float dead_time; + if (dead_zone != NOT_SET){ + dead_time = dead_zone/2; + }else{ + dead_time = DEAD_TIME/2; + } + + int pA_l = digitalPinToPinName(pinA_l); //g_ADigitalPinMap[pinA_l]; + int pA_h = digitalPinToPinName(pinA_h); //g_ADigitalPinMap[pinA_h]; + int pB_l = digitalPinToPinName(pinB_l); //g_ADigitalPinMap[pinB_l]; + int pB_h = digitalPinToPinName(pinB_h); //g_ADigitalPinMap[pinB_h]; + int pC_l = digitalPinToPinName(pinC_l); //g_ADigitalPinMap[pinC_l]; + int pC_h = digitalPinToPinName(pinC_h); //g_ADigitalPinMap[pinC_h]; + + + // determine which motor are we connecting + // and set the appropriate configuration parameters + int slot_num; + for(slot_num = 0; slot_num < 2; slot_num++){ + if(nrf52_bldc_6pwm_motor_slots[slot_num].pinAH == _EMPTY_SLOT){ // put the new motor in the first empty slot + nrf52_bldc_6pwm_motor_slots[slot_num].pinAH = pinA_h; + break; + } + } + // if no slots available + if(slot_num >= 2) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // disable all the slots with the same MCPWM + if( slot_num == 0 ){ + // slots 0 and 1 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[0].pinA = _TAKEN_SLOT; + nrf52_bldc_3pwm_motor_slots[1].pinA = _TAKEN_SLOT; + // slot 0 and 1 of the stepper + nrf52_stepper_motor_slots[0].pin1A = _TAKEN_SLOT; + nrf52_stepper_motor_slots[1].pin1A = _TAKEN_SLOT; + }else{ + // slots 2 and 3 of the 3pwm bldc + nrf52_bldc_3pwm_motor_slots[2].pinA = _TAKEN_SLOT; + nrf52_bldc_3pwm_motor_slots[3].pinA = _TAKEN_SLOT; + // slot 1 of the stepper + nrf52_stepper_motor_slots[2].pin1A = _TAKEN_SLOT; + nrf52_stepper_motor_slots[3].pin1A = _TAKEN_SLOT; + } + + // Configure pwm outputs + + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[0] = pA_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[1] = pA_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[2] = pB_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->PSEL.OUT[3] = pB_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->SEQ[0].PTR = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm_channel_sequence[0]; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->SEQ[0].CNT = 4; + + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->PSEL.OUT[0] = pC_h; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->PSEL.OUT[1] = pC_l; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->SEQ[0].PTR = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm_channel_sequence[4]; + nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->SEQ[0].CNT = 4; + + // Initializing the PPI peripheral for sync the pwm slots + + NRF_PPI->CH[slot_num].EEP = (uint32_t)&NRF_EGU0->EVENTS_TRIGGERED[0]; + NRF_PPI->CH[slot_num].TEP = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1->TASKS_SEQSTART[0]; + NRF_PPI->FORK[slot_num].TEP = (uint32_t)&nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2->TASKS_SEQSTART[0]; + NRF_PPI->CHEN = 1UL << slot_num; + + // configure the pwm type + _configureHwPwm(nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm1, nrf52_bldc_6pwm_motor_slots[slot_num].mcpwm2); + + NRF52DriverParams* params = new NRF52DriverParams(); + params->slot.slot6pwm = &(nrf52_bldc_6pwm_motor_slots[slot_num]); + params->pwm_frequency = pwm_frequency; + params->dead_time = dead_time; + return params; +} + + + + +/* Function setting the duty cycle to the pwm pin +// - BLDC driver - 6PWM setting +// - hardware specific +*/ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + bldc_6pwm_motor_slots_t* p = ((NRF52DriverParams*)params)->slot.slot6pwm; + float dead_time = ((NRF52DriverParams*)params)->dead_time; + p->mcpwm_channel_sequence[0] = (int)(_constrain(dc_a-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[1] = (int)(_constrain(dc_a+dead_time,0,1)*pwm_range); + p->mcpwm_channel_sequence[2] = (int)(_constrain(dc_b-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[3] = (int)(_constrain(dc_b+dead_time,0,1)*pwm_range); + p->mcpwm_channel_sequence[4] = (int)(_constrain(dc_c-dead_time,0,1)*pwm_range) | 0x8000; + p->mcpwm_channel_sequence[5] = (int)(_constrain(dc_c+dead_time,0,1)*pwm_range); + NRF_EGU0->TASKS_TRIGGER[0] = 1; + + _UNUSED(phase_state); +} + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp new file mode 100644 index 0000000..ad16646 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/portenta_h7_mcu.cpp @@ -0,0 +1,545 @@ + +#include "../hardware_api.h" + +#if defined(TARGET_PORTENTA_H7) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Portenta_H7") +#pragma message("") + + +#include "pwmout_api.h" +#include "pinDefinitions.h" +#include "platform/mbed_critical.h" +#include "platform/mbed_power_mgmt.h" +#include "platform/mbed_assert.h" +#include "PeripheralPins.h" +#include "pwmout_device.h" + +// default pwm parameters +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + + +// // 6pwm parameters +// #define _HARDWARE_6PWM 1 +// #define _SOFTWARE_6PWM 0 +// #define _ERROR_6PWM -1 + + + +typedef struct PortentaDriverParams { + pwmout_t pins[4]; + long pwm_frequency; +// float dead_zone; +} PortentaDriverParams; + + + +/* Convert STM32 Cube HAL channel to LL channel */ +uint32_t _TIM_ChannelConvert_HAL2LL(uint32_t channel, pwmout_t *obj) +{ +#if !defined(PWMOUT_INVERTED_NOT_SUPPORTED) + if (obj->inverted) { + switch (channel) { + case TIM_CHANNEL_1 : + return LL_TIM_CHANNEL_CH1N; + case TIM_CHANNEL_2 : + return LL_TIM_CHANNEL_CH2N; + case TIM_CHANNEL_3 : + return LL_TIM_CHANNEL_CH3N; +#if defined(LL_TIM_CHANNEL_CH4N) + case TIM_CHANNEL_4 : + return LL_TIM_CHANNEL_CH4N; +#endif + default : /* Optional */ + return 0; + } + } else +#endif + { + switch (channel) { + case TIM_CHANNEL_1 : + return LL_TIM_CHANNEL_CH1; + case TIM_CHANNEL_2 : + return LL_TIM_CHANNEL_CH2; + case TIM_CHANNEL_3 : + return LL_TIM_CHANNEL_CH3; + case TIM_CHANNEL_4 : + return LL_TIM_CHANNEL_CH4; + default : /* Optional */ + return 0; + } + } +} + + + +// int _pwm_init(pwmout_t *obj, uint32_t pin, long frequency){ +// return _pwm_init(obj, digitalPinToPinName(pin), frequency); +// } + +int _pwm_init(pwmout_t *obj, uint32_t pin, long frequency){ + int peripheral = (int)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM); + int function = (int)pinmap_find_function(digitalPinToPinName(pin), PinMap_PWM); + + const PinMap static_pinmap = {digitalPinToPinName(pin), peripheral, function}; + + pwmout_init_direct(obj, &static_pinmap); + + TIM_HandleTypeDef TimHandle; + TimHandle.Instance = (TIM_TypeDef *)(obj->pwm); + RCC_ClkInitTypeDef RCC_ClkInitStruct; + uint32_t PclkFreq = 0; + uint32_t APBxCLKDivider = RCC_HCLK_DIV1; + uint8_t i = 0; + + + __HAL_TIM_DISABLE(&TimHandle); + + // Get clock configuration + // Note: PclkFreq contains here the Latency (not used after) + HAL_RCC_GetClockConfig(&RCC_ClkInitStruct, &PclkFreq); + + /* Parse the pwm / apb mapping table to find the right entry */ + while (pwm_apb_map_table[i].pwm != obj->pwm) i++; + // sanity check + if (pwm_apb_map_table[i].pwm == 0) return -1; + + + if (pwm_apb_map_table[i].pwmoutApb == PWMOUT_ON_APB1) { + PclkFreq = HAL_RCC_GetPCLK1Freq(); + APBxCLKDivider = RCC_ClkInitStruct.APB1CLKDivider; + } else { +#if !defined(PWMOUT_APB2_NOT_SUPPORTED) + PclkFreq = HAL_RCC_GetPCLK2Freq(); + APBxCLKDivider = RCC_ClkInitStruct.APB2CLKDivider; +#endif + } + + long period_us = 500000.0/((float)frequency); + /* By default use, 1us as SW pre-scaler */ + obj->prescaler = 1; + // TIMxCLK = PCLKx when the APB prescaler = 1 else TIMxCLK = 2 * PCLKx + if (APBxCLKDivider == RCC_HCLK_DIV1) { + TimHandle.Init.Prescaler = (((PclkFreq) / 1000000)) - 1; // 1 us tick + } else { + TimHandle.Init.Prescaler = (((PclkFreq * 2) / 1000000)) - 1; // 1 us tick + } + TimHandle.Init.Period = (period_us - 1); + + /* In case period or pre-scalers are out of range, loop-in to get valid values */ + while ((TimHandle.Init.Period > 0xFFFF) || (TimHandle.Init.Prescaler > 0xFFFF)) { + obj->prescaler = obj->prescaler * 2; + if (APBxCLKDivider == RCC_HCLK_DIV1) { + TimHandle.Init.Prescaler = (((PclkFreq) / 1000000) * obj->prescaler) - 1; + } else { + TimHandle.Init.Prescaler = (((PclkFreq * 2) / 1000000) * obj->prescaler) - 1; + } + TimHandle.Init.Period = (period_us - 1) / obj->prescaler; + /* Period decreases and prescaler increases over loops, so check for + * possible out of range cases */ + if ((TimHandle.Init.Period < 0xFFFF) && (TimHandle.Init.Prescaler > 0xFFFF)) { + break; + } + } + + TimHandle.Init.ClockDivision = 0; + TimHandle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; // center aligned + + if (HAL_TIM_PWM_Init(&TimHandle) != HAL_OK) { + return -1; + } + + TIM_OC_InitTypeDef sConfig; + // Configure channels + sConfig.OCMode = TIM_OCMODE_PWM1; + sConfig.Pulse = obj->pulse / obj->prescaler; + sConfig.OCPolarity = TIM_OCPOLARITY_HIGH; + sConfig.OCFastMode = TIM_OCFAST_DISABLE; +#if defined(TIM_OCIDLESTATE_RESET) + sConfig.OCIdleState = TIM_OCIDLESTATE_RESET; +#endif +#if defined(TIM_OCNIDLESTATE_RESET) + sConfig.OCNPolarity = TIM_OCNPOLARITY_HIGH; + sConfig.OCNIdleState = TIM_OCNIDLESTATE_RESET; +#endif + + int channel = 0; + switch (obj->channel) { + case 1: + channel = TIM_CHANNEL_1; + break; + case 2: + channel = TIM_CHANNEL_2; + break; + case 3: + channel = TIM_CHANNEL_3; + break; + case 4: + channel = TIM_CHANNEL_4; + break; + default: + return -1; + } + + if (LL_TIM_CC_IsEnabledChannel(TimHandle.Instance, _TIM_ChannelConvert_HAL2LL(channel, obj)) == 0) { + // If channel is not enabled, proceed to channel configuration + if (HAL_TIM_PWM_ConfigChannel(&TimHandle, &sConfig, channel) != HAL_OK) { + return -1; + } + } + + // Save for future use + obj->period = period_us; +#if !defined(PWMOUT_INVERTED_NOT_SUPPORTED) + if (obj->inverted) { + HAL_TIMEx_PWMN_Start(&TimHandle, channel); + } else +#endif + { + HAL_TIM_PWM_Start(&TimHandle, channel); + } + + return 0; +} + +// setting pwm to hardware pin - instead analogWrite() +void _pwm_write(pwmout_t *obj, float value){ + TIM_HandleTypeDef TimHandle; + int channel = 0; + + TimHandle.Instance = (TIM_TypeDef *)(obj->pwm); + + if (value < (float)0.0) { + value = 0.0; + } else if (value > (float)1.0) { + value = 1.0; + } + + obj->pulse = (uint32_t)((float)obj->period * value + 0.5); + + switch (obj->channel) { + case 1: + channel = TIM_CHANNEL_1; + break; + case 2: + channel = TIM_CHANNEL_2; + break; + case 3: + channel = TIM_CHANNEL_3; + break; + case 4: + channel = TIM_CHANNEL_4; + break; + default: + return; + } + + // If channel already enabled, only update compare value to avoid glitch + __HAL_TIM_SET_COMPARE(&TimHandle, channel, obj->pulse / obj->prescaler); +} + +// init low side pin +// HardwareTimer* _initPinPWMLow(uint32_t PWM_freq, int ulPin) +// { +// PinName pin = digitalPinToPinName(ulPin); +// TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM); +// uint32_t index = get_timer_index(Instance); + +// if (HardwareTimer_Handle[index] == NULL) { +// HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM)); +// HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; +// HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); +// TIM_OC_InitTypeDef sConfigOC = TIM_OC_InitTypeDef(); +// sConfigOC.OCMode = TIM_OCMODE_PWM2; +// sConfigOC.Pulse = 100; +// sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW; +// sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; +// sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; +// sConfigOC.OCIdleState = TIM_OCIDLESTATE_SET; +// sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; +// uint32_t channel = STM_PIN_CHANNEL(pinmap_function(pin, PinMap_PWM)); +// HAL_TIM_PWM_ConfigChannel(&(HardwareTimer_Handle[index]->handle), &sConfigOC, channel); +// } +// HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); +// uint32_t channel = STM_PIN_CHANNEL(pinmap_function(pin, PinMap_PWM)); +// HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM2, pin); +// HT->setOverflow(PWM_freq, HERTZ_FORMAT); +// HT->pause(); +// HT->refresh(); +// return HT; +// } + + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2){ + TIM_HandleTypeDef TimHandle1, TimHandle2; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); +} + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2, pwmout_t *t3){ + TIM_HandleTypeDef TimHandle1, TimHandle2, TimHandle3; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + TimHandle3.Instance = (TIM_TypeDef *)(t3->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_DISABLE(&TimHandle3); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle3); +} + +// align the timers to end the init +void _alignPWMTimers(pwmout_t *t1, pwmout_t *t2, pwmout_t *t3, pwmout_t *t4){ + TIM_HandleTypeDef TimHandle1, TimHandle2, TimHandle3, TimHandle4; + TimHandle1.Instance = (TIM_TypeDef *)(t1->pwm); + TimHandle2.Instance = (TIM_TypeDef *)(t2->pwm); + TimHandle3.Instance = (TIM_TypeDef *)(t3->pwm); + TimHandle4.Instance = (TIM_TypeDef *)(t4->pwm); + __HAL_TIM_DISABLE(&TimHandle1); + __HAL_TIM_DISABLE(&TimHandle2); + __HAL_TIM_DISABLE(&TimHandle3); + __HAL_TIM_DISABLE(&TimHandle4); + __HAL_TIM_ENABLE(&TimHandle1); + __HAL_TIM_ENABLE(&TimHandle2); + __HAL_TIM_ENABLE(&TimHandle3); + __HAL_TIM_ENABLE(&TimHandle4); +} + +// // configure hardware 6pwm interface only one timer with inverted channels +// HardwareTimer* _initHardware6PWMInterface(uint32_t PWM_freq, float dead_zone, int pinA_h, int pinA_l, int pinB_h, int pinB_l, int pinC_h, int pinC_l) +// { +// PinName uhPinName = digitalPinToPinName(pinA_h); +// PinName ulPinName = digitalPinToPinName(pinA_l); +// PinName vhPinName = digitalPinToPinName(pinB_h); +// PinName vlPinName = digitalPinToPinName(pinB_l); +// PinName whPinName = digitalPinToPinName(pinC_h); +// PinName wlPinName = digitalPinToPinName(pinC_l); + +// TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(uhPinName, PinMap_PWM); + +// uint32_t index = get_timer_index(Instance); + +// if (HardwareTimer_Handle[index] == NULL) { +// HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef *)pinmap_peripheral(uhPinName, PinMap_PWM)); +// HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; +// HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); +// ((HardwareTimer *)HardwareTimer_Handle[index]->__this)->setOverflow(PWM_freq, HERTZ_FORMAT); +// } +// HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(uhPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, uhPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(ulPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, ulPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(vhPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, vhPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(vlPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, vlPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(whPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, whPinName); +// HT->setMode(STM_PIN_CHANNEL(pinmap_function(wlPinName, PinMap_PWM)), TIMER_OUTPUT_COMPARE_PWM1, wlPinName); + +// // dead time is set in nanoseconds +// uint32_t dead_time_ns = (float)(1e9f/PWM_freq)*dead_zone; +// uint32_t dead_time = __LL_TIM_CALC_DEADTIME(SystemCoreClock, LL_TIM_GetClockDivision(HT->getHandle()->Instance), dead_time_ns); +// LL_TIM_OC_SetDeadTime(HT->getHandle()->Instance, dead_time); // deadtime is non linear! +// LL_TIM_CC_EnableChannel(HT->getHandle()->Instance, LL_TIM_CHANNEL_CH1 | LL_TIM_CHANNEL_CH1N | LL_TIM_CHANNEL_CH2 | LL_TIM_CHANNEL_CH2N | LL_TIM_CHANNEL_CH3 | LL_TIM_CHANNEL_CH3N); + +// HT->pause(); +// HT->refresh(); +// HT->resume(); +// return HT; +// } + + +// // returns 0 if each pair of pwm channels has same channel +// // returns 1 all the channels belong to the same timer - hardware inverted channels +// // returns -1 if neither - avoid configuring - error!!! +// int _interfaceType(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ +// PinName nameAH = digitalPinToPinName(pinA_h); +// PinName nameAL = digitalPinToPinName(pinA_l); +// PinName nameBH = digitalPinToPinName(pinB_h); +// PinName nameBL = digitalPinToPinName(pinB_l); +// PinName nameCH = digitalPinToPinName(pinC_h); +// PinName nameCL = digitalPinToPinName(pinC_l); +// int tim1 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameAH, PinMap_PWM)); +// int tim2 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameAL, PinMap_PWM)); +// int tim3 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameBH, PinMap_PWM)); +// int tim4 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameBL, PinMap_PWM)); +// int tim5 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameCH, PinMap_PWM)); +// int tim6 = get_timer_index((TIM_TypeDef *)pinmap_peripheral(nameCL, PinMap_PWM)); +// if(tim1 == tim2 && tim2==tim3 && tim3==tim4 && tim4==tim5 && tim5==tim6) +// return _HARDWARE_6PWM; // hardware 6pwm interface - only on timer +// else if(tim1 == tim2 && tim3==tim4 && tim5==tim6) +// return _SOFTWARE_6PWM; // software 6pwm interface - each pair of high-low side same timer +// else +// return _ERROR_6PWM; // might be error avoid configuration +// } + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1])); + core_util_critical_section_exit(); + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + _pwm_init(&(params->pins[2]), pinC, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1]), &(params->pins[2])); + core_util_critical_section_exit(); + + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + + PortentaDriverParams* params = new PortentaDriverParams(); + params->pwm_frequency = pwm_frequency; + + core_util_critical_section_enter(); + _pwm_init(&(params->pins[0]), pinA, (long)pwm_frequency); + _pwm_init(&(params->pins[1]), pinB, (long)pwm_frequency); + _pwm_init(&(params->pins[2]), pinC, (long)pwm_frequency); + _pwm_init(&(params->pins[3]), pinD, (long)pwm_frequency); + // allign the timers + _alignPWMTimers(&(params->pins[0]), &(params->pins[1]), &(params->pins[2]), &(params->pins[3])); + core_util_critical_section_exit(); + + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +//- hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_b); + core_util_critical_section_exit(); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +//- hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_b); + _pwm_write(&(((PortentaDriverParams*)params)->pins[2]), (float)dc_c); + core_util_critical_section_exit(); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +//- hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + core_util_critical_section_enter(); + _pwm_write(&(((PortentaDriverParams*)params)->pins[0]), (float)dc_1a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[1]), (float)dc_1b); + _pwm_write(&(((PortentaDriverParams*)params)->pins[2]), (float)dc_2a); + _pwm_write(&(((PortentaDriverParams*)params)->pins[3]), (float)dc_2b); + core_util_critical_section_exit(); +} + + +// 6-PWM currently not supported, defer to generic, which also doesn't support it ;-) + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +//void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + // if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + // else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to |%0kHz max + // // center-aligned frequency is uses two periods + // pwm_frequency *=2; + + // // find configuration + // int config = _interfaceType(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // // configure accordingly + // switch(config){ + // case _ERROR_6PWM: + // return -1; // fail + // break; + // case _HARDWARE_6PWM: + // _initHardware6PWMInterface(pwm_frequency, dead_zone, pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // break; + // case _SOFTWARE_6PWM: + // HardwareTimer* HT1 = _initPinPWMHigh(pwm_frequency, pinA_h); + // _initPinPWMLow(pwm_frequency, pinA_l); + // HardwareTimer* HT2 = _initPinPWMHigh(pwm_frequency,pinB_h); + // _initPinPWMLow(pwm_frequency, pinB_l); + // HardwareTimer* HT3 = _initPinPWMHigh(pwm_frequency,pinC_h); + // _initPinPWMLow(pwm_frequency, pinC_l); + // _alignPWMTimers(HT1, HT2, HT3); + // break; + // } +// return -1; // success +// } + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +//void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, void* params){ + // // find configuration + // int config = _interfaceType(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + // // set pwm accordingly + // switch(config){ + // case _HARDWARE_6PWM: + // _setPwm(pinA_h, _PWM_RANGE*dc_a, _PWM_RESOLUTION); + // _setPwm(pinB_h, _PWM_RANGE*dc_b, _PWM_RESOLUTION); + // _setPwm(pinC_h, _PWM_RANGE*dc_c, _PWM_RESOLUTION); + // break; + // case _SOFTWARE_6PWM: + // _setPwm(pinA_l, _constrain(dc_a + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinA_h, _constrain(dc_a - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinB_l, _constrain(dc_b + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinB_h, _constrain(dc_b - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinC_l, _constrain(dc_c + dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // _setPwm(pinC_h, _constrain(dc_c - dead_zone/2, 0, 1)*_PWM_RANGE, _PWM_RESOLUTION); + // break; + // } +//} +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp new file mode 100644 index 0000000..f90a4c5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.cpp @@ -0,0 +1,609 @@ + +#include "./renesas.h" + +#if defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_UNOR4_MINIMA) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Arduino/Renesas (UNO R4)") +#pragma message("") + + + +#include "communication/SimpleFOCDebug.h" +#include "FspTimer.h" + +#define GPT_OPEN (0x00475054ULL) + +/* + We use the GPT timers, there are 2 channels (32 bit) + 6 channels (16 bit) + Each channel has 2 outputs (GTIOCAx and GTIOCBx) which can be complimentary. + + So each timer channel can handle one half-bridge, using either a single (3-PWM) or + two complimentary PWM signals (6-PWM). + + For 1-PWM through 4-PWM, we need as many channels as PWM signals, and we can use + either output A or B of the timer (we can set the polarity) - but not both. + + For 6-PWM we need 3 channels, and use both outputs A and B of each channel, then + we can do hardware dead-time. + Or we can use seperate channels for high and low side, with software dead-time. + Each phase can be either hardware (1 channel) or software (2 channels) dead-time + individually, they don't all have to be one or the other. + + Selected channels can be started together, so we can keep the phases in sync for + low-side current sensing and software 6-PWM. + + The event system should permit low side current sensing without needing interrupts, + but this will be handled by the current sense driver. + + Supported: + - arbitrary PWM frequencies between 1Hz (minimum we can set with our integer based API) + and around 48kHz (more would be possible but the range will be low) + - PWM range at 24kHz (default) is 1000 + - PWM range at 48kHz is 500 + - polarity setting is supported, in all modes + - phase state setting is supported, in 3-PWM, 6-PWM hardware dead-time and 6-PWM software dead-time + + TODOs: + - change setDutyCycle to use register access for speed + - add event system support for low-side current sensing + - perhaps add support to reserve timers used also in + Arduino Pwm.h code, for compatibility with analogWrite() + - check if there is a better way for phase-state setting + */ + + +// track which channels are used +bool channel_used[GPT_HOWMANY] = { false }; + + +struct RenesasTimerConfig { + timer_cfg_t timer_cfg; + gpt_instance_ctrl_t ctrl; + gpt_extended_cfg_t ext_cfg; + gpt_extended_pwm_cfg_t pwm_cfg; + gpt_io_pin_t duty_pin; +}; + +struct ClockDivAndRange { + timer_source_div_t clk_div; + uint32_t range; +}; + +ClockDivAndRange getClockDivAndRange(uint32_t pwm_frequency, uint8_t timer_channel) { + ClockDivAndRange result; + uint32_t max_count = (timer_channel < GTP32_HOWMANY)? 4294967295 : 65535; + uint32_t freq_hz = R_FSP_SystemClockHzGet(FSP_PRIV_CLOCK_PCLKD); + float range = (float) freq_hz / ((float) pwm_frequency * 2.0f); + if(range / 1.0 < max_count) { + result.range = (uint32_t) (range / 1.0); + result.clk_div = TIMER_SOURCE_DIV_1; + } + else if (range / 2.0 < max_count) { + result.range = (uint32_t) (range / 2.0); + result.clk_div = TIMER_SOURCE_DIV_2; + } + else if(range / 4.0 < max_count) { + result.range = (uint32_t) (range / 4.0); + result.clk_div = TIMER_SOURCE_DIV_4; + } + else if(range / 8.0 < max_count) { + result.range = (uint32_t) (range / 8.0 ); + result.clk_div = TIMER_SOURCE_DIV_8; + } + else if(range / 16.0 < max_count) { + result.range = (uint32_t) (range / 16.0 ); + result.clk_div = TIMER_SOURCE_DIV_16; + } + else if (range / 32.0 < max_count) { + result.range = (uint32_t) (range / 32.0 ); + result.clk_div = TIMER_SOURCE_DIV_32; + } + else if(range / 64.0 < max_count) { + result.range = (uint32_t) (range / 64.0 ); + result.clk_div = TIMER_SOURCE_DIV_64; + } + else if(range / 128.0 < max_count) { + result.range = (uint32_t) (range / 128.0 ); + result.clk_div = TIMER_SOURCE_DIV_128; + } + else if(range / 256.0 < max_count) { + result.range = (uint32_t) (range / 256.0 ); + result.clk_div = TIMER_SOURCE_DIV_256; + } + else if(range / 512.0 < max_count) { + result.range = (uint32_t) (range / 512.0 ); + result.clk_div = TIMER_SOURCE_DIV_512; + } + else if(range / 1024.0 < max_count) { + result.range = (uint32_t) (range / 1024.0 ); + result.clk_div = TIMER_SOURCE_DIV_1024; + } + else { + SimpleFOCDebug::println("DRV: PWM frequency too low"); + } + return result; +}; + + +bool configureTimerPin(RenesasHardwareDriverParams* params, uint8_t index, bool active_high, bool complementary = false, bool complementary_active_high = true) { + uint8_t pin = params->pins[index]; + uint8_t pin_C; + std::array pinCfgs = getPinCfgs(pin, PIN_CFG_REQ_PWM); + std::array pinCfgs_C; + if(pinCfgs[0] == 0) { + SIMPLEFOC_DEBUG("DRV: no PWM on pin ", pin); + return false; + } + if (IS_PIN_AGT_PWM(pinCfgs[0])) { + SIMPLEFOC_DEBUG("DRV: AGT timer not supported"); + return false; + } + // get the timer channel + uint8_t timer_channel = GET_CHANNEL(pinCfgs[0]); + // check its not used + if (channel_used[timer_channel]) { + SIMPLEFOC_DEBUG("DRV: channel in use"); + return false; + } + + if (complementary) { + pin_C = params->pins[index+1]; + pinCfgs_C = getPinCfgs(pin_C, PIN_CFG_REQ_PWM); + if(pinCfgs_C[0] == 0) { + SIMPLEFOC_DEBUG("DRV: no PWM on pin ", pin_C); + return false; + } + if (IS_PIN_AGT_PWM(pinCfgs_C[0]) || GET_CHANNEL(pinCfgs_C[0])!=timer_channel) { + SIMPLEFOC_DEBUG("DRV: comp. channel different"); + return false; + } + } + TimerPWMChannel_t pwm_output = IS_PWM_ON_A(pinCfgs[0]) ? CHANNEL_A : CHANNEL_B; + if (complementary) { + TimerPWMChannel_t pwm_output_C = IS_PWM_ON_A(pinCfgs_C[0]) ? CHANNEL_A : CHANNEL_B; + if (pwm_output_C != CHANNEL_A || pwm_output != CHANNEL_B) { + SIMPLEFOC_DEBUG("DRV: output A/B mismatch"); + return false; + } + } + + // configure GPIO pin + fsp_err_t err = R_IOPORT_PinCfg(&g_ioport_ctrl, g_pin_cfg[pin].pin, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_GPT1)); + if ((err == FSP_SUCCESS) && complementary) + err = R_IOPORT_PinCfg(&g_ioport_ctrl, g_pin_cfg[pin_C].pin, (uint32_t) (IOPORT_CFG_PERIPHERAL_PIN | IOPORT_PERIPHERAL_GPT1)); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: pin config failed"); + return false; + } + + + // configure timer channel - frequency / top value + ClockDivAndRange timings = getClockDivAndRange(params->pwm_frequency, timer_channel); + #if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("---PWM Config---"); + SimpleFOCDebug::println("DRV: pwm pin: ", pin); + if (complementary) + SimpleFOCDebug::println("DRV: compl. pin: ", pin_C); + SimpleFOCDebug::println("DRV: pwm channel: ", timer_channel); + SimpleFOCDebug::print("DRV: pwm A/B: "); SimpleFOCDebug::println(complementary?"A+B":((pwm_output==CHANNEL_A)?"A":"B")); + SimpleFOCDebug::println("DRV: pwm freq: ", (int)params->pwm_frequency); + SimpleFOCDebug::println("DRV: pwm range: ", (int)timings.range); + SimpleFOCDebug::println("DRV: pwm clkdiv: ", timings.clk_div); + #endif + + RenesasTimerConfig* t = new RenesasTimerConfig(); + // configure timer channel - count mode + t->timer_cfg.channel = timer_channel; + t->timer_cfg.mode = TIMER_MODE_TRIANGLE_WAVE_SYMMETRIC_PWM; + t->timer_cfg.source_div = timings.clk_div; + t->timer_cfg.period_counts = timings.range; + t->timer_cfg.duty_cycle_counts = 0; + t->timer_cfg.p_callback = nullptr; + t->timer_cfg.p_context = nullptr; + t->timer_cfg.p_extend = &(t->ext_cfg); + t->timer_cfg.cycle_end_ipl = BSP_IRQ_DISABLED; + t->timer_cfg.cycle_end_irq = FSP_INVALID_VECTOR; + + t->ext_cfg.p_pwm_cfg = &(t->pwm_cfg); + t->pwm_cfg.trough_ipl = BSP_IRQ_DISABLED; + t->pwm_cfg.trough_irq = FSP_INVALID_VECTOR; + t->pwm_cfg.poeg_link = GPT_POEG_LINK_POEG0; + t->pwm_cfg.output_disable = GPT_OUTPUT_DISABLE_NONE; + t->pwm_cfg.adc_trigger = GPT_ADC_TRIGGER_NONE; + t->pwm_cfg.dead_time_count_up = 0; + t->pwm_cfg.dead_time_count_down = 0; + t->pwm_cfg.adc_a_compare_match = 0; + t->pwm_cfg.adc_b_compare_match = 0; + t->pwm_cfg.interrupt_skip_source = GPT_INTERRUPT_SKIP_SOURCE_NONE; + t->pwm_cfg.interrupt_skip_count = GPT_INTERRUPT_SKIP_COUNT_0; + t->pwm_cfg.interrupt_skip_adc = GPT_INTERRUPT_SKIP_ADC_NONE; + t->pwm_cfg.gtioca_disable_setting = GPT_GTIOC_DISABLE_PROHIBITED; + t->pwm_cfg.gtiocb_disable_setting = GPT_GTIOC_DISABLE_PROHIBITED; + // configure dead-time if both outputs are used + if (complementary) { + uint32_t dt = params->dead_zone * timings.range; + t->pwm_cfg.dead_time_count_up = dt; + t->pwm_cfg.dead_time_count_down = dt; + } + + // configure timer channel - outputs and polarity + t->ext_cfg.gtior_setting.gtior = 0L; + if (!complementary) { + if(pwm_output == CHANNEL_A) { + t->duty_pin = GPT_IO_PIN_GTIOCA; + t->ext_cfg.gtioca.output_enabled = true; + t->ext_cfg.gtiocb.output_enabled = false; + t->ext_cfg.gtior_setting.gtior_b.gtioa = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.oadflt = active_high ? 0x00 : 0x01; + // t->ext_cfg.gtior_setting.gtior_b.oahld = 0x0; + // t->ext_cfg.gtior_setting.gtior_b.oadf = 0x0; + // t->ext_cfg.gtior_setting.gtior_b.nfaen = 0x0; + } + else { + t->duty_pin = GPT_IO_PIN_GTIOCB; + t->ext_cfg.gtiocb.output_enabled = true; + t->ext_cfg.gtioca.output_enabled = false; + t->ext_cfg.gtior_setting.gtior_b.gtiob = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.obdflt = active_high ? 0x00 : 0x01; + } + } + else { + t->duty_pin = GPT_IO_PIN_GTIOCA_AND_GTIOCB; + t->ext_cfg.gtioca.output_enabled = true; + t->ext_cfg.gtiocb.output_enabled = true; + t->ext_cfg.gtior_setting.gtior_b.gtioa = 0x03 | (!complementary_active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.oadflt = !complementary_active_high ? 0x00 : 0x01; + t->ext_cfg.gtior_setting.gtior_b.gtiob = 0x03 | (active_high ? 0x00 : 0x10); + t->ext_cfg.gtior_setting.gtior_b.obdflt = active_high ? 0x00 : 0x01; + } + + // lets stop the timer in case its running + if (GPT_OPEN == t->ctrl.open) { + if (R_GPT_Stop(&(t->ctrl)) != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: timer stop failed"); + return false; + } + } + + memset(&(t->ctrl), 0, sizeof(gpt_instance_ctrl_t)); + err = R_GPT_Open(&(t->ctrl),&(t->timer_cfg)); + if ((err != FSP_ERR_ALREADY_OPEN) && (err != FSP_SUCCESS)) { + SIMPLEFOC_DEBUG("DRV: open failed"); + return false; + } + #if defined(SIMPLEFOC_RESENSAS_DEBUG) + if (err == FSP_ERR_ALREADY_OPEN) { + SimpleFOCDebug::println("DRV: timer already open"); + } + #endif + err = R_GPT_PeriodSet(&(t->ctrl), t->timer_cfg.period_counts); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: period set failed"); + return false; + } + err = R_GPT_OutputEnable(&(t->ctrl), t->duty_pin); + if (err != FSP_SUCCESS) { + SIMPLEFOC_DEBUG("DRV: pin enable failed"); + return false; + } + + channel_used[timer_channel] = true; + params->timer_config[index] = t; + params->channels[index] = timer_channel; + if (complementary) { + params->timer_config[index+1] = t; + params->channels[index+1] = timer_channel; + } + + return true; +} + + +// start the timer channels for the motor, synchronously +bool startTimerChannels(RenesasHardwareDriverParams* params, int num_channels) { + uint32_t mask = 0; + for (int i = 0; i < num_channels; i++) { + // RenesasTimerConfig* t = params->timer_config[i]; + // if (R_GPT_Start(&(t->ctrl)) != FSP_SUCCESS) { + // SIMPLEFOC_DEBUG("DRV: timer start failed"); + // return false; + // } + mask |= (1 << params->channels[i]); +#if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("DRV: starting timer: ", params->channels[i]); +#endif + } + params->timer_config[0]->ctrl.p_reg->GTSTR |= mask; + #if defined(SIMPLEFOC_RENESAS_DEBUG) + SimpleFOCDebug::println("DRV: timers started"); + #endif + return true; +} + + +// check if the given pins are on the same timer channel +bool isHardware6Pwm(const int pin1, const int pin2) { + std::array pinCfgs1 = getPinCfgs(pin1, PIN_CFG_REQ_PWM); + std::array pinCfgs2 = getPinCfgs(pin2, PIN_CFG_REQ_PWM); + if(pinCfgs1[0] == 0 || pinCfgs2[0] == 0) + return false; + if (IS_PIN_AGT_PWM(pinCfgs1[0]) || IS_PIN_AGT_PWM(pinCfgs2[0])) + return false; + uint8_t timer_channel1 = GET_CHANNEL(pinCfgs1[0]); + uint8_t timer_channel2 = GET_CHANNEL(pinCfgs2[0]); + return timer_channel1==timer_channel2; +} + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 1); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure2PWM(long pwm_frequency,const int pinA, const int pinB) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; params->pins[1] = pinB; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (!success) + success &= startTimerChannels(params, 2); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA; params->pins[1] = pinB; params->pins[2] = pinC; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 3); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pin1A; params->pins[1] = pin1B; params->pins[2] = pin2A; params->pins[3] = pin2B; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + bool success = true; + success = configureTimerPin(params, 0, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_ACTIVE_HIGH); + success &= configureTimerPin(params, 3, SIMPLEFOC_PWM_ACTIVE_HIGH); + if (success) + success = startTimerChannels(params, 4); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + RenesasHardwareDriverParams* params = new RenesasHardwareDriverParams; + params->pins[0] = pinA_h; params->pins[1] = pinA_l; params->pins[2] = pinB_h; params->pins[3] = pinB_l; params->pins[4] = pinC_h; params->pins[5] = pinC_l; + params->pwm_frequency = (pwm_frequency==NOT_SET)?RENESAS_DEFAULT_PWM_FREQUENCY:pwm_frequency; + params->dead_zone = (dead_zone==NOT_SET)?RENESAS_DEFAULT_DEAD_ZONE:dead_zone; + + bool success = true; + if (isHardware6Pwm(pinA_h, pinA_l)) + success &= configureTimerPin(params, 0, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 0, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 1, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); // reverse polarity on low side gives desired active high/low behaviour + } + + if (isHardware6Pwm(pinB_h, pinB_l)) + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 2, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 3, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + } + + if (isHardware6Pwm(pinC_h, pinC_l)) + success &= configureTimerPin(params, 4, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, true, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + else { + success &= configureTimerPin(params, 4, SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH); + success &= configureTimerPin(params, 5, !SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH); + } + + if (success) + success = startTimerChannels(params, 6); + if (!success) + return SIMPLEFOC_DRIVER_INIT_FAILED; + return params; +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + duty_cycle_counts = (uint32_t)(dc_c * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + uint32_t duty_cycle_counts = (uint32_t)(dc_1a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + duty_cycle_counts = (uint32_t)(dc_1b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + duty_cycle_counts = (uint32_t)(dc_2a * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + t = ((RenesasHardwareDriverParams*)params)->timer_config[3]; + duty_cycle_counts = (uint32_t)(dc_2b * (float)(t->timer_cfg.period_counts)); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } +} + + + +void _setSinglePhaseState(RenesasTimerConfig* hi, RenesasTimerConfig* lo, PhaseState state) { + gpt_gtior_setting_t gtior; + gtior.gtior = hi->ctrl.p_reg->GTIOR; + bool on = (state==PHASE_ON) || (state==PHASE_HI); + + if (hi->duty_pin == GPT_IO_PIN_GTIOCA_AND_GTIOCB) { + bool ch = false; + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + ch = true; + } // B is high side + on = (state==PHASE_ON) || (state==PHASE_LO); + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + ch = true; + } + if (ch) + hi->ctrl.p_reg->GTIOR = gtior.gtior; + return; + } + + if (hi->duty_pin == GPT_IO_PIN_GTIOCA) { + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + hi->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + else if (hi->duty_pin == GPT_IO_PIN_GTIOCB) { + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + hi->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + + gtior.gtior = lo->ctrl.p_reg->GTIOR; + on = (state==PHASE_ON) || (state==PHASE_LO); + if (lo->duty_pin == GPT_IO_PIN_GTIOCA) { + if (gtior.gtior_b.oae != on) { + gtior.gtior_b.oae = on; + lo->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + else if (lo->duty_pin == GPT_IO_PIN_GTIOCB) { + if (gtior.gtior_b.obe != on) { + gtior.gtior_b.obe = on; + lo->ctrl.p_reg->GTIOR = gtior.gtior; + } + } + +} + + +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + RenesasTimerConfig* t = ((RenesasHardwareDriverParams*)params)->timer_config[0]; + RenesasTimerConfig* t1 = ((RenesasHardwareDriverParams*)params)->timer_config[1]; + uint32_t dt = (uint32_t)(((RenesasHardwareDriverParams*)params)->dead_zone * (float)(t->timer_cfg.period_counts)); + uint32_t duty_cycle_counts = (uint32_t)(dc_a * (float)(t->timer_cfg.period_counts)); + bool hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[0] == ((RenesasHardwareDriverParams*)params)->channels[1]; + uint32_t dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[0]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts - dt_act, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + + t = ((RenesasHardwareDriverParams*)params)->timer_config[2]; + t1 = ((RenesasHardwareDriverParams*)params)->timer_config[3]; + duty_cycle_counts = (uint32_t)(dc_b * (float)(t->timer_cfg.period_counts)); + hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[2] == ((RenesasHardwareDriverParams*)params)->channels[3]; + dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[1]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts - dt_act, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + + t = ((RenesasHardwareDriverParams*)params)->timer_config[4]; + t1 = ((RenesasHardwareDriverParams*)params)->timer_config[5]; + duty_cycle_counts = (uint32_t)(dc_c * (float)(t->timer_cfg.period_counts)); + hw_deadtime = ((RenesasHardwareDriverParams*)params)->channels[4] == ((RenesasHardwareDriverParams*)params)->channels[5]; + dt_act = (duty_cycle_counts>0 && !hw_deadtime)?dt:0; + _setSinglePhaseState(t, t1, phase_state[2]); + if (R_GPT_DutyCycleSet(&(t->ctrl), duty_cycle_counts, t->duty_pin) != FSP_SUCCESS) { + // error + } + if (!hw_deadtime) { + if (R_GPT_DutyCycleSet(&(t1->ctrl), duty_cycle_counts + dt_act, t1->duty_pin) != FSP_SUCCESS) { + // error + } + } + +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h new file mode 100644 index 0000000..91bacdc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/renesas/renesas.h @@ -0,0 +1,28 @@ +#pragma once + + +#include "../../hardware_api.h" + + +#if defined(ARDUINO_UNOR4_WIFI) || defined(ARDUINO_UNOR4_MINIMA) + +// uncomment to enable debug output from Renesas driver +// can set this as build-flag in Arduino IDE or PlatformIO +#define SIMPLEFOC_RENESAS_DEBUG + +#define RENESAS_DEFAULT_PWM_FREQUENCY 24000 +#define RENESAS_DEFAULT_DEAD_ZONE 0.05f + +struct RenesasTimerConfig; + +typedef struct RenesasHardwareDriverParams { + uint8_t pins[6]; + uint8_t channels[6]; + RenesasTimerConfig* timer_config[6]; + long pwm_frequency; + float dead_zone; +} RenesasHardwareDriverParams; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp new file mode 100644 index 0000000..eee5797 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.cpp @@ -0,0 +1,237 @@ + +/** + * Support for the RP2040 MCU, as found on the Raspberry Pi Pico. + */ +#if defined(TARGET_RP2040) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for RP2040") +#pragma message("") + + +#define SIMPLEFOC_DEBUG_RP2040 + +#include "../../hardware_api.h" +#include "./rp2040_mcu.h" +#include "hardware/pwm.h" +#include "hardware/clocks.h" + +#define _PWM_FREQUENCY 24000 +#define _PWM_FREQUENCY_MAX 66000 +#define _PWM_FREQUENCY_MIN 1 + + + +// until I can figure out if this can be quickly read from some register, keep it here. +// it also serves as a marker for what slices are already used. +uint16_t wrapvalues[NUM_PWM_SLICES]; + + +// TODO add checks which channels are already used... + +void setupPWM(int pin, long pwm_frequency, bool invert, RP2040DriverParams* params, uint8_t index) { + gpio_set_function(pin, GPIO_FUNC_PWM); + uint slice = pwm_gpio_to_slice_num(pin); + uint chan = pwm_gpio_to_channel(pin); + params->pins[index] = pin; + params->slice[index] = slice; + params->chan[index] = chan; + uint32_t sysclock_hz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS) * 1000; + uint32_t factor = 4096 * 2 * pwm_frequency; + uint32_t div = sysclock_hz / factor; + if (sysclock_hz % factor !=0) div+=1; + if (div < 16) div = 16; + uint32_t wrapvalue = (sysclock_hz * 8) / div / pwm_frequency - 1; +#ifdef SIMPLEFOC_DEBUG_RP2040 + SimpleFOCDebug::print("Configuring pin "); + SimpleFOCDebug::print(pin); + SimpleFOCDebug::print(" slice "); + SimpleFOCDebug::print((int)slice); + SimpleFOCDebug::print(" channel "); + SimpleFOCDebug::print((int)chan); + SimpleFOCDebug::print(" frequency "); + SimpleFOCDebug::print((int)pwm_frequency); + SimpleFOCDebug::print(" divisor "); + SimpleFOCDebug::print((int)(div>>4)); + SimpleFOCDebug::print("."); + SimpleFOCDebug::print((int)(div&0xF)); + SimpleFOCDebug::print(" top value "); + SimpleFOCDebug::println((int)wrapvalue); +#endif + if (wrapvalue < 999) + SimpleFOCDebug::println("Warning: PWM resolution is low."); + pwm_set_clkdiv_int_frac(slice, div>>4, div&0xF); + pwm_set_phase_correct(slice, true); + pwm_set_wrap(slice, wrapvalue); + wrapvalues[slice] = wrapvalue; + if (invert) { + if (chan==0) + hw_write_masked(&pwm_hw->slice[slice].csr, 0x1 << PWM_CH0_CSR_A_INV_LSB, PWM_CH0_CSR_A_INV_BITS); + else + hw_write_masked(&pwm_hw->slice[slice].csr, 0x1 << PWM_CH0_CSR_B_INV_LSB, PWM_CH0_CSR_B_INV_BITS); + } + pwm_set_chan_level(slice, chan, 0); // switch off initially +} + + +void syncSlices() { + for (uint i=0;ipwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + syncSlices(); + return params; +} + + + +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pinB, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + syncSlices(); + return params; +} + + + +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pinA, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pinB, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + setupPWM(pinC, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 2); + syncSlices(); + return params; +} + + + + +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + setupPWM(pin1A, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 0); + setupPWM(pin1B, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 1); + setupPWM(pin2A, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 2); + setupPWM(pin2B, pwm_frequency, !SIMPLEFOC_PWM_ACTIVE_HIGH, params, 3); + syncSlices(); + return params; +} + + +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // non-PIO solution... + RP2040DriverParams* params = new RP2040DriverParams(); + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; + else pwm_frequency = _constrain(pwm_frequency, _PWM_FREQUENCY_MIN, _PWM_FREQUENCY_MAX); + params->pwm_frequency = pwm_frequency; + params->dead_zone = dead_zone; + setupPWM(pinA_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 0); + setupPWM(pinB_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 2); + setupPWM(pinC_h, pwm_frequency, !SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH, params, 4); + setupPWM(pinA_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 1); + setupPWM(pinB_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 3); + setupPWM(pinC_l, pwm_frequency, SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH, params, 5); + syncSlices(); + return params; +} + + + + + +void writeDutyCycle(float val, uint slice, uint chan) { + pwm_set_chan_level(slice, chan, (wrapvalues[slice]+1) * val); +} + + + + +void _writeDutyCycle1PWM(float dc_a, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); +} + + + + +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); +} + + + +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params) { + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + writeDutyCycle(dc_c, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); +} + + + +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params) { + writeDutyCycle(dc_1a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + writeDutyCycle(dc_1b, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + writeDutyCycle(dc_2a, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + writeDutyCycle(dc_2b, ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); +} + +inline float swDti(float val, float dt) { + float ret = dt+val; + if (ret>1.0) ret = 1.0f; + return ret; +} + +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params) { + if (phase_state[0]==PhaseState::PHASE_ON || phase_state[0]==PhaseState::PHASE_HI) + writeDutyCycle(dc_a, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[0], ((RP2040DriverParams*)params)->chan[0]); + if (phase_state[0]==PhaseState::PHASE_ON || phase_state[0]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_a, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[1], ((RP2040DriverParams*)params)->chan[1]); + + if (phase_state[1]==PhaseState::PHASE_ON || phase_state[1]==PhaseState::PHASE_HI) + writeDutyCycle(dc_b, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[2], ((RP2040DriverParams*)params)->chan[2]); + if (phase_state[1]==PhaseState::PHASE_ON || phase_state[1]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_b, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[3], ((RP2040DriverParams*)params)->chan[3]); + + if (phase_state[2]==PhaseState::PHASE_ON || phase_state[2]==PhaseState::PHASE_HI) + writeDutyCycle(dc_c, ((RP2040DriverParams*)params)->slice[4], ((RP2040DriverParams*)params)->chan[4]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[4], ((RP2040DriverParams*)params)->chan[4]); + if (phase_state[2]==PhaseState::PHASE_ON || phase_state[2]==PhaseState::PHASE_LO) + writeDutyCycle(swDti(dc_c, ((RP2040DriverParams*)params)->dead_zone), ((RP2040DriverParams*)params)->slice[5], ((RP2040DriverParams*)params)->chan[5]); + else + writeDutyCycle(0.0f, ((RP2040DriverParams*)params)->slice[5], ((RP2040DriverParams*)params)->chan[5]); + + _UNUSED(phase_state); +} + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h new file mode 100644 index 0000000..bbfb387 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/rp2040/rp2040_mcu.h @@ -0,0 +1,22 @@ + + +#pragma once + +#include "Arduino.h" + +#if defined(TARGET_RP2040) + + + +typedef struct RP2040DriverParams { + int pins[6]; + uint slice[6]; + uint chan[6]; + long pwm_frequency; + float dead_zone; +} RP2040DriverParams; + + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp new file mode 100644 index 0000000..d59a309 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd21_mcu.cpp @@ -0,0 +1,353 @@ + + + +#include "./samd_mcu.h" + + +#ifdef _SAMD21_ + + +#pragma message("") +#pragma message("SimpleFOC: compiling for SAMD21") +#pragma message("") + + +#ifndef TCC3_CH0 +#define TCC3_CH0 NOT_ON_TIMER +#endif +#ifndef TCC3_CH1 +#define TCC3_CH1 NOT_ON_TIMER +#endif +#ifndef TCC3_CH2 +#define TCC3_CH2 NOT_ON_TIMER +#endif +#ifndef TCC3_CH3 +#define TCC3_CH3 NOT_ON_TIMER +#endif +#ifndef TCC3_CH4 +#define TCC3_CH4 NOT_ON_TIMER +#endif +#ifndef TCC3_CH5 +#define TCC3_CH5 NOT_ON_TIMER +#endif +#ifndef TCC3_CH6 +#define TCC3_CH6 NOT_ON_TIMER +#endif +#ifndef TCC3_CH7 +#define TCC3_CH7 NOT_ON_TIMER +#endif +#ifndef TC6_CH0 +#define TC6_CH0 NOT_ON_TIMER +#endif +#ifndef TC6_CH1 +#define TC6_CH1 NOT_ON_TIMER +#endif +#ifndef TC7_CH0 +#define TC7_CH0 NOT_ON_TIMER +#endif +#ifndef TC7_CH1 +#define TC7_CH1 NOT_ON_TIMER +#endif + + + +#define NUM_WO_ASSOCIATIONS 48 + +/* + * For SAM D21 A/B/C/D Variant Devices and SAM DA1 A/B Variant Devices + * Good for SAMD2xE, SAMD2xG and SAMD2xJ devices. Other SAMD21s currently not supported in arduino anyway? + * + * Note: only the pins which have timers associated are listed in this table. + * You can use the values from g_APinDescription.ulPort and g_APinDescription.ulPin to find the correct row in the table. + * + * See Microchip Technology datasheet DS40001882F-page 30 + */ +struct wo_association WO_associations[] = { + + { PORTA, 0, TCC2_CH0, 0, NOT_ON_TIMER, 0}, + { PORTA, 1, TCC2_CH1, 1, NOT_ON_TIMER, 0}, + { PORTA, 2, NOT_ON_TIMER, 0, TCC3_CH0, 0}, + { PORTA, 3, NOT_ON_TIMER, 0, TCC3_CH1, 1}, + // PB04, PB05, PB06, PB07 - no timers + { PORTB, 8, TC4_CH0, 0, TCC3_CH6, 6}, + { PORTB, 9, TC4_CH1, 1, TCC3_CH7, 7}, + { PORTA, 4, TCC0_CH0, 0, TCC3_CH2, 2}, + { PORTA, 5, TCC0_CH1, 1, TCC3_CH3, 3}, + { PORTA, 6, TCC1_CH0, 0, TCC3_CH4, 4}, + { PORTA, 7, TCC1_CH1, 1, TCC3_CH5, 5}, + { PORTA, 8, TCC0_CH0, 0, TCC1_CH2, 2}, + { PORTA, 9, TCC0_CH1, 1, TCC1_CH3, 3}, + { PORTA, 10, TCC1_CH0, 0, TCC0_CH2, 2}, + { PORTA, 11, TCC1_CH1, 1, TCC0_CH3, 3}, + { PORTB, 10, TC5_CH0, 0, TCC0_CH4, 4}, + { PORTB, 11, TC5_CH1, 1, TCC0_CH5, 5}, + { PORTB, 12, TC4_CH0, 0, TCC0_CH6, 6}, + { PORTB, 13, TC4_CH1, 1, TCC0_CH7, 7}, + { PORTB, 14, TC5_CH0, 0, NOT_ON_TIMER, 0}, + { PORTB, 15, TC5_CH1, 1, NOT_ON_TIMER, 0}, + { PORTA, 12, TCC2_CH0, 0, TCC0_CH6, 6}, + { PORTA, 13, TCC2_CH1, 1, TCC0_CH7, 7}, + { PORTA, 14, TC3_CH0, 0, TCC0_CH4, 4}, + { PORTA, 15, TC3_CH1, 1, TCC0_CH5, 5}, + { PORTA, 16, TCC2_CH0, 0, TCC0_CH6, 6}, + { PORTA, 17, TCC2_CH1, 1, TCC0_CH7, 7}, + { PORTA, 18, TC3_CH0, 0, TCC0_CH2, 2}, + { PORTA, 19, TC3_CH1, 1, TCC0_CH3, 3}, + { PORTB, 16, TC6_CH0, 0, TCC0_CH4, 4}, + { PORTB, 17, TC6_CH1, 1, TCC0_CH5, 5}, + { PORTA, 20, TC7_CH0, 0, TCC0_CH6, 6}, + { PORTA, 21, TC7_CH1, 1, TCC0_CH7, 7}, + { PORTA, 22, TC4_CH0, 0, TCC0_CH4, 4}, + { PORTA, 23, TC4_CH1, 1, TCC0_CH5, 5}, + { PORTA, 24, TC5_CH0, 0, TCC1_CH2, 2}, + { PORTA, 25, TC5_CH1, 1, TCC1_CH3, 3}, + { PORTB, 22, TC7_CH0, 0, TCC3_CH0, 0}, + { PORTB, 23, TC7_CH1, 1, TCC3_CH1, 1}, + { PORTA, 27, NOT_ON_TIMER, 0, TCC3_CH6, 6}, + { PORTA, 28, NOT_ON_TIMER, 0, TCC3_CH7, 7}, + { PORTA, 30, TCC1_CH0, 0, TCC3_CH4, 4}, + { PORTA, 31, TCC1_CH1, 1, TCC3_CH5, 5}, + { PORTB, 30, TCC0_CH0, 0, TCC1_CH2, 2}, + { PORTB, 31, TCC0_CH1, 1, TCC1_CH3, 3}, + { PORTB, 0, TC7_CH0, 0, NOT_ON_TIMER, 0}, + { PORTB, 1, TC7_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 2, TC6_CH0, 0, TCC3_CH2, 2}, + { PORTB, 3, TC6_CH1, 1, TCC3_CH3, 3} +}; +wo_association ASSOCIATION_NOT_FOUND = { NOT_A_PORT, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}; + + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin) { + for (int i=0;i>pin_position)&0x01)==0x1?PIO_TIMER_ALT:PIO_TIMER; +} + + + + + +void syncTCC(Tcc* TCCx) { + while (TCCx->SYNCBUSY.reg & TCC_SYNCBUSY_MASK); // Wait for synchronization of registers between the clock domains +} + + + +/** + * Configure Clock 4 - we want all simplefoc PWMs to use the same clock. This ensures that + * any compatible pin combination can be used without having to worry about configuring different + * clocks. + */ +void configureSAMDClock() { + + // TODO investigate using the FDPLL96M clock to get 96MHz timer clocks... this + // would enable 48KHz PWM clocks, and setting the frequency between 24Khz with resolution 2000, to 48KHz with resolution 1000 + + if (!SAMDClockConfigured) { + SAMDClockConfigured = true; // mark clock as configured + for (int i=0;iSTATUS.bit.SYNCBUSY); // Wait for synchronization + + REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW + GCLK_GENCTRL_GENEN | // Enable GCLK4 + GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source + // GCLK_GENCTRL_SRC_FDPLL | // Set the 96MHz clock source + GCLK_GENCTRL_ID(4); // Select GCLK4 + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured clock..."); +#endif + } +} + + + + +/** + * Configure a TCC unit + * pwm_frequency is fixed at 24kHz for now. We could go slower, but going + * faster won't be possible without sacrificing resolution. + */ +void configureTCC(tccConfiguration& tccConfig, long pwm_frequency, bool negate, float hw6pwm) { + + long pwm_resolution = (24000000) / pwm_frequency; + if (pwm_resolution>SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION) + pwm_resolution = SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION; + if (pwm_resolution>1) { + case 0: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TCC0_TCC1); break; //GCLK_CLKCTRL_ID_TCC0_TCC1; + case 1: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TCC2_TC3); break; //GCLK_CLKCTRL_ID_TCC2_TC3; + case 2: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TC4_TC5); break; //GCLK_CLKCTRL_ID_TC4_TC5; + case 3: GCLK_CLKCTRL_ID_ofthistcc = GCLK_CLKCTRL_ID(GCM_TC6_TC7); break; + default: return; + } + + // Feed GCLK4 to TCC + REG_GCLK_CLKCTRL = (uint16_t) GCLK_CLKCTRL_CLKEN | // Enable GCLK4 + GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4 + GCLK_CLKCTRL_ID_ofthistcc; // Feed GCLK4 to tcc + while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization + + tccConfigured[tccConfig.tcc.tccn] = true; + + if (tccConfig.tcc.tccn>=TCC_INST_NUM) { + Tc* tc = (Tc*)GetTC(tccConfig.tcc.chaninfo); + // disable + tc->COUNT8.CTRLA.bit.ENABLE = 0; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // unfortunately we need the 8-bit counter mode to use the PER register... + tc->COUNT8.CTRLA.reg |= TC_CTRLA_MODE_COUNT8 | TC_CTRLA_WAVEGEN_NPWM ; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // meaning prescaler of 8, since TC-Unit has no up/down mode, and has resolution of 250 rather than 1000... + tc->COUNT8.CTRLA.bit.PRESCALER = TC_CTRLA_PRESCALER_DIV8_Val ; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // period is 250, period cannot be higher than 256! + tc->COUNT8.PER.reg = SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // initial duty cycle is 0 + tc->COUNT8.CC[tccConfig.tcc.chan].reg = 0; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // enable + tc->COUNT8.CTRLA.bit.ENABLE = 1; + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Initialized TC ", tccConfig.tcc.tccn); +#endif + } + else { + Tcc* tcc = (Tcc*)GetTC(tccConfig.tcc.chaninfo); + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + tcc->WAVE.reg |= TCC_WAVE_POL(0xF)|TCC_WAVEB_WAVEGENB_DSBOTH; // Set wave form configuration + while ( tcc->SYNCBUSY.bit.WAVE == 1 ); // wait for sync + + if (hw6pwm>0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + tcc->PER.reg = pwm_resolution - 1; // Set counter Top using the PER register + while ( tcc->SYNCBUSY.bit.PER == 1 ); // wait for sync + + // set all channels to 0% + uint8_t chanCount = (tccConfig.tcc.tccn==1||tccConfig.tcc.tccn==2)?2:4; + for (int i=0;iCC[i].reg = 0; // start off at 0% duty cycle + uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+i); + while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + } + + // Enable TC + tcc->CTRLA.reg |= TCC_CTRLA_ENABLE | TCC_CTRLA_PRESCALER_DIV1; //48Mhz/1=48Mhz/2(up/down)=24MHz/1024=24KHz + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + } + else if (tccConfig.tcc.tccnCTRLA.bit.ENABLE = 0; + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + if (hw6pwm>0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + tcc->CTRLA.bit.ENABLE = 1; + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: (Re-)Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + + +} + + + + + +void writeSAMDDutyCycle(tccConfiguration* info, float dc) { + uint8_t tccn = GetTCNumber(info->tcc.chaninfo); + uint8_t chan = GetTCChannelNumber(info->tcc.chaninfo); + if (tccntcc.chaninfo); + // set via CC +// tcc->CC[chan].reg = (uint32_t)((SIMPLEFOC_SAMD_PWM_RESOLUTION-1) * dc); +// uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+chan); +// while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + // set via CCB + //while ( (tcc->SYNCBUSY.vec.CC & (0x1< 0 ); + tcc->CCB[chan].reg = (uint32_t)((info->pwm_res-1) * dc); +// while ( (tcc->SYNCBUSY.vec.CCB & (0x1< 0 ); +// tcc->STATUS.vec.CCBV |= (0x1<SYNCBUSY.bit.STATUS > 0 ); +// tcc->CTRLBSET.reg |= TCC_CTRLBSET_CMD(TCC_CTRLBSET_CMD_UPDATE_Val); +// while ( tcc->SYNCBUSY.bit.CTRLB > 0 ); + } + else { + Tc* tc = (Tc*)GetTC(info->tcc.chaninfo); + tc->COUNT8.CC[chan].reg = (uint8_t)((SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1) * dc); + while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + } +} + + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp new file mode 100644 index 0000000..71bf0b8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd51_mcu.cpp @@ -0,0 +1,351 @@ + + +#include "./samd_mcu.h" + + +#if defined(_SAMD51_)||defined(_SAME51_) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for SAMD51/SAME51") +#pragma message("") + + + +// expected frequency on DPLL, since we don't configure it ourselves. Typically this is the CPU frequency. +// for custom boards or overclockers you can override it using this define. +#ifndef SIMPLEFOC_SAMD51_DPLL_FREQ +#define SIMPLEFOC_SAMD51_DPLL_FREQ 120000000 +#endif + + +#ifndef TCC3_CH0 +#define TCC3_CH0 NOT_ON_TIMER +#define TCC3_CH1 NOT_ON_TIMER +#endif + +#ifndef TCC4_CH0 +#define TCC4_CH0 NOT_ON_TIMER +#define TCC4_CH1 NOT_ON_TIMER +#endif + + +#ifndef TC4_CH0 +#define TC4_CH0 NOT_ON_TIMER +#define TC4_CH1 NOT_ON_TIMER +#endif + +#ifndef TC5_CH0 +#define TC5_CH0 NOT_ON_TIMER +#define TC5_CH1 NOT_ON_TIMER +#endif + +#ifndef TC6_CH0 +#define TC6_CH0 NOT_ON_TIMER +#define TC6_CH1 NOT_ON_TIMER +#endif + +#ifndef TC7_CH0 +#define TC7_CH0 NOT_ON_TIMER +#define TC7_CH1 NOT_ON_TIMER +#endif + + + +// TCC# Channels WO_NUM Counter size Fault Dithering Output matrix DTI SWAP Pattern generation +// 0 6 8 24-bit Yes Yes Yes Yes Yes Yes +// 1 4 8 24-bit Yes Yes Yes Yes Yes Yes +// 2 3 3 16-bit Yes - Yes - - - +// 3 2 2 16-bit Yes - - - - - +// 4 2 2 16-bit Yes - - - - - + + +#define NUM_WO_ASSOCIATIONS 72 + +struct wo_association WO_associations[] = { + + { PORTB, 9, TC4_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 4, TC0_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 5, TC0_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 6, TC1_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTA, 7, TC1_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTC, 4, NOT_ON_TIMER, 0, TCC0_CH0, 0, NOT_ON_TIMER, 0}, + // PC05, PC06, PC07 -> no timers + { PORTA, 8, TC0_CH0, 0, TCC0_CH0, 0, TCC1_CH0, 4}, + { PORTA, 9, TC0_CH1, 1, TCC0_CH1, 1, TCC1_CH1, 5}, + { PORTA, 10, TC1_CH0, 0, TCC0_CH2, 2, TCC1_CH2, 6}, + { PORTA, 11, TC1_CH1, 1, TCC0_CH3, 3, TCC1_CH3, 7}, + { PORTB, 10, TC5_CH0, 0, TCC0_CH4, 4, TCC1_CH0, 0}, + { PORTB, 11, TC5_CH1, 1, TCC0_CH5, 5, TCC1_CH1, 1}, + { PORTB, 12, TC4_CH0, 0, TCC3_CH0, 0, TCC0_CH0, 0}, + { PORTB, 13, TC4_CH1, 1, TCC3_CH1, 1, TCC0_CH1, 1}, + { PORTB, 14, TC5_CH0, 0, TCC4_CH0, 0, TCC0_CH2, 2}, + { PORTB, 15, TC5_CH1, 1, TCC4_CH1, 1, TCC0_CH3, 3}, + { PORTD, 8, NOT_ON_TIMER, 0, TCC0_CH1, 1, NOT_ON_TIMER, 0}, + { PORTD, 9, NOT_ON_TIMER, 0, TCC0_CH2, 2, NOT_ON_TIMER, 0}, + { PORTD, 10, NOT_ON_TIMER, 0, TCC0_CH3, 3, NOT_ON_TIMER, 0}, + { PORTD, 11, NOT_ON_TIMER, 0, TCC0_CH4, 4, NOT_ON_TIMER, 0}, + { PORTD, 12, NOT_ON_TIMER, 0, TCC0_CH5, 5, NOT_ON_TIMER, 0}, + { PORTC, 10, NOT_ON_TIMER, 0, TCC0_CH0, 0, TCC1_CH0, 4}, + { PORTC, 11, NOT_ON_TIMER, 0, TCC0_CH1, 1, TCC1_CH1, 5}, + { PORTC, 12, NOT_ON_TIMER, 0, TCC0_CH2, 2, TCC1_CH2, 6}, + { PORTC, 13, NOT_ON_TIMER, 0, TCC0_CH3, 3, TCC1_CH3, 7}, + { PORTC, 14, NOT_ON_TIMER, 0, TCC0_CH4, 4, TCC1_CH0, 0}, + { PORTC, 15, NOT_ON_TIMER, 0, TCC0_CH5, 5, TCC1_CH1, 1}, + { PORTA, 12, TC2_CH0, 0, TCC0_CH0, 6, TCC1_CH2, 2}, + { PORTA, 13, TC2_CH1, 1, TCC0_CH1, 7, TCC1_CH3, 3}, + { PORTA, 14, TC3_CH0, 0, TCC2_CH0, 0, TCC1_CH2, 2}, + { PORTA, 15, TC3_CH1, 1, TCC2_CH1, 1, TCC1_CH3, 3}, + { PORTA, 16, TC2_CH0, 0, TCC1_CH0, 0, TCC0_CH4, 4}, + { PORTA, 17, TC2_CH1, 1, TCC1_CH1, 1, TCC0_CH5, 5}, + { PORTA, 18, TC3_CH0, 0, TCC1_CH2, 2, TCC0_CH0, 6}, + { PORTA, 19, TC3_CH1, 1, TCC1_CH3, 3, TCC0_CH1, 7}, + { PORTC, 16, NOT_ON_TIMER, 0, TCC0_CH0, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTC, 17, NOT_ON_TIMER, 0, TCC0_CH1, 1, NOT_ON_TIMER, 0}, // PDEC1 + { PORTC, 18, NOT_ON_TIMER, 0, TCC0_CH2, 2, NOT_ON_TIMER, 0}, // PDEC2 + { PORTC, 19, NOT_ON_TIMER, 0, TCC0_CH3, 3, NOT_ON_TIMER, 0}, + { PORTC, 20, NOT_ON_TIMER, 0, TCC0_CH4, 4, NOT_ON_TIMER, 0}, + { PORTC, 21, NOT_ON_TIMER, 0, TCC0_CH5, 5, NOT_ON_TIMER, 0}, + { PORTC, 22, NOT_ON_TIMER, 0, TCC0_CH0, 6, NOT_ON_TIMER, 0}, + { PORTC, 23, NOT_ON_TIMER, 0, TCC0_CH1, 7, NOT_ON_TIMER, 0}, + { PORTD, 20, NOT_ON_TIMER, 0, TCC1_CH0, 0, NOT_ON_TIMER, 0}, + { PORTD, 21, NOT_ON_TIMER, 0, TCC1_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 16, TC6_CH0, 0, TCC3_CH0, 0, TCC0_CH4, 4}, + { PORTB, 17, TC6_CH1, 1, TCC3_CH1, 1, TCC0_CH5, 5}, + { PORTB, 18, NOT_ON_TIMER, 0, TCC1_CH0, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTB, 19, NOT_ON_TIMER, 0, TCC1_CH1, 1, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 20, NOT_ON_TIMER, 0, TCC1_CH2, 2, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 21, NOT_ON_TIMER, 0, TCC1_CH3, 3, NOT_ON_TIMER, 0}, + { PORTA, 20, TC7_CH0, 0, TCC1_CH0, 4, TCC0_CH0, 0}, + { PORTA, 21, TC7_CH1, 1, TCC1_CH1, 5, TCC0_CH1, 1}, + { PORTA, 22, TC4_CH0, 0, TCC1_CH2, 6, TCC0_CH2, 2}, + { PORTA, 23, TC4_CH1, 1, TCC1_CH3, 7, TCC0_CH3, 3}, + { PORTA, 24, TC5_CH0, 0, TCC2_CH2, 2, NOT_ON_TIMER, 0}, // PDEC0 + { PORTA, 25, TC5_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 22, TC7_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 23, TC7_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC0 + { PORTB, 24, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC1 + { PORTB, 25, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, // PDEC2 + { PORTB, 26, NOT_ON_TIMER, 0, TCC1_CH2, 2, NOT_ON_TIMER, 0}, + { PORTB, 27, NOT_ON_TIMER, 0, TCC1_CH3, 3, NOT_ON_TIMER, 0}, + { PORTB, 28, NOT_ON_TIMER, 0, TCC1_CH0, 4, NOT_ON_TIMER, 0}, + { PORTB, 29, NOT_ON_TIMER, 0, TCC1_CH1, 5, NOT_ON_TIMER, 0}, + // PC24-PC28, PA27, RESET -> no TC/TCC peripherals + { PORTA, 30, TC6_CH0, 0, TCC2_CH0, 0, NOT_ON_TIMER, 0}, + { PORTA, 31, TC6_CH1, 1, TCC2_CH1, 1, NOT_ON_TIMER, 0}, + { PORTB, 30, TC0_CH0, 0, TCC4_CH0, 0, TCC0_CH0, 6}, + { PORTB, 31, TC0_CH1, 1, TCC4_CH1, 1, TCC0_CH1, 7}, + // PC30, PC31 -> no TC/TCC peripherals + { PORTB, 0, TC7_CH0, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTB, 1, TC7_CH1, 1, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}, + { PORTB, 2, TC6_CH0, 0, TCC2_CH2, 2, NOT_ON_TIMER, 0}, + +}; + +wo_association ASSOCIATION_NOT_FOUND = { NOT_A_PORT, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0, NOT_ON_TIMER, 0}; + +#ifndef TCC3_CC_NUM +uint8_t TCC_CHANNEL_COUNT[] = { TCC0_CC_NUM, TCC1_CC_NUM, TCC2_CC_NUM }; +#else +uint8_t TCC_CHANNEL_COUNT[] = { TCC0_CC_NUM, TCC1_CC_NUM, TCC2_CC_NUM, TCC3_CC_NUM, TCC4_CC_NUM }; +#endif + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin) { + for (int i=0;i>pin_position)&0x01)==0x1?PIO_TCC_PDEC:PIO_TIMER_ALT; +} + + + +void syncTCC(Tcc* TCCx) { + while (TCCx->SYNCBUSY.reg & TCC_SYNCBUSY_MASK); +} + + + + +void writeSAMDDutyCycle(tccConfiguration* info, float dc) { + uint8_t tccn = GetTCNumber(info->tcc.chaninfo); + uint8_t chan = GetTCChannelNumber(info->tcc.chaninfo); + if (tccntcc.chaninfo); + // set via CCBUF +// while ( (tcc->SYNCBUSY.vec.CC & (0x1< 0 ); + tcc->CCBUF[chan].reg = (uint32_t)((info->pwm_res-1) * dc); // TODO pwm frequency! + } + else { + // we don't support the TC channels on SAMD51, isn't worth it. + } +} + + +#define DPLL_CLOCK_NUM 2 // use GCLK2 +#define PWM_CLOCK_NUM 3 // use GCLK3 + + +/** + * Configure Clock 4 - we want all simplefoc PWMs to use the same clock. This ensures that + * any compatible pin combination can be used without having to worry about configuring different + * clocks. + */ +void configureSAMDClock() { + if (!SAMDClockConfigured) { + SAMDClockConfigured = true; // mark clock as configured + for (int i=0;iGENCTRL[DPLL_CLOCK_NUM].reg = GCLK_GENCTRL_GENEN | GCLK_GENCTRL_DIV(1) + // | GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL_Val); + // while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<PCHCTRL[1].reg = GCLK_PCHCTRL_GEN(DPLL_CLOCK_NUM)|GCLK_PCHCTRL_CHEN; + // while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<Dpll[0].DPLLCTRLA.bit.ENABLE = 0; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + // OSCCTRL->Dpll[0].DPLLCTRLB.bit.REFCLK = OSCCTRL_DPLLCTRLB_REFCLK_GCLK_Val; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + // OSCCTRL->Dpll[0].DPLLRATIO.reg = 3; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + // OSCCTRL->Dpll[0].DPLLCTRLA.bit.ENABLE = 1; + // while (OSCCTRL->Dpll[0].DPLLSYNCBUSY.reg!=0x0); + + GCLK->GENCTRL[PWM_CLOCK_NUM].bit.GENEN = 0; + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<GENCTRL[PWM_CLOCK_NUM].reg = GCLK_GENCTRL_GENEN | GCLK_GENCTRL_DIV(1) | GCLK_GENCTRL_IDC + //| GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DFLL_Val); + | GCLK_GENCTRL_SRC(GCLK_GENCTRL_SRC_DPLL0_Val); + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<PCHCTRL[GCLK_CLKCTRL_ID_ofthistcc].reg = GCLK_PCHCTRL_GEN(PWM_CLOCK_NUM)|GCLK_PCHCTRL_CHEN; + while (GCLK->SYNCBUSY.vec.GENCTRL&(0x1<CTRLA.bit.ENABLE = 0; //switch off tcc + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + + uint8_t invenMask = ~(1<DRVCTRL.vec.INVEN = (tcc->DRVCTRL.vec.INVEN&invenMask)|invenVal; + syncTCC(tcc); // wait for sync + + // work out pwm resolution for desired frequency and constrain to max/min values + long pwm_resolution = (SIMPLEFOC_SAMD51_DPLL_FREQ/2) / pwm_frequency; + if (pwm_resolution>SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION) + pwm_resolution = SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION; + if (pwm_resolution0.0) { + tcc->WEXCTRL.vec.DTIEN |= (1<WEXCTRL.bit.DTLS = hw6pwm*(pwm_resolution-1); + tcc->WEXCTRL.bit.DTHS = hw6pwm*(pwm_resolution-1); + syncTCC(tcc); // wait for sync + } + + if (!tccConfigured[tccConfig.tcc.tccn]) { + tcc->WAVE.reg |= TCC_WAVE_POL(0xF)|TCC_WAVE_WAVEGEN_DSTOP; // Set wave form configuration - TODO check this... why set like this? + while ( tcc->SYNCBUSY.bit.WAVE == 1 ); // wait for sync + + tcc->PER.reg = pwm_resolution - 1; // Set counter Top using the PER register + while ( tcc->SYNCBUSY.bit.PER == 1 ); // wait for sync + + // set all channels to 0% + for (int i=0;iCC[i].reg = 0; // start off at 0% duty cycle + uint32_t chanbit = 0x1<<(TCC_SYNCBUSY_CC0_Pos+i); + while ( (tcc->SYNCBUSY.reg & chanbit) > 0 ); + } + } + + // Enable TCC + tcc->CTRLA.reg |= TCC_CTRLA_ENABLE | TCC_CTRLA_PRESCALER_DIV1; //48Mhz/1=48Mhz/2(up/down)=24MHz/1024=24KHz + while ( tcc->SYNCBUSY.bit.ENABLE == 1 ); // wait for sync + +#if defined(SIMPLEFOC_SAMD_DEBUG) && !defined(SIMPLEFOC_DISABLE_DEBUG) + SimpleFOCDebug::print("SAMD: (Re-)Initialized TCC "); + SimpleFOCDebug::print(tccConfig.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(tccConfig.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(tccConfig.wo); + SimpleFOCDebug::print("] pwm res "); + SimpleFOCDebug::print((int)pwm_resolution); + SimpleFOCDebug::println(); +#endif + } + else if (tccConfig.tcc.tccn>=TCC_INST_NUM) { + //Tc* tc = (Tc*)GetTC(tccConfig.tcc.chaninfo); + + // disable + // tc->COUNT8.CTRLA.bit.ENABLE = 0; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // unfortunately we need the 8-bit counter mode to use the PER register... + // tc->COUNT8.CTRLA.reg |= TC_CTRLA_MODE_COUNT8 | TC_CTRLA_WAVEGEN_NPWM ; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // meaning prescaler of 8, since TC-Unit has no up/down mode, and has resolution of 250 rather than 1000... + // tc->COUNT8.CTRLA.bit.PRESCALER = TC_CTRLA_PRESCALER_DIV8_Val ; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // period is 250, period cannot be higher than 256! + // tc->COUNT8.PER.reg = SIMPLEFOC_SAMD_PWM_TC_RESOLUTION-1; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // initial duty cycle is 0 + // tc->COUNT8.CC[tccConfig.tcc.chan].reg = 0; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + // // enable + // tc->COUNT8.CTRLA.bit.ENABLE = 1; + // while ( tc->COUNT8.STATUS.bit.SYNCBUSY == 1 ); + + #ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Not initialized: TC ", tccConfig.tcc.tccn); + SIMPLEFOC_DEBUG("SAMD: TC units not supported on SAMD51"); + #endif + } + + // set as configured + tccConfigured[tccConfig.tcc.tccn] = true; + + +} + + + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp new file mode 100644 index 0000000..f697891 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.cpp @@ -0,0 +1,914 @@ + + + +#include "./samd_mcu.h" + +#if defined(_SAMD21_)||defined(_SAMD51_)||defined(_SAME51_) + + + +/** + * Global state + */ +tccConfiguration tccPinConfigurations[SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS]; +uint8_t numTccPinConfigurations = 0; +bool SAMDClockConfigured = false; +bool tccConfigured[TCC_INST_NUM+TC_INST_NUM]; + + + + + +/** + * Attach the TCC to the pin + */ +bool attachTCC(tccConfiguration& tccConfig) { + if (numTccPinConfigurations>=SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS) + return false; + pinMode(tccConfig.pin, OUTPUT); + + pinPeripheral(tccConfig.pin, tccConfig.peripheral); + tccPinConfigurations[numTccPinConfigurations++] = tccConfig; + return true; +} + + + + + +int getPermutationNumber(int pins) { + int num = 1; + for (int i=0;i=TCC_INST_NUM) + return false; + + if (pinAh.tcc.chan==pinBh.tcc.chan || pinAh.tcc.chan==pinBl.tcc.chan || pinAh.tcc.chan==pinCh.tcc.chan || pinAh.tcc.chan==pinCl.tcc.chan) + return false; + if (pinBh.tcc.chan==pinCh.tcc.chan || pinBh.tcc.chan==pinCl.tcc.chan) + return false; + if (pinAl.tcc.chan==pinBh.tcc.chan || pinAl.tcc.chan==pinBl.tcc.chan || pinAl.tcc.chan==pinCh.tcc.chan || pinAl.tcc.chan==pinCl.tcc.chan) + return false; + if (pinBl.tcc.chan==pinCh.tcc.chan || pinBl.tcc.chan==pinCl.tcc.chan) + return false; + + if (pinAh.tcc.chan!=pinAl.tcc.chan || pinBh.tcc.chan!=pinBl.tcc.chan || pinCh.tcc.chan!=pinCl.tcc.chan) + return false; + if (pinAh.wo==pinAl.wo || pinBh.wo==pinBl.wo || pinCh.wo!=pinCl.wo) + return false; + + return true; +} + + + + +bool checkPeripheralPermutationCompatible(tccConfiguration pins[], uint8_t num) { + for (int i=0;i=TCC_INST_NUM || pinAl.tcc.tccn>=TCC_INST_NUM || pinBh.tcc.tccn>=TCC_INST_NUM + || pinBl.tcc.tccn>=TCC_INST_NUM || pinCh.tcc.tccn>=TCC_INST_NUM || pinCl.tcc.tccn>=TCC_INST_NUM) + return false; + + // check we're not in use + if (inUse(pinAh) || inUse(pinAl) || inUse(pinBh) || inUse(pinBl) || inUse(pinCh) || inUse(pinCl)) + return false; + + // check pins are all different tccs/channels + if (pinAh.tcc.chaninfo==pinBh.tcc.chaninfo || pinAh.tcc.chaninfo==pinBl.tcc.chaninfo || pinAh.tcc.chaninfo==pinCh.tcc.chaninfo || pinAh.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinAl.tcc.chaninfo==pinBh.tcc.chaninfo || pinAl.tcc.chaninfo==pinBl.tcc.chaninfo || pinAl.tcc.chaninfo==pinCh.tcc.chaninfo || pinAl.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinBh.tcc.chaninfo==pinCh.tcc.chaninfo || pinBh.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + if (pinBl.tcc.chaninfo==pinCh.tcc.chaninfo || pinBl.tcc.chaninfo==pinCl.tcc.chaninfo) + return false; + + // check H/L pins are on same timer + if (pinAh.tcc.tccn!=pinAl.tcc.tccn || pinBh.tcc.tccn!=pinBl.tcc.tccn || pinCh.tcc.tccn!=pinCl.tcc.tccn) + return false; + + // check H/L pins aren't on both the same timer and wo + if (pinAh.wo==pinAl.wo || pinBh.wo==pinBl.wo || pinCh.wo==pinCl.wo) + return false; + + return true; +} + + + + + +int checkHardware6PWM(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + for (int i=0;i<64;i++) { + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(i, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(i, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(i, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(i, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(i, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(i, 5)); + if (checkPeripheralPermutationSameTCC6(pinAh, pinAl, pinBh, pinBl, pinCh, pinCl)) + return i; + } + return -1; +} + + + + +int checkSoftware6PWM(const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + for (int i=0;i<64;i++) { + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(i, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(i, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(i, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(i, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(i, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(i, 5)); + if (checkPeripheralPermutationCompatible6(pinAh, pinAl, pinBh, pinBl, pinCh, pinCl)) + return i; + } + return -1; +} + + + +int scorePermutation(tccConfiguration pins[], uint8_t num) { + uint32_t usedtccs = 0; + for (int i=0;i>1; + } + for (int i=0;i>1; + } + return score; +} + + + + + + + + +int checkPermutations(uint8_t num, int pins[], bool (*checkFunc)(tccConfiguration[], uint8_t) ) { + tccConfiguration tccConfs[num]; + int best = -1; + int bestscore = 1000000; + for (int i=0;i<(0x1<pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pinB)->pwm_res = tccConfs[1].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + SAMDHardwareDriverParams* params = new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA), getTccPinConfiguration(pinB) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; // Someone with a stepper-setup who can test it? + return params; +} + + + + + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 3PWM setting + * - hardware specific + * + * SAMD21 will support up to 2 BLDC motors in 3-PWM: + * one on TCC0 using 3 of the channels 0,1,2 or 3 + * one on TCC3 using 3 of the channels 0,1,2 or 3 + * i.e. 8 different pins can be used, but only 4 different signals (WO[x]) on those 8 pins + * WO[0] and WO[4] are the same + * WO[1] and WO[5] are the same + * WO[2] and WO[6] are the same + * WO[3] and WO[7] are the same + * + * If you're on the Arduino Nano33 IoT, please see the Nano33 IoT pinout diagram to see which TCC0/WO[x] + * signal is on which pin of the Nano. You can drive one motor on TCC0. For other boards, consult their documentation. + * + * Note: + * That's if we want to keep the signals strictly in sync. + * + * If we can accept out-of-sync PWMs on the different phases, we could drive up to 4 BLDCs in 3-PWM mode, + * using all the TCC channels. (TCC0 & TCC3 - 4 channels each, TCC1 & TCC2 - 2 channels each) + * + * All channels will use the same resolution, prescaler and clock, but they will have different start-times leading + * to misaligned signals. + * + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pinA pinA bldc driver + * @param pinB pinB bldc driver + * @param pinC pinC bldc driver + */ +void* _configure3PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC) { +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int pins[3] = { pinA, pinB, pinC }; + int compatibility = checkPermutations(3, pins, checkPeripheralPermutationCompatible); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + + tccConfiguration tccConfs[3] = { getTCCChannelNr(pinA, getPeripheralOfPermutation(compatibility, 0)), + getTCCChannelNr(pinB, getPeripheralOfPermutation(compatibility, 1)), + getTCCChannelNr(pinC, getPeripheralOfPermutation(compatibility, 2)) }; + + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: score=", scorePermutation(tccConfs, 3)); + printTCCConfiguration(tccConfs[0]); + printTCCConfiguration(tccConfs[1]); + printTCCConfiguration(tccConfs[2]); +#endif + + // attach pins to timer peripherals + attachTCC(tccConfs[0]); // in theory this can fail, but there is no way to signal it... + attachTCC(tccConfs[1]); + attachTCC(tccConfs[2]); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + + // set up clock - Note: if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API-changes in SimpleFOC... + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC (waveform, top-value, pre-scaler = frequency) + configureTCC(tccConfs[0], pwm_frequency); + configureTCC(tccConfs[1], pwm_frequency); + configureTCC(tccConfs[2], pwm_frequency); + getTccPinConfiguration(pinA)->pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pinB)->pwm_res = tccConfs[1].pwm_res; + getTccPinConfiguration(pinC)->pwm_res = tccConfs[2].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA), getTccPinConfiguration(pinB), getTccPinConfiguration(pinC) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; + +} + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param pin1A pin1A stepper driver + * @param pin1B pin1B stepper driver + * @param pin2A pin2A stepper driver + * @param pin2B pin2B stepper driver + */ +void* _configure4PWM(long pwm_frequency, const int pin1A, const int pin1B, const int pin2A, const int pin2B) { +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int pins[4] = { pin1A, pin1B, pin2A, pin2B }; + int compatibility = checkPermutations(4, pins, checkPeripheralPermutationCompatible); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + + tccConfiguration tccConfs[4] = { getTCCChannelNr(pin1A, getPeripheralOfPermutation(compatibility, 0)), + getTCCChannelNr(pin1B, getPeripheralOfPermutation(compatibility, 1)), + getTCCChannelNr(pin2A, getPeripheralOfPermutation(compatibility, 2)), + getTCCChannelNr(pin2B, getPeripheralOfPermutation(compatibility, 3)) }; + + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: score=", scorePermutation(tccConfs, 4)); + printTCCConfiguration(tccConfs[0]); + printTCCConfiguration(tccConfs[1]); + printTCCConfiguration(tccConfs[2]); + printTCCConfiguration(tccConfs[3]); +#endif + + // attach pins to timer peripherals + attachTCC(tccConfs[0]); // in theory this can fail, but there is no way to signal it... + attachTCC(tccConfs[1]); + attachTCC(tccConfs[2]); + attachTCC(tccConfs[3]); +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + + // set up clock - Note: if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API-changes in SimpleFOC... + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC (waveform, top-value, pre-scaler = frequency) + configureTCC(tccConfs[0], pwm_frequency); + configureTCC(tccConfs[1], pwm_frequency); + configureTCC(tccConfs[2], pwm_frequency); + configureTCC(tccConfs[3], pwm_frequency); + getTccPinConfiguration(pin1A)->pwm_res = tccConfs[0].pwm_res; + getTccPinConfiguration(pin2A)->pwm_res = tccConfs[1].pwm_res; + getTccPinConfiguration(pin1B)->pwm_res = tccConfs[2].pwm_res; + getTccPinConfiguration(pin2B)->pwm_res = tccConfs[3].pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pin1A), getTccPinConfiguration(pin1B), getTccPinConfiguration(pin2A), getTccPinConfiguration(pin2B) }, + .pwm_frequency = (uint32_t)pwm_frequency + }; +} + + + + + + + + + +/** + * Configuring PWM frequency, resolution and alignment + * - BLDC driver - 6PWM setting + * - hardware specific + * + * SAMD21 will support up to 2 BLDC motors in 6-PWM: + * one on TCC0 using 3 of the channels 0,1,2 or 3 + * one on TCC3 using 3 of the channels 0,1,2 or 3 + * i.e. 6 out of 8 pins must be used, in the following high/low side pairs: + * WO[0] & WO[4] (high side & low side) + * WO[1] & WO[5] + * WO[2] & WO[6] + * WO[3] & WO[7] + * + * If you're on the Arduino Nano33 IoT, please see the Nano33 IoT pinout diagram to see which TCC0/WO[x] + * signal is on which pin of the Nano. You can drive 1 BLDC on TCC0. For other boards, consult their documentation. + * + * + * @param pwm_frequency - frequency in hertz - if applicable + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low - if applicable + * @param pinA_h pinA high-side bldc driver + * @param pinA_l pinA low-side bldc driver + * @param pinB_h pinA high-side bldc driver + * @param pinB_l pinA low-side bldc driver + * @param pinC_h pinA high-side bldc driver + * @param pinC_l pinA low-side bldc driver + * + * @return 0 if config good, -1 if failed + */ +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + // we want to use a TCC channel with 1 non-inverted and 1 inverted output for each phase, with dead-time insertion +#ifdef SIMPLEFOC_SAMD_DEBUG + printAllPinInfos(); +#endif + int compatibility = checkHardware6PWM(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + if (compatibility<0) { + compatibility = checkSoftware6PWM(pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l); + if (compatibility<0) { + // no result! +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Bad pin combination!"); +#endif + return SIMPLEFOC_DRIVER_INIT_FAILED; + } + } + + tccConfiguration pinAh = getTCCChannelNr(pinA_h, getPeripheralOfPermutation(compatibility, 0)); + tccConfiguration pinAl = getTCCChannelNr(pinA_l, getPeripheralOfPermutation(compatibility, 1)); + tccConfiguration pinBh = getTCCChannelNr(pinB_h, getPeripheralOfPermutation(compatibility, 2)); + tccConfiguration pinBl = getTCCChannelNr(pinB_l, getPeripheralOfPermutation(compatibility, 3)); + tccConfiguration pinCh = getTCCChannelNr(pinC_h, getPeripheralOfPermutation(compatibility, 4)); + tccConfiguration pinCl = getTCCChannelNr(pinC_l, getPeripheralOfPermutation(compatibility, 5)); + +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Found configuration: "); + printTCCConfiguration(pinAh); + printTCCConfiguration(pinAl); + printTCCConfiguration(pinBh); + printTCCConfiguration(pinBl); + printTCCConfiguration(pinCh); + printTCCConfiguration(pinCl); +#endif + + // attach pins to timer peripherals + bool allAttached = true; + allAttached |= attachTCC(pinAh); // in theory this can fail, but there is no way to signal it... + allAttached |= attachTCC(pinAl); + allAttached |= attachTCC(pinBh); + allAttached |= attachTCC(pinBl); + allAttached |= attachTCC(pinCh); + allAttached |= attachTCC(pinCl); + if (!allAttached) + return SIMPLEFOC_DRIVER_INIT_FAILED; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Attached pins..."); +#endif + // set up clock - if we did this right it should be possible to get all TCC units synchronized? + // e.g. attach all the timers, start them, and then start the clock... but this would require API changes in SimpleFOC driver API + configureSAMDClock(); + + if (pwm_frequency==NOT_SET) { + // use default frequency + pwm_frequency = SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ; + } + + // configure the TCC(s) + configureTCC(pinAh, pwm_frequency, false, (pinAh.tcc.chaninfo==pinAl.tcc.chaninfo)?dead_zone:-1); + if ((pinAh.tcc.chaninfo!=pinAl.tcc.chaninfo)) + configureTCC(pinAl, pwm_frequency, true, -1.0); + configureTCC(pinBh, pwm_frequency, false, (pinBh.tcc.chaninfo==pinBl.tcc.chaninfo)?dead_zone:-1); + if ((pinBh.tcc.chaninfo!=pinBl.tcc.chaninfo)) + configureTCC(pinBl, pwm_frequency, true, -1.0); + configureTCC(pinCh, pwm_frequency, false, (pinCh.tcc.chaninfo==pinCl.tcc.chaninfo)?dead_zone:-1); + if ((pinCh.tcc.chaninfo!=pinCl.tcc.chaninfo)) + configureTCC(pinCl, pwm_frequency, true, -1.0); + getTccPinConfiguration(pinA_h)->pwm_res = pinAh.pwm_res; + getTccPinConfiguration(pinA_l)->pwm_res = pinAh.pwm_res; // use the high phase resolution, in case we didn't set it + getTccPinConfiguration(pinB_h)->pwm_res = pinBh.pwm_res; + getTccPinConfiguration(pinB_l)->pwm_res = pinBh.pwm_res; + getTccPinConfiguration(pinC_h)->pwm_res = pinCh.pwm_res; + getTccPinConfiguration(pinC_l)->pwm_res = pinCh.pwm_res; +#ifdef SIMPLEFOC_SAMD_DEBUG + SIMPLEFOC_DEBUG("SAMD: Configured TCCs..."); +#endif + + return new SAMDHardwareDriverParams { + .tccPinConfigurations = { getTccPinConfiguration(pinA_h), getTccPinConfiguration(pinA_l), getTccPinConfiguration(pinB_h), getTccPinConfiguration(pinB_l), getTccPinConfiguration(pinC_h), getTccPinConfiguration(pinC_l) }, + .pwm_frequency = (uint32_t)pwm_frequency, + .dead_zone = dead_zone, + }; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 2PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param pinA phase A hardware pin number + * @param pinB phase B hardware pin number + */ +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_b); + return; +} + + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 3PWM setting + * - hardware specific + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param pinA phase A hardware pin number + * @param pinB phase B hardware pin number + * @param pinC phase C hardware pin number + */ +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params) { + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_b); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[2], dc_c); + return; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - Stepper driver - 4PWM setting + * - hardware specific + * + * @param dc_1a duty cycle phase 1A [0, 1] + * @param dc_1b duty cycle phase 1B [0, 1] + * @param dc_2a duty cycle phase 2A [0, 1] + * @param dc_2b duty cycle phase 2B [0, 1] + * @param pin1A phase 1A hardware pin number + * @param pin1B phase 1B hardware pin number + * @param pin2A phase 2A hardware pin number + * @param pin2B phase 2B hardware pin number + */ +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[0], dc_1a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[1], dc_1b); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[2], dc_2a); + writeSAMDDutyCycle(((SAMDHardwareDriverParams*)params)->tccPinConfigurations[3], dc_2b); + return; +} + + + + +/** + * Function setting the duty cycle to the pwm pin (ex. analogWrite()) + * - BLDC driver - 6PWM setting + * - hardware specific + * + * Note: dead-time must be setup in advance, so parameter "dead_zone" is ignored + * the low side pins are automatically driven by the SAMD DTI module, so it is enough to set the high-side + * duty cycle. + * No sanity checks are perfomed to ensure the pinA, pinB, pinC are the same pins you used in configure method... + * so use appropriately. + * + * @param dc_a duty cycle phase A [0, 1] + * @param dc_b duty cycle phase B [0, 1] + * @param dc_c duty cycle phase C [0, 1] + * @param dead_zone duty cycle protection zone [0, 1] - both low and high side low + * @param pinA_h phase A high-side hardware pin number + * @param pinA_l phase A low-side hardware pin number + * @param pinB_h phase B high-side hardware pin number + * @param pinB_l phase B low-side hardware pin number + * @param pinC_h phase C high-side hardware pin number + * @param pinC_l phase C low-side hardware pin number + * + */ +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + SAMDHardwareDriverParams* p = (SAMDHardwareDriverParams*)params; + tccConfiguration* tcc1 = p->tccPinConfigurations[0]; + tccConfiguration* tcc2 = p->tccPinConfigurations[1]; + uint32_t pwm_res =p->tccPinConfigurations[0]->pwm_res; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + // low-side on a different pin of same TCC - do dead-time in software... + float ls = dc_a+(p->dead_zone * (pwm_res-1)); // TODO resolution!!! + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_a); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_a); // dead-time is done is hardware, no need to set low side pin explicitly + + tcc1 = p->tccPinConfigurations[2]; + tcc2 = p->tccPinConfigurations[3]; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + float ls = dc_b+(p->dead_zone * (pwm_res-1)); + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_b); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_b); + + tcc1 = p->tccPinConfigurations[4]; + tcc2 = p->tccPinConfigurations[5]; + if (tcc1->tcc.chaninfo!=tcc2->tcc.chaninfo) { + float ls = dc_c+(p->dead_zone * (pwm_res-1)); + if (ls>1.0) ls = 1.0f; // no off-time is better than too-short dead-time + writeSAMDDutyCycle(tcc1, dc_c); + writeSAMDDutyCycle(tcc2, ls); + } + else + writeSAMDDutyCycle(tcc1, dc_c); + return; + + _UNUSED(phase_state); +} + + + + +#ifdef SIMPLEFOC_SAMD_DEBUG + +/** + * Prints a table of pin assignments for your SAMD MCU. Very useful since the + * board pinout descriptions and variant.cpp are usually quite wrong, and this + * saves you hours of cross-referencing with the datasheet. + */ +void printAllPinInfos() { + SimpleFOCDebug::println(); + for (uint8_t pin=0;pin=TCC_INST_NUM) + SimpleFOCDebug::print(": TC Peripheral"); + else + SimpleFOCDebug::print(": TCC Peripheral"); + switch (info.peripheral) { + case PIO_TIMER: + SimpleFOCDebug::print(" E "); break; + case PIO_TIMER_ALT: + SimpleFOCDebug::print(" F "); break; +#if defined(_SAMD51_)||defined(_SAME51_) + case PIO_TCC_PDEC: + SimpleFOCDebug::print(" G "); break; +#endif + default: + SimpleFOCDebug::print(" ? "); break; + } + if (info.tcc.tccn>=0) { + SimpleFOCDebug::print(info.tcc.tccn); + SimpleFOCDebug::print("-"); + SimpleFOCDebug::print(info.tcc.chan); + SimpleFOCDebug::print("["); + SimpleFOCDebug::print(info.wo); + SimpleFOCDebug::println("]"); + } + else + SimpleFOCDebug::println(" None"); +} + + + +#endif + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h new file mode 100644 index 0000000..74004d6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/samd/samd_mcu.h @@ -0,0 +1,127 @@ + +#ifndef SAMD_MCU_H +#define SAMD_MCU_H + + +// uncomment to enable debug output from SAMD driver +// can set this as build-flag in Arduino IDE or PlatformIO +// #define SIMPLEFOC_SAMD_DEBUG + +#include "../../hardware_api.h" + + +#if defined(__SAME51J19A__) || defined(__ATSAME51J19A__) +#ifndef _SAME51_ +#define _SAME51_ +#endif +#endif + + +#if defined(_SAMD21_)||defined(_SAMD51_)||defined(_SAME51_) + + +#include "Arduino.h" +#include "variant.h" +#include "wiring_private.h" + + +#ifndef SIMPLEFOC_SAMD_PWM_RESOLUTION +#define SIMPLEFOC_SAMD_PWM_RESOLUTION 1000 +#endif + +#define SIMPLEFOC_SAMD_DEFAULT_PWM_FREQUENCY_HZ 24000 +// arbitrary maximum. On SAMD51 with 120MHz clock this means 2kHz minimum pwm frequency +#define SIMPLEFOC_SAMD_MAX_PWM_RESOLUTION 30000 +// lets not go too low - 400 with clock speed of 120MHz on SAMD51 means 150kHz maximum PWM frequency... +// 400 with 48MHz clock on SAMD21 means 60kHz maximum PWM frequency... +#define SIMPLEFOC_SAMD_MIN_PWM_RESOLUTION 400 +// this is the most we can support on the TC units +#define SIMPLEFOC_SAMD_PWM_TC_RESOLUTION 250 + +#ifndef SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS +#define SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS 24 +#endif + + + +struct tccConfiguration { + uint8_t pin; + EPioType peripheral; // 1=true, 0=false + uint8_t wo; + union tccChanInfo { + struct { + int8_t chan; + int8_t tccn; + }; + uint16_t chaninfo; + } tcc; + uint16_t pwm_res; +}; + + + + + + +struct wo_association { + EPortType port; + uint32_t pin; + ETCChannel tccE; + uint8_t woE; + ETCChannel tccF; + uint8_t woF; +#if defined(_SAMD51_)||defined(_SAME51_) + ETCChannel tccG; + uint8_t woG; +#endif +}; + + + +typedef struct SAMDHardwareDriverParams { + tccConfiguration* tccPinConfigurations[6]; + uint32_t pwm_frequency; + float dead_zone; +} SAMDHardwareDriverParams; + + + + +#if defined(_SAMD21_) +#define NUM_PIO_TIMER_PERIPHERALS 2 +#elif defined(_SAMD51_)||defined(_SAME51_) +#define NUM_PIO_TIMER_PERIPHERALS 3 +#endif + + + +/** + * Global state + */ +extern struct wo_association WO_associations[]; +extern uint8_t TCC_CHANNEL_COUNT[]; +extern tccConfiguration tccPinConfigurations[SIMPLEFOC_SAMD_MAX_TCC_PINCONFIGURATIONS]; +extern uint8_t numTccPinConfigurations; +extern bool SAMDClockConfigured; +extern bool tccConfigured[TCC_INST_NUM+TC_INST_NUM]; + + + +struct wo_association& getWOAssociation(EPortType port, uint32_t pin); +void writeSAMDDutyCycle(tccConfiguration* info, float dc); +void configureSAMDClock(); +void configureTCC(tccConfiguration& tccConfig, long pwm_frequency, bool negate=false, float hw6pwm=-1); +__inline__ void syncTCC(Tcc* TCCx) __attribute__((always_inline, unused)); +EPioType getPeripheralOfPermutation(int permutation, int pin_position); + +#ifdef SIMPLEFOC_SAMD_DEBUG +void printTCCConfiguration(tccConfiguration& info); +void printAllPinInfos(); +#endif + + + +#endif + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp new file mode 100644 index 0000000..65dad9f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.cpp @@ -0,0 +1,952 @@ + +#include "../../hardware_api.h" +#include "stm32_mcu.h" + +#if defined(_STM32_DEF_) + +#pragma message("") +#pragma message("SimpleFOC: compiling for STM32") +#pragma message("") + + +//#define SIMPLEFOC_STM32_DEBUG + +#ifdef SIMPLEFOC_STM32_DEBUG +void printTimerCombination(int numPins, PinMap* timers[], int score); +int getTimerNumber(int timerIndex); +#endif + + + + +#ifndef SIMPLEFOC_STM32_MAX_PINTIMERSUSED +#define SIMPLEFOC_STM32_MAX_PINTIMERSUSED 12 +#endif +int numTimerPinsUsed; +PinMap* timerPinsUsed[SIMPLEFOC_STM32_MAX_PINTIMERSUSED]; + + + + + + +// setting pwm to hardware pin - instead analogWrite() +void _setPwm(HardwareTimer *HT, uint32_t channel, uint32_t value, int resolution) +{ + // TODO - remove commented code + // PinName pin = digitalPinToPinName(ulPin); + // TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(pin, PinMap_PWM); + // uint32_t index = get_timer_index(Instance); + // HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + + HT->setCaptureCompare(channel, value, (TimerCompareFormat_t)resolution); +} + + + + +int getLLChannel(PinMap* timer) { +#if defined(TIM_CCER_CC1NE) + if (STM_PIN_INVERTED(timer->function)) { + switch (STM_PIN_CHANNEL(timer->function)) { + case 1: return LL_TIM_CHANNEL_CH1N; + case 2: return LL_TIM_CHANNEL_CH2N; + case 3: return LL_TIM_CHANNEL_CH3N; +#if defined(LL_TIM_CHANNEL_CH4N) + case 4: return LL_TIM_CHANNEL_CH4N; +#endif + default: return -1; + } + } else +#endif + { + switch (STM_PIN_CHANNEL(timer->function)) { + case 1: return LL_TIM_CHANNEL_CH1; + case 2: return LL_TIM_CHANNEL_CH2; + case 3: return LL_TIM_CHANNEL_CH3; + case 4: return LL_TIM_CHANNEL_CH4; + default: return -1; + } + } + return -1; +} + + + + + +// init pin pwm +HardwareTimer* _initPinPWM(uint32_t PWM_freq, PinMap* timer) { + // sanity check + if (timer==NP) + return NP; + uint32_t index = get_timer_index((TIM_TypeDef*)timer->peripheral); + bool init = false; + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)timer->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + init = true; + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + uint32_t channel = STM_PIN_CHANNEL(timer->function); + HT->pause(); + if (init) + HT->setOverflow(PWM_freq, HERTZ_FORMAT); + HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM1, timer->pin); + #if SIMPLEFOC_PWM_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Configuring high timer ", (int)getTimerNumber(get_timer_index(HardwareTimer_Handle[index]->handle.Instance))); + SIMPLEFOC_DEBUG("STM32-DRV: Configuring high channel ", (int)channel); +#endif + return HT; +} + + + + + + + +// init high side pin +HardwareTimer* _initPinPWMHigh(uint32_t PWM_freq, PinMap* timer) { + HardwareTimer* HT = _initPinPWM(PWM_freq, timer); + #if SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH==false && SIMPLEFOC_PWM_ACTIVE_HIGH==true + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif + return HT; +} + +// init low side pin +HardwareTimer* _initPinPWMLow(uint32_t PWM_freq, PinMap* timer) +{ + uint32_t index = get_timer_index((TIM_TypeDef*)timer->peripheral); + + bool init = false; + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)timer->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + init = true; + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + uint32_t channel = STM_PIN_CHANNEL(timer->function); + +#ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Configuring low timer ", (int)getTimerNumber(get_timer_index(HardwareTimer_Handle[index]->handle.Instance))); + SIMPLEFOC_DEBUG("STM32-DRV: Configuring low channel ", (int)channel); +#endif + + HT->pause(); + if (init) + HT->setOverflow(PWM_freq, HERTZ_FORMAT); + // sets internal fields of HT, but we can't set polarity here + HT->setMode(channel, TIMER_OUTPUT_COMPARE_PWM2, timer->pin); + #if SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(timer), LL_TIM_OCPOLARITY_LOW); + #endif + return HT; +} + + + +// align the timers to end the init +void _alignPWMTimers(HardwareTimer *HT1, HardwareTimer *HT2, HardwareTimer *HT3) +{ + HT1->pause(); + HT1->refresh(); + HT2->pause(); + HT2->refresh(); + HT3->pause(); + HT3->refresh(); + HT1->resume(); + HT2->resume(); + HT3->resume(); +} + + + + +// align the timers to end the init +void _alignPWMTimers(HardwareTimer *HT1, HardwareTimer *HT2, HardwareTimer *HT3, HardwareTimer *HT4) +{ + HT1->pause(); + HT1->refresh(); + HT2->pause(); + HT2->refresh(); + HT3->pause(); + HT3->refresh(); + HT4->pause(); + HT4->refresh(); + HT1->resume(); + HT2->resume(); + HT3->resume(); + HT4->resume(); +} + + +// align the timers to end the init +void _stopTimers(HardwareTimer **timers_to_stop, int timer_num) +{ + // TODO - stop each timer only once + // stop timers + for (int i=0; i < timer_num; i++) { + if(timers_to_stop[i] == NP) return; + timers_to_stop[i]->pause(); + timers_to_stop[i]->refresh(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Stopping timer ", getTimerNumber(get_timer_index(timers_to_stop[i]->getHandle()->Instance))); + #endif + } +} + +// align the timers to end the init +void _startTimers(HardwareTimer **timers_to_start, int timer_num) +{ + // TODO - sart each timer only once + // sart timers + for (int i=0; i < timer_num; i++) { + if(timers_to_start[i] == NP) return; + timers_to_start[i]->resume(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Starting timer ", getTimerNumber(get_timer_index(timers_to_start[i]->getHandle()->Instance))); + #endif + } +} + +void _alignTimersNew() { + int numTimers = 0; + HardwareTimer *timers[numTimerPinsUsed]; + + // reset timer counters + for (int i=0; iperipheral); + HardwareTimer *timer = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + bool found = false; + for (int j=0; jpause(); + timers[i]->refresh(); + #ifdef SIMPLEFOC_STM32_DEBUG + SIMPLEFOC_DEBUG("STM32-DRV: Restarting timer ", getTimerNumber(get_timer_index(timers[i]->getHandle()->Instance))); + #endif + } + + for (int i=0; iresume(); + } + +} + + + + + +// configure hardware 6pwm for a complementary pair of channels +STM32DriverParams* _initHardware6PWMPair(long PWM_freq, float dead_zone, PinMap* pinH, PinMap* pinL, STM32DriverParams* params, int paramsPos) { + // sanity check + if (pinH==NP || pinL==NP) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + +#if defined(STM32L0xx) // L0 boards dont have hardware 6pwm interface + return SIMPLEFOC_DRIVER_INIT_FAILED; // return nothing +#endif + + uint32_t channel1 = STM_PIN_CHANNEL(pinH->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinL->function); + + // more sanity check + if (channel1!=channel2 || pinH->peripheral!=pinL->peripheral) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + uint32_t index = get_timer_index((TIM_TypeDef*)pinH->peripheral); + + if (HardwareTimer_Handle[index] == NULL) { + HardwareTimer_Handle[index]->__this = new HardwareTimer((TIM_TypeDef*)pinH->peripheral); + HardwareTimer_Handle[index]->handle.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED3; + HardwareTimer_Handle[index]->handle.Init.RepetitionCounter = 1; + // HardwareTimer_Handle[index]->handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; + HAL_TIM_Base_Init(&(HardwareTimer_Handle[index]->handle)); + ((HardwareTimer *)HardwareTimer_Handle[index]->__this)->setOverflow((uint32_t)PWM_freq, HERTZ_FORMAT); + } + HardwareTimer *HT = (HardwareTimer *)(HardwareTimer_Handle[index]->__this); + + HT->setMode(channel1, TIMER_OUTPUT_COMPARE_PWM1, pinH->pin); + HT->setMode(channel2, TIMER_OUTPUT_COMPARE_PWM1, pinL->pin); + + // dead time is set in nanoseconds + uint32_t dead_time_ns = (float)(1e9f/PWM_freq)*dead_zone; + uint32_t dead_time = __LL_TIM_CALC_DEADTIME(SystemCoreClock, LL_TIM_GetClockDivision(HT->getHandle()->Instance), dead_time_ns); + if (dead_time>255) dead_time = 255; + if (dead_time==0 && dead_zone>0) { + dead_time = 255; // LL_TIM_CALC_DEADTIME returns 0 if dead_time_ns is too large + SIMPLEFOC_DEBUG("STM32-DRV: WARN: dead time too large, setting to max"); + } + LL_TIM_OC_SetDeadTime(HT->getHandle()->Instance, dead_time); // deadtime is non linear! + #if SIMPLEFOC_PWM_HIGHSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(pinH), LL_TIM_OCPOLARITY_LOW); + #endif + #if SIMPLEFOC_PWM_LOWSIDE_ACTIVE_HIGH==false + LL_TIM_OC_SetPolarity(HT->getHandle()->Instance, getLLChannel(pinL), LL_TIM_OCPOLARITY_LOW); + #endif + LL_TIM_CC_EnableChannel(HT->getHandle()->Instance, getLLChannel(pinH) | getLLChannel(pinL)); + HT->pause(); + + // make sure timer output goes to LOW when timer channels are temporarily disabled + LL_TIM_SetOffStates(HT->getHandle()->Instance, LL_TIM_OSSI_DISABLE, LL_TIM_OSSR_ENABLE); + + params->timers[paramsPos] = HT; + params->timers[paramsPos+1] = HT; + params->channels[paramsPos] = channel1; + params->channels[paramsPos+1] = channel2; + return params; +} + + + + +STM32DriverParams* _initHardware6PWMInterface(long PWM_freq, float dead_zone, PinMap* pinA_h, PinMap* pinA_l, PinMap* pinB_h, PinMap* pinB_l, PinMap* pinC_h, PinMap* pinC_l) { + STM32DriverParams* params = new STM32DriverParams { + .timers = { NP, NP, NP, NP, NP, NP }, + .channels = { 0, 0, 0, 0, 0, 0 }, + .pwm_frequency = PWM_freq, + .dead_zone = dead_zone, + .interface_type = _HARDWARE_6PWM + }; + + if (_initHardware6PWMPair(PWM_freq, dead_zone, pinA_h, pinA_l, params, 0) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + if(_initHardware6PWMPair(PWM_freq, dead_zone, pinB_h, pinB_l, params, 2) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + if (_initHardware6PWMPair(PWM_freq, dead_zone, pinC_h, pinC_l, params, 4) == SIMPLEFOC_DRIVER_INIT_FAILED) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + return params; +} + + + + + + +/* + timer combination scoring function + assigns a score, and also checks the combination is valid + returns <0 if combination is invalid, >=0 if combination is valid. lower (but positive) score is better + for 6 pwm, hardware 6-pwm is preferred over software 6-pwm + hardware 6-pwm is possible if each low channel is the inverted counterpart of its high channel + inverted channels are not allowed except when using hardware 6-pwm (in theory they could be but lets not complicate things) +*/ +int scoreCombination(int numPins, PinMap* pinTimers[]) { + // check already used - TODO move this to outer loop also... + for (int i=0; iperipheral == timerPinsUsed[i]->peripheral + && STM_PIN_CHANNEL(pinTimers[i]->function) == STM_PIN_CHANNEL(timerPinsUsed[i]->function)) + return -2; // bad combination - timer channel already used + } + + // TODO LPTIM and HRTIM should be ignored for now + + // check for inverted channels + if (numPins < 6) { + for (int i=0; ifunction)) + return -3; // bad combination - inverted channel used in non-hardware 6pwm + } + } + // check for duplicated channels + for (int i=0; iperipheral == pinTimers[j]->peripheral + && STM_PIN_CHANNEL(pinTimers[i]->function) == STM_PIN_CHANNEL(pinTimers[j]->function) + && STM_PIN_INVERTED(pinTimers[i]->function) == STM_PIN_INVERTED(pinTimers[j]->function)) + return -4; // bad combination - duplicated channel + } + } + int score = 0; + for (int i=0; iperipheral == pinTimers[j]->peripheral) + found = true; + } + if (!found) score++; + } + if (numPins==6) { + // check for inverted channels - best: 1 timer, 3 channels, 3 matching inverted channels + // >1 timer, 3 channels, 3 matching inverted channels + // 1 timer, 6 channels (no inverted channels) + // >1 timer, 6 channels (no inverted channels) + // check for inverted high-side channels - TODO is this a configuration we should allow? what if all 3 high side channels are inverted and the low-side non-inverted? + if (STM_PIN_INVERTED(pinTimers[0]->function) || STM_PIN_INVERTED(pinTimers[2]->function) || STM_PIN_INVERTED(pinTimers[4]->function)) + return -5; // bad combination - inverted channel used on high-side channel + if (pinTimers[0]->peripheral == pinTimers[1]->peripheral + && pinTimers[2]->peripheral == pinTimers[3]->peripheral + && pinTimers[4]->peripheral == pinTimers[5]->peripheral + && STM_PIN_CHANNEL(pinTimers[0]->function) == STM_PIN_CHANNEL(pinTimers[1]->function) + && STM_PIN_CHANNEL(pinTimers[2]->function) == STM_PIN_CHANNEL(pinTimers[3]->function) + && STM_PIN_CHANNEL(pinTimers[4]->function) == STM_PIN_CHANNEL(pinTimers[5]->function) + && STM_PIN_INVERTED(pinTimers[1]->function) && STM_PIN_INVERTED(pinTimers[3]->function) && STM_PIN_INVERTED(pinTimers[5]->function)) { + // hardware 6pwm, score <10 + + // TODO F37xxx doesn't support dead-time insertion, it has no TIM1/TIM8 + // F301, F302 --> 6 channels, but only 1-3 have dead-time insertion + // TIM2/TIM3/TIM4/TIM5 don't do dead-time insertion + // TIM15/TIM16/TIM17 do dead-time insertion only on channel 1 + + // TODO check these defines + #if defined(STM32F4xx_HAL_TIM_H) || defined(STM32F3xx_HAL_TIM_H) || defined(STM32F2xx_HAL_TIM_H) || defined(STM32F1xx_HAL_TIM_H) || defined(STM32F100_HAL_TIM_H) || defined(STM32FG0x1_HAL_TIM_H) || defined(STM32G0x0_HAL_TIM_H) + if (STM_PIN_CHANNEL(pinTimers[0]->function)>3 || STM_PIN_CHANNEL(pinTimers[2]->function)>3 || STM_PIN_CHANNEL(pinTimers[4]->function)>3 ) + return -8; // channel 4 does not have dead-time insertion + #endif + #ifdef STM32G4xx_HAL_TIM_H + if (STM_PIN_CHANNEL(pinTimers[0]->function)>4 || STM_PIN_CHANNEL(pinTimers[2]->function)>4 || STM_PIN_CHANNEL(pinTimers[4]->function)>4 ) + return -8; // channels 5 & 6 do not have dead-time insertion + #endif + } + else { + // check for inverted low-side channels + if (STM_PIN_INVERTED(pinTimers[1]->function) || STM_PIN_INVERTED(pinTimers[3]->function) || STM_PIN_INVERTED(pinTimers[5]->function)) + return -6; // bad combination - inverted channel used on low-side channel in software 6-pwm + if (pinTimers[0]->peripheral != pinTimers[1]->peripheral + || pinTimers[2]->peripheral != pinTimers[3]->peripheral + || pinTimers[4]->peripheral != pinTimers[5]->peripheral) + return -7; // bad combination - non-matching timers for H/L side in software 6-pwm + score += 10; // software 6pwm, score >10 + } + } + return score; +} + + + + +int findIndexOfFirstPinMapEntry(int pin) { + PinName pinName = digitalPinToPinName(pin); + int i = 0; + while (PinMap_TIM[i].pin!=NC) { + if (pinName == PinMap_TIM[i].pin) + return i; + i++; + } + return -1; +} + + +int findIndexOfLastPinMapEntry(int pin) { + PinName pinName = digitalPinToPinName(pin); + int i = 0; + while (PinMap_TIM[i].pin!=NC) { + if ( pinName == (PinMap_TIM[i].pin & ~ALTX_MASK) + && pinName != (PinMap_TIM[i+1].pin & ~ALTX_MASK)) + return i; + i++; + } + return -1; +} + + + + + + +#define NOT_FOUND 1000 + +int findBestTimerCombination(int numPins, int index, int pins[], PinMap* pinTimers[]) { + PinMap* searchArray[numPins]; + for (int j=0;j=0 && score= 0) { + #ifdef SIMPLEFOC_STM32_DEBUG + SimpleFOCDebug::print("STM32-DRV: best: "); + printTimerCombination(numPins, pinTimers, bestScore); + #endif + } + return bestScore; +}; + + + +void* _configure1PWM(long pwm_frequency, const int pinA) { + if (numTimerPinsUsed+1 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[1] = { pinA }; + PinMap* pinTimers[1] = { NP }; + if (findBestTimerCombination(1, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]);\ + // allign the timers + _alignTimersNew(); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1 }, + .channels = { channel1 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + return params; +} + + + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if (numTimerPinsUsed+2 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[2] = { pinA, pinB }; + PinMap* pinTimers[2] = { NP, NP }; + if (findBestTimerCombination(2, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + // allign the timers + _alignPWMTimers(HT1, HT2, HT2); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2 }, + .channels = { channel1, channel2 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + return params; +} + + + + + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if (numTimerPinsUsed+3 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[3] = { pinA, pinB, pinC }; + PinMap* pinTimers[3] = { NP, NP, NP }; + if (findBestTimerCombination(3, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWM(pwm_frequency, pinTimers[2]); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2, HT3 }, + .channels = { channel1, channel2, channel3 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[2]; + + _alignTimersNew(); + + return params; +} + + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC, const int pinD) { + if (numTimerPinsUsed+4 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + int pins[4] = { pinA, pinB, pinC, pinD }; + PinMap* pinTimers[4] = { NP, NP, NP, NP }; + if (findBestTimerCombination(4, pins, pinTimers)<0) + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + + HardwareTimer* HT1 = _initPinPWM(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWM(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWM(pwm_frequency, pinTimers[2]); + HardwareTimer* HT4 = _initPinPWM(pwm_frequency, pinTimers[3]); + // allign the timers + _alignPWMTimers(HT1, HT2, HT3, HT4); + + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + uint32_t channel4 = STM_PIN_CHANNEL(pinTimers[3]->function); + + STM32DriverParams* params = new STM32DriverParams { + .timers = { HT1, HT2, HT3, HT4 }, + .channels = { channel1, channel2, channel3, channel4 }, + .pwm_frequency = pwm_frequency + }; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[0]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[1]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[2]; + timerPinsUsed[numTimerPinsUsed++] = pinTimers[3]; + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - DC motor - 1PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params){ + // transform duty cycle from [0,1] to [0,255] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +//- hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_b, _PWM_RESOLUTION); +} + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +//- hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_b, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_c, _PWM_RESOLUTION); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +//- hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,4095] + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_1a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _PWM_RANGE*dc_1b, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_2a, _PWM_RESOLUTION); + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], _PWM_RANGE*dc_2b, _PWM_RESOLUTION); +} + + + + +// Configuring PWM frequency, resolution and alignment +// - BLDC driver - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l){ + if (numTimerPinsUsed+6 > SIMPLEFOC_STM32_MAX_PINTIMERSUSED) { + SIMPLEFOC_DEBUG("STM32-DRV: ERR: too many pins used"); + return (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + } + if( !pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to |%0kHz max + // center-aligned frequency is uses two periods + pwm_frequency *=2; + + // find configuration + int pins[6] = { pinA_h, pinA_l, pinB_h, pinB_l, pinC_h, pinC_l }; + PinMap* pinTimers[6] = { NP, NP, NP, NP, NP, NP }; + int score = findBestTimerCombination(6, pins, pinTimers); + + STM32DriverParams* params; + // configure accordingly + if (score<0) + params = (STM32DriverParams*)SIMPLEFOC_DRIVER_INIT_FAILED; + else if (score<10) // hardware pwm + params = _initHardware6PWMInterface(pwm_frequency, dead_zone, pinTimers[0], pinTimers[1], pinTimers[2], pinTimers[3], pinTimers[4], pinTimers[5]); + else { // software pwm + HardwareTimer* HT1 = _initPinPWMHigh(pwm_frequency, pinTimers[0]); + HardwareTimer* HT2 = _initPinPWMLow(pwm_frequency, pinTimers[1]); + HardwareTimer* HT3 = _initPinPWMHigh(pwm_frequency, pinTimers[2]); + HardwareTimer* HT4 = _initPinPWMLow(pwm_frequency, pinTimers[3]); + HardwareTimer* HT5 = _initPinPWMHigh(pwm_frequency, pinTimers[4]); + HardwareTimer* HT6 = _initPinPWMLow(pwm_frequency, pinTimers[5]); + uint32_t channel1 = STM_PIN_CHANNEL(pinTimers[0]->function); + uint32_t channel2 = STM_PIN_CHANNEL(pinTimers[1]->function); + uint32_t channel3 = STM_PIN_CHANNEL(pinTimers[2]->function); + uint32_t channel4 = STM_PIN_CHANNEL(pinTimers[3]->function); + uint32_t channel5 = STM_PIN_CHANNEL(pinTimers[4]->function); + uint32_t channel6 = STM_PIN_CHANNEL(pinTimers[5]->function); + params = new STM32DriverParams { + .timers = { HT1, HT2, HT3, HT4, HT5, HT6 }, + .channels = { channel1, channel2, channel3, channel4, channel5, channel6 }, + .pwm_frequency = pwm_frequency, + .dead_zone = dead_zone, + .interface_type = _SOFTWARE_6PWM + }; + } + if (score>=0) { + for (int i=0; i<6; i++) + timerPinsUsed[numTimerPinsUsed++] = pinTimers[i]; + _alignTimersNew(); + } + return params; // success +} + + + +void _setSinglePhaseState(PhaseState state, HardwareTimer *HT, int channel1,int channel2) { + _UNUSED(channel2); + switch (state) { + case PhaseState::PHASE_OFF: + // Due to a weird quirk in the arduino timer API, pauseChannel only disables the complementary channel (e.g. CC1NE). + // To actually make the phase floating, we must also set pwm to 0. + HT->pauseChannel(channel1); + break; + default: + HT->resumeChannel(channel1); + break; + } +} + + +// Function setting the duty cycle to the pwm pin (ex. analogWrite()) +// - BLDC driver - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState* phase_state, void* params){ + switch(((STM32DriverParams*)params)->interface_type){ + case _HARDWARE_6PWM: + // phase a + _setSinglePhaseState(phase_state[0], ((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], ((STM32DriverParams*)params)->channels[1]); + if(phase_state[0] == PhaseState::PHASE_OFF) dc_a = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _PWM_RANGE*dc_a, _PWM_RESOLUTION); + // phase b + _setSinglePhaseState(phase_state[1], ((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], ((STM32DriverParams*)params)->channels[3]); + if(phase_state[1] == PhaseState::PHASE_OFF) dc_b = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _PWM_RANGE*dc_b, _PWM_RESOLUTION); + // phase c + _setSinglePhaseState(phase_state[2], ((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], ((STM32DriverParams*)params)->channels[5]); + if(phase_state[2] == PhaseState::PHASE_OFF) dc_c = 0.0f; + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], _PWM_RANGE*dc_c, _PWM_RESOLUTION); + break; + case _SOFTWARE_6PWM: + float dead_zone = ((STM32DriverParams*)params)->dead_zone / 2.0f; + if (phase_state[0] == PhaseState::PHASE_ON || phase_state[0] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], _constrain(dc_a - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[0], ((STM32DriverParams*)params)->channels[0], 0.0f, _PWM_RESOLUTION); + if (phase_state[0] == PhaseState::PHASE_ON || phase_state[0] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], _constrain(dc_a + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[1], ((STM32DriverParams*)params)->channels[1], 0.0f, _PWM_RESOLUTION); + + if (phase_state[1] == PhaseState::PHASE_ON || phase_state[1] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], _constrain(dc_b - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[2], ((STM32DriverParams*)params)->channels[2], 0.0f, _PWM_RESOLUTION); + if (phase_state[1] == PhaseState::PHASE_ON || phase_state[1] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], _constrain(dc_b + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[3], ((STM32DriverParams*)params)->channels[3], 0.0f, _PWM_RESOLUTION); + + if (phase_state[2] == PhaseState::PHASE_ON || phase_state[2] == PhaseState::PHASE_HI) + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], _constrain(dc_c - dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[4], ((STM32DriverParams*)params)->channels[4], 0.0f, _PWM_RESOLUTION); + if (phase_state[2] == PhaseState::PHASE_ON || phase_state[2] == PhaseState::PHASE_LO) + _setPwm(((STM32DriverParams*)params)->timers[5], ((STM32DriverParams*)params)->channels[5], _constrain(dc_c + dead_zone, 0.0f, 1.0f)*_PWM_RANGE, _PWM_RESOLUTION); + else + _setPwm(((STM32DriverParams*)params)->timers[5], ((STM32DriverParams*)params)->channels[5], 0.0f, _PWM_RESOLUTION); + break; + } + _UNUSED(phase_state); +} + + + +#ifdef SIMPLEFOC_STM32_DEBUG + +int getTimerNumber(int timerIndex) { + #if defined(TIM1_BASE) + if (timerIndex==TIMER1_INDEX) return 1; + #endif + #if defined(TIM2_BASE) + if (timerIndex==TIMER2_INDEX) return 2; + #endif + #if defined(TIM3_BASE) + if (timerIndex==TIMER3_INDEX) return 3; + #endif + #if defined(TIM4_BASE) + if (timerIndex==TIMER4_INDEX) return 4; + #endif + #if defined(TIM5_BASE) + if (timerIndex==TIMER5_INDEX) return 5; + #endif + #if defined(TIM6_BASE) + if (timerIndex==TIMER6_INDEX) return 6; + #endif + #if defined(TIM7_BASE) + if (timerIndex==TIMER7_INDEX) return 7; + #endif + #if defined(TIM8_BASE) + if (timerIndex==TIMER8_INDEX) return 8; + #endif + #if defined(TIM9_BASE) + if (timerIndex==TIMER9_INDEX) return 9; + #endif + #if defined(TIM10_BASE) + if (timerIndex==TIMER10_INDEX) return 10; + #endif + #if defined(TIM11_BASE) + if (timerIndex==TIMER11_INDEX) return 11; + #endif + #if defined(TIM12_BASE) + if (timerIndex==TIMER12_INDEX) return 12; + #endif + #if defined(TIM13_BASE) + if (timerIndex==TIMER13_INDEX) return 13; + #endif + #if defined(TIM14_BASE) + if (timerIndex==TIMER14_INDEX) return 14; + #endif + #if defined(TIM15_BASE) + if (timerIndex==TIMER15_INDEX) return 15; + #endif + #if defined(TIM16_BASE) + if (timerIndex==TIMER16_INDEX) return 16; + #endif + #if defined(TIM17_BASE) + if (timerIndex==TIMER17_INDEX) return 17; + #endif + #if defined(TIM18_BASE) + if (timerIndex==TIMER18_INDEX) return 18; + #endif + #if defined(TIM19_BASE) + if (timerIndex==TIMER19_INDEX) return 19; + #endif + #if defined(TIM20_BASE) + if (timerIndex==TIMER20_INDEX) return 20; + #endif + #if defined(TIM21_BASE) + if (timerIndex==TIMER21_INDEX) return 21; + #endif + #if defined(TIM22_BASE) + if (timerIndex==TIMER22_INDEX) return 22; + #endif + return -1; +} + + +void printTimerCombination(int numPins, PinMap* timers[], int score) { + for (int i=0; iperipheral))); + SimpleFOCDebug::print("-CH"); + SimpleFOCDebug::print(STM_PIN_CHANNEL(timers[i]->function)); + if (STM_PIN_INVERTED(timers[i]->function)) + SimpleFOCDebug::print("N"); + } + SimpleFOCDebug::print(" "); + } + SimpleFOCDebug::println("score: ", score); +} + +#endif + + + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h new file mode 100644 index 0000000..fa6280e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/stm32/stm32_mcu.h @@ -0,0 +1,32 @@ +#ifndef STM32_DRIVER_MCU_DEF +#define STM32_DRIVER_MCU_DEF +#include "../../hardware_api.h" + +#if defined(_STM32_DEF_) + +// default pwm parameters +#define _PWM_RESOLUTION 12 // 12bit +#define _PWM_RANGE 4095.0f // 2^12 -1 = 4095 +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// 6pwm parameters +#define _HARDWARE_6PWM 1 +#define _SOFTWARE_6PWM 0 +#define _ERROR_6PWM -1 + + +typedef struct STM32DriverParams { + HardwareTimer* timers[6] = {NULL}; + uint32_t channels[6]; + long pwm_frequency; + float dead_zone; + uint8_t interface_type; +} STM32DriverParams; + +// timer synchornisation functions +void _stopTimers(HardwareTimer **timers_to_stop, int timer_num=6); +void _startTimers(HardwareTimer **timers_to_start, int timer_num=6); + +#endif +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp new file mode 100644 index 0000000..3a7b5de --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy3_mcu.cpp @@ -0,0 +1,228 @@ +#include "teensy_mcu.h" + +// if defined +// - Teensy 3.0 MK20DX128 +// - Teensy 3.1/3.2 MK20DX256 +// - Teensy 3.5 MK20DX128 +// - Teensy LC MKL26Z64 +// - Teensy 3.5 MK64FX512 +// - Teensy 3.6 MK66FX1M0 +#if defined(__arm__) && defined(CORE_TEENSY) && (defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MKL26Z64__) || defined(__MK64FX512__) || defined(__MK66FX1M0__)) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Teensy 3.x") +#pragma message("") + +// pin definition from https://github.com/PaulStoffregen/cores/blob/286511f3ec849a6c9e0ec8b73ad6a2fada52e44c/teensy3/pins_teensy.c +#if defined(__MK20DX128__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#elif defined(__MK20DX256__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 32 +#define FTM2_CH1_PIN 25 +#elif defined(__MKL26Z64__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM1_CH0_PIN 16 +#define FTM1_CH1_PIN 17 +#define FTM2_CH0_PIN 3 +#define FTM2_CH1_PIN 4 +#elif defined(__MK64FX512__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 29 +#define FTM2_CH1_PIN 30 +#define FTM3_CH0_PIN 2 +#define FTM3_CH1_PIN 14 +#define FTM3_CH2_PIN 7 +#define FTM3_CH3_PIN 8 +#define FTM3_CH4_PIN 35 +#define FTM3_CH5_PIN 36 +#define FTM3_CH6_PIN 37 +#define FTM3_CH7_PIN 38 +#elif defined(__MK66FX1M0__) +#define FTM0_CH0_PIN 22 +#define FTM0_CH1_PIN 23 +#define FTM0_CH2_PIN 9 +#define FTM0_CH3_PIN 10 +#define FTM0_CH4_PIN 6 +#define FTM0_CH5_PIN 20 +#define FTM0_CH6_PIN 21 +#define FTM0_CH7_PIN 5 +#define FTM1_CH0_PIN 3 +#define FTM1_CH1_PIN 4 +#define FTM2_CH0_PIN 29 +#define FTM2_CH1_PIN 30 +#define FTM3_CH0_PIN 2 +#define FTM3_CH1_PIN 14 +#define FTM3_CH2_PIN 7 +#define FTM3_CH3_PIN 8 +#define FTM3_CH4_PIN 35 +#define FTM3_CH5_PIN 36 +#define FTM3_CH6_PIN 37 +#define FTM3_CH7_PIN 38 +#define TPM1_CH0_PIN 16 +#define TPM1_CH1_PIN 17 +#endif + +int _findTimer( const int Ah, const int Al, const int Bh, const int Bl, const int Ch, const int Cl){ + + if((Ah == FTM0_CH0_PIN && Al == FTM0_CH1_PIN) || + (Ah == FTM0_CH2_PIN && Al == FTM0_CH3_PIN) || + (Ah == FTM0_CH4_PIN && Al == FTM0_CH5_PIN) ){ + if((Bh == FTM0_CH0_PIN && Bl == FTM0_CH1_PIN) || + (Bh == FTM0_CH2_PIN && Bl == FTM0_CH3_PIN) || + (Bh == FTM0_CH4_PIN && Bl == FTM0_CH5_PIN) ){ + if((Ch == FTM0_CH0_PIN && Cl == FTM0_CH1_PIN) || + (Ch == FTM0_CH2_PIN && Cl == FTM0_CH3_PIN) || + (Ch == FTM0_CH4_PIN && Cl == FTM0_CH5_PIN) ){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: Using timer FTM0."); +#endif + // timer FTM0 + return 0; + } + } + } + + #ifdef FTM3_SC // if the board has FTM3 timer + if((Ah == FTM3_CH0_PIN && Al == FTM3_CH1_PIN) || + (Ah == FTM3_CH2_PIN && Al == FTM3_CH3_PIN) || + (Ah == FTM3_CH4_PIN && Al == FTM3_CH5_PIN) ){ + if((Bh == FTM3_CH0_PIN && Bl == FTM3_CH1_PIN) || + (Bh == FTM3_CH2_PIN && Bl == FTM3_CH3_PIN) || + (Bh == FTM3_CH4_PIN && Bl == FTM3_CH5_PIN) ){ + if((Ch == FTM3_CH0_PIN && Cl == FTM3_CH1_PIN) || + (Ch == FTM3_CH2_PIN && Cl == FTM3_CH3_PIN) || + (Ch == FTM3_CH4_PIN && Cl == FTM3_CH5_PIN) ){ + // timer FTM3 +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: Using timer FTM3."); +#endif + return 3; + } + } + } + #endif + +#ifdef SIMPLEFOC_TEENSY_DEBUG + SIMPLEFOC_DEBUG("TEENSY-DRV: ERR: Pins not on timers FTM0 or FTM3!"); +#endif + return -1; + +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 6PWM setting +// - hardware specific +void* _configure6PWM(long pwm_frequency, float dead_zone, const int pinA_h, const int pinA_l, const int pinB_h, const int pinB_l, const int pinC_h, const int pinC_l) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + unsigned long pwm_freq = 2*pwm_frequency; // center-aligned pwm has 4 times lower freq + _setHighFrequency(pwm_freq, pinA_h); + _setHighFrequency(pwm_freq, pinA_l); + _setHighFrequency(pwm_freq, pinB_h); + _setHighFrequency(pwm_freq, pinB_l); + _setHighFrequency(pwm_freq, pinC_h); + _setHighFrequency(pwm_freq, pinC_l); + + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA_h,pinA_l, pinB_h,pinB_l, pinC_h, pinC_l }, + .pwm_frequency = pwm_frequency + }; + + + int timer = _findTimer(pinA_h,pinA_l,pinB_h,pinB_l,pinC_h,pinC_l); + if(timer<0) return SIMPLEFOC_DRIVER_INIT_FAILED; + + // find the best combination of prescalers and counter value + double dead_time = dead_zone/pwm_freq; + int prescaler = 1; // initial prescaler (1,4 or 16) + double count = 1; // inital count (1 - 63) + for (; prescaler<=16; prescaler*=4){ + count = dead_time*((double)F_CPU)/((double)prescaler); + if(count < 64) break; // found the solution + } + count = _constrain(count, 1, 63); + + // configure the timer + if(timer==0){ + // Configure FTM0 + // // inverting and deadtime insertion for FTM1 + FTM0_COMBINE = 0x00121212; // 0x2 - complemetary mode, 0x1 - dead timer insertion enabled + + // Deadtime config + FTM0_DEADTIME = (int)count; // set counter - 1-63 + FTM0_DEADTIME |= ((prescaler>1) << 7) | ((prescaler>4) << 6); // set prescaler (0b01 - 1, 0b10 - 4, 0b11 - 16) + + // configure center aligned PWM + FTM0_SC = 0x00000028; // 0x2 - center-alignment, 0x8 - fixed clock freq + }else if(timer==3){ + // Configure FTM3 + // inverting and deadtime insertion for FTM1 + FTM3_COMBINE = 0x00121212; // 0x2 - complemetary mode, 0x1 - dead timer insertion enabled + + // Deadtime config + FTM3_DEADTIME = (int)count; // set counter - 1-63 + FTM3_DEADTIME |= ((prescaler>1) << 7) | ((prescaler>4) << 6); // set prescaler (0b01 - 1, 0b10 - 4, 0b11 - 16) + + // configure center aligned PWM + FTM3_SC = 0x00000028; // 0x2 - center-alignment, 0x8 - fixed clock freq + } + + return params; +} + + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 6PWM setting +// - hardware specific +void _writeDutyCycle6PWM(float dc_a, float dc_b, float dc_c, PhaseState *phase_state, void* params){ + _UNUSED(phase_state); + // transform duty cycle from [0,1] to [0,255] + // phase A + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_a); + + // phase B + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_b); + + // phase C + analogWrite(((GenericDriverParams*)params)->pins[4], 255.0f*dc_c); + analogWrite(((GenericDriverParams*)params)->pins[5], 255.0f*dc_c); +} +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp new file mode 100644 index 0000000..322d5a3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.cpp @@ -0,0 +1,306 @@ +#include "teensy_mcu.h" +#include "teensy4_mcu.h" +#include "../../../communication/SimpleFOCDebug.h" + +// if defined +// - Teensy 4.0 +// - Teensy 4.1 +#if defined(__arm__) && defined(CORE_TEENSY) && ( defined(__IMXRT1062__) || defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41) || defined(ARDUINO_TEENSY_MICROMOD) ) + + +#pragma message("") +#pragma message("SimpleFOC: compiling for Teensy 4.x") +#pragma message("") + + +// half_cycle of the PWM variable +int half_cycle = 0; + + +// function which finds the flexpwm instance for a pair of pins +// if they do not belong to the same timer it returns a nullpointer +IMXRT_FLEXPWM_t* get_flexpwm(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return nullptr; + } + info = pwm_pin_info + pin; + // FlexPWM pin + IMXRT_FLEXPWM_t *flexpwm1,*flexpwm2; + switch ((info->module >> 4) & 3) { + case 0: flexpwm1 = &IMXRT_FLEXPWM1; break; + case 1: flexpwm1 = &IMXRT_FLEXPWM2; break; + case 2: flexpwm1 = &IMXRT_FLEXPWM3; break; + default: flexpwm1 = &IMXRT_FLEXPWM4; + } + + info = pwm_pin_info + pin1; + switch ((info->module >> 4) & 3) { + case 0: flexpwm2 = &IMXRT_FLEXPWM1; break; + case 1: flexpwm2 = &IMXRT_FLEXPWM2; break; + case 2: flexpwm2 = &IMXRT_FLEXPWM3; break; + default: flexpwm2 = &IMXRT_FLEXPWM4; + } + if(flexpwm1 == flexpwm2){ + char s[60]; + sprintf (s, "TEENSY-DRV: Pins: %d, %d on Flextimer %d.", pin, pin1, ((info->module >> 4) & 3) + 1); + SIMPLEFOC_DEBUG(s); + return flexpwm1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not in same Flextimer!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return nullptr; + } +} + + +// function which finds the timer submodule for a pair of pins +// if they do not belong to the same submodule it returns a -1 +int get_submodule(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } + + info = pwm_pin_info + pin; + int sm1 = info->module&0x3; + info = pwm_pin_info + pin1; + int sm2 = info->module&0x3; + + if (sm1 == sm2) { + char s[60]; + sprintf (s, "TEENSY-DRV: Pins: %d, %d on submodule %d.", pin, pin1, sm1); + SIMPLEFOC_DEBUG(s); + return sm1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[50]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not in same submodule!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } +} + + +// function which finds the timer submodule for a pair of pins +// if they do not belong to the same submodule it returns a -1 +int get_inverted_channel(uint8_t pin, uint8_t pin1){ + + const struct pwm_pin_info_struct *info; + if (pin >= CORE_NUM_DIGITAL || pin1 >= CORE_NUM_DIGITAL){ +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pins: %d, %d not Flextimer pins!", pin, pin1); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } + + info = pwm_pin_info + pin; + int ch1 = info->channel; + info = pwm_pin_info + pin1; + int ch2 = info->channel; + + if (ch1 != 1) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Pin: %d on channel %s - only A supported", pin1, ch1==2 ? "B" : "X"); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } else if (ch2 != 2) { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: ERR: Inverted pin: %d on channel %s - only B supported", pin1, ch2==1 ? "A" : "X"); + SIMPLEFOC_DEBUG(s); +#endif + return -1; + } else { +#ifdef SIMPLEFOC_TEENSY_DEBUG + char s[60]; + sprintf (s, "TEENSY-DRV: Pin: %d on channel B inverted.", pin1); + SIMPLEFOC_DEBUG(s); +#endif +return ch2; + } +} + +// Helper to set up A/B pair on a FlexPWM submodule. +// can configure sync, prescale and B inversion. +// sets the desired frequency of the PWM +// sets the center-aligned pwm +void setup_pwm_pair (IMXRT_FLEXPWM_t * flexpwm, int submodule, const long frequency, float dead_zone ) +{ + int submodule_mask = 1 << submodule ; + flexpwm->MCTRL &= ~ FLEXPWM_MCTRL_RUN (submodule_mask) ; // stop it if its already running + flexpwm->MCTRL |= FLEXPWM_MCTRL_CLDOK (submodule_mask) ; // clear load OK + + + // calculate the counter and prescaler for the desired pwm frequency + uint32_t newdiv = (uint32_t)((float)F_BUS_ACTUAL / frequency + 0.5f); + uint32_t prescale = 0; + //printf(" div=%lu\n", newdiv); + while (newdiv > 65535 && prescale < 7) { + newdiv = newdiv >> 1; + prescale = prescale + 1; + } + if (newdiv > 65535) { + newdiv = 65535; + } else if (newdiv < 2) { + newdiv = 2; + } + + // the halfcycle of the PWM + half_cycle = int(newdiv/2.0f); + int dead_time = int(dead_zone*half_cycle); //default dead-time - 2% + int mid_pwm = int((half_cycle)/2.0f); + + // setup the timer + // https://github.com/PaulStoffregen/cores/blob/master/teensy4/imxrt.h + flexpwm->SM[submodule].CTRL2 = FLEXPWM_SMCTRL2_WAITEN | FLEXPWM_SMCTRL2_DBGEN | + FLEXPWM_SMCTRL2_FRCEN | FLEXPWM_SMCTRL2_INIT_SEL(0) | FLEXPWM_SMCTRL2_FORCE_SEL(6); + flexpwm->SM[submodule].CTRL = FLEXPWM_SMCTRL_FULL | FLEXPWM_SMCTRL_HALF | FLEXPWM_SMCTRL_PRSC(prescale) ; + // https://github.com/PaulStoffregen/cores/blob/70ba01accd728abe75ebfc8dcd8b3d3a8f3e3f25/teensy4/imxrt.h#L4948 + flexpwm->SM[submodule].OCTRL = 0;//channel_to_invert==2 ? 0 : FLEXPWM_SMOCTRL_POLA | FLEXPWM_SMOCTRL_POLB ; + flexpwm->SM[submodule].DTCNT0 = dead_time ; // should try this out (deadtime control) + flexpwm->SM[submodule].DTCNT1 = dead_time ; // should try this out (deadtime control) + // flexpwm->SM[submodule].TCTRL = FLEXPWM_SMTCTRL_OUT_TRIG_EN (0b000010) ; // sync trig out on VAL1 match. + flexpwm->SM[submodule].INIT = -half_cycle; // count from -HALFCYCLE to +HALFCYCLE + flexpwm->SM[submodule].VAL0 = 0 ; + flexpwm->SM[submodule].VAL1 = half_cycle ; + flexpwm->SM[submodule].VAL2 = -mid_pwm ; + flexpwm->SM[submodule].VAL3 = +mid_pwm ; + // flexpwm->SM[submodule].VAL4 = -mid_pwm ; + // flexpwm->SM[submodule].VAL5 = +mid_pwm ; + + flexpwm->MCTRL |= FLEXPWM_MCTRL_LDOK (submodule_mask) ; // loading reenabled + flexpwm->MCTRL |= FLEXPWM_MCTRL_RUN (submodule_mask) ; // start it running +} + + +// staring the PWM on A and B channels of the submodule +void startup_pwm_pair (IMXRT_FLEXPWM_t * flexpwm, int submodule) +{ + int submodule_mask = 1 << submodule ; + + flexpwm->OUTEN |= FLEXPWM_OUTEN_PWMA_EN (submodule_mask); // enable A output + flexpwm->OUTEN |= FLEXPWM_OUTEN_PWMB_EN (submodule_mask); // enable B output +} + + + +// PWM setting on the high and low pair of the PWM channels +void write_pwm_pair(IMXRT_FLEXPWM_t * flexpwm, int submodule, float duty){ + + int mid_pwm = int((half_cycle)/2.0f); + int count_pwm = int(mid_pwm*(duty*2-1)) + mid_pwm; + + flexpwm->SM[submodule].VAL2 = count_pwm; // A on + flexpwm->SM[submodule].VAL3 = -count_pwm ; // A off + // flexpwm->SM[submodule].VAL4 = - count_pwm ; // B off (assuming B inverted) + // flexpwm->SM[submodule].VAL5 = + count_pwm ; // B on + flexpwm->MCTRL |= FLEXPWM_MCTRL_LDOK (1<flextimers[0], ((Teensy4DriverParams*)params)->submodules[0], dc_a); + write_pwm_pair (((Teensy4DriverParams*)params)->flextimers[1], ((Teensy4DriverParams*)params)->submodules[1], dc_b); + write_pwm_pair (((Teensy4DriverParams*)params)->flextimers[2], ((Teensy4DriverParams*)params)->submodules[2], dc_c); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h new file mode 100644 index 0000000..5e38462 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy4_mcu.h @@ -0,0 +1,110 @@ +#include "teensy_mcu.h" + +// if defined +// - Teensy 4.0 +// - Teensy 4.1 +#if defined(__arm__) && defined(CORE_TEENSY) && ( defined(__IMXRT1062__) || defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41) || defined(ARDUINO_TEENSY_MICROMOD) ) + +// teensy 4 driver configuration parameters +typedef struct Teensy4DriverParams { + IMXRT_FLEXPWM_t* flextimers[3] = {NULL}; + int submodules[3]; + long pwm_frequency; + float dead_zone; +} Teensy4DriverParams; + + +// pin definition from https://github.com/PaulStoffregen/cores/blob/master/teensy4/pwm.c +struct pwm_pin_info_struct { + uint8_t type; // 0=no pwm, 1=flexpwm, 2=quad + uint8_t module; // 0-3, 0-3 + uint8_t channel; // 0=X, 1=A, 2=B + uint8_t muxval; // +}; +#define M(a, b) ((((a) - 1) << 4) | (b)) +#if defined(__IMXRT1062__) +const struct pwm_pin_info_struct pwm_pin_info[] = { + {1, M(1, 1), 0, 4}, // FlexPWM1_1_X 0 // AD_B0_03 + {1, M(1, 0), 0, 4}, // FlexPWM1_0_X 1 // AD_B0_02 + {1, M(4, 2), 1, 1}, // FlexPWM4_2_A 2 // EMC_04 + {1, M(4, 2), 2, 1}, // FlexPWM4_2_B 3 // EMC_05 + {1, M(2, 0), 1, 1}, // FlexPWM2_0_A 4 // EMC_06 + {1, M(2, 1), 1, 1}, // FlexPWM2_1_A 5 // EMC_08 + {1, M(2, 2), 1, 2}, // FlexPWM2_2_A 6 // B0_10 + {1, M(1, 3), 2, 6}, // FlexPWM1_3_B 7 // B1_01 + {1, M(1, 3), 1, 6}, // FlexPWM1_3_A 8 // B1_00 + {1, M(2, 2), 2, 2}, // FlexPWM2_2_B 9 // B0_11 + {2, M(1, 0), 0, 1}, // QuadTimer1_0 10 // B0_00 + {2, M(1, 2), 0, 1}, // QuadTimer1_2 11 // B0_02 + {2, M(1, 1), 0, 1}, // QuadTimer1_1 12 // B0_01 + {2, M(2, 0), 0, 1}, // QuadTimer2_0 13 // B0_03 + {2, M(3, 2), 0, 1}, // QuadTimer3_2 14 // AD_B1_02 + {2, M(3, 3), 0, 1}, // QuadTimer3_3 15 // AD_B1_03 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {2, M(3, 1), 0, 1}, // QuadTimer3_1 18 // AD_B1_01 + {2, M(3, 0), 0, 1}, // QuadTimer3_0 19 // AD_B1_00 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(4, 0), 1, 1}, // FlexPWM4_0_A 22 // AD_B1_08 + {1, M(4, 1), 1, 1}, // FlexPWM4_1_A 23 // AD_B1_09 + {1, M(1, 2), 0, 4}, // FlexPWM1_2_X 24 // AD_B0_12 + {1, M(1, 3), 0, 4}, // FlexPWM1_3_X 25 // AD_B0_13 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(3, 1), 2, 1}, // FlexPWM3_1_B 28 // EMC_32 + {1, M(3, 1), 1, 1}, // FlexPWM3_1_A 29 // EMC_31 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(2, 0), 2, 1}, // FlexPWM2_0_B 33 // EMC_07 +#ifdef ARDUINO_TEENSY40 + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 34 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 35 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 36 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 37 // SD_B0_00 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 38 // SD_B0_05 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 39 // SD_B0_04 +#endif +#ifdef ARDUINO_TEENSY41 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(2, 3), 1, 6}, // FlexPWM2_3_A 36 // B1_00 + {1, M(2, 3), 2, 6}, // FlexPWM2_3_B 37 // B1_01 + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {0, M(1, 0), 0, 0}, + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 42 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 43 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 44 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 45 // SD_B0_00 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 46 // SD_B0_05 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 47 // SD_B0_04 + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_0_B + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_2_A + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_2_B + {1, M(3, 3), 2, 1}, // FlexPWM3_3_B 51 // EMC_22 + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_1_B + {0, M(1, 0), 0, 0}, // duplicate FlexPWM1_1_A + {1, M(3, 0), 1, 1}, // FlexPWM3_0_A 54 // EMC_29 +#endif +#ifdef ARDUINO_TEENSY_MICROMOD + {1, M(1, 1), 2, 1}, // FlexPWM1_1_B 34 // SD_B0_03 + {1, M(1, 1), 1, 1}, // FlexPWM1_1_A 35 // SD_B0_02 + {1, M(1, 0), 2, 1}, // FlexPWM1_0_B 36 // SD_B0_01 + {1, M(1, 0), 1, 1}, // FlexPWM1_0_A 37 // SD_B0_00 + {1, M(1, 2), 1, 1}, // FlexPWM1_2_A 38 // SD_B0_04 + {1, M(1, 2), 2, 1}, // FlexPWM1_2_B 39 // SD_B0_05 + {2, M(2, 1), 0, 1}, // QuadTimer2_1 40 // B0_04 + {2, M(2, 2), 0, 1}, // QuadTimer2_2 41 // B0_05 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_0 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_1 + {0, M(1, 0), 0, 0}, // duplicate QuadTimer3_2 + {2, M(4, 0), 0, 1}, // QuadTimer4_0 45 // B0_09 +#endif +}; + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp new file mode 100644 index 0000000..dcfa3e1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.cpp @@ -0,0 +1,116 @@ +#include "teensy_mcu.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin){ + analogWrite(pin, 0); + analogWriteFrequency(pin, freq); +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure1PWM(long pwm_frequency, const int pinA) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 2PWM setting +// - hardware speciffic +void* _configure2PWM(long pwm_frequency, const int pinA, const int pinB) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - BLDC motor - 3PWM setting +// - hardware speciffic +void* _configure3PWM(long pwm_frequency,const int pinA, const int pinB, const int pinC) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC }, + .pwm_frequency = pwm_frequency + }; + return params; +} + +// function setting the high pwm frequency to the supplied pins +// - Stepper motor - 4PWM setting +// - hardware speciffic +void* _configure4PWM(long pwm_frequency, const int pinA, const int pinB, const int pinC, const int pinD) { + if(!pwm_frequency || !_isset(pwm_frequency) ) pwm_frequency = _PWM_FREQUENCY; // default frequency 25khz + else pwm_frequency = _constrain(pwm_frequency, 0, _PWM_FREQUENCY_MAX); // constrain to 50kHz max + _setHighFrequency(pwm_frequency, pinA); + _setHighFrequency(pwm_frequency, pinB); + _setHighFrequency(pwm_frequency, pinC); + _setHighFrequency(pwm_frequency, pinD); + GenericDriverParams* params = new GenericDriverParams { + .pins = { pinA, pinB, pinC, pinD }, + .pwm_frequency = pwm_frequency + }; + return params; +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle1PWM(float dc_a, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); +} + + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 2PWM setting +// - hardware speciffic +void _writeDutyCycle2PWM(float dc_a, float dc_b, void* params) { + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); +} + + +// function setting the pwm duty cycle to the hardware +// - BLDC motor - 3PWM setting +// - hardware speciffic +void _writeDutyCycle3PWM(float dc_a, float dc_b, float dc_c, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_c); +} + +// function setting the pwm duty cycle to the hardware +// - Stepper motor - 4PWM setting +// - hardware speciffic +void _writeDutyCycle4PWM(float dc_1a, float dc_1b, float dc_2a, float dc_2b, void* params){ + // transform duty cycle from [0,1] to [0,255] + analogWrite(((GenericDriverParams*)params)->pins[0], 255.0f*dc_1a); + analogWrite(((GenericDriverParams*)params)->pins[1], 255.0f*dc_1b); + analogWrite(((GenericDriverParams*)params)->pins[2], 255.0f*dc_2a); + analogWrite(((GenericDriverParams*)params)->pins[3], 255.0f*dc_2b); +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h new file mode 100644 index 0000000..7956ea9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/drivers/hardware_specific/teensy/teensy_mcu.h @@ -0,0 +1,14 @@ +#include "../../hardware_api.h" + +#if defined(__arm__) && defined(CORE_TEENSY) + +#define _PWM_FREQUENCY 25000 // 25khz +#define _PWM_FREQUENCY_MAX 50000 // 50khz + +// debugging output +#define SIMPLEFOC_TEENSY_DEBUG + +// configure High PWM frequency +void _setHighFrequency(const long freq, const int pin); + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.cpp new file mode 100644 index 0000000..fa4b8e7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.cpp @@ -0,0 +1,229 @@ +#include "Encoder.h" + + +/* + Encoder(int encA, int encB , int cpr, int index) + - encA, encB - encoder A and B pins + - cpr - counts per rotation number (cpm=ppm*4) + - index pin - (optional input) +*/ + +Encoder::Encoder(int _encA, int _encB , float _ppr, int _index){ + + // Encoder measurement structure init + // hardware pins + pinA = _encA; + pinB = _encB; + // counter setup + pulse_counter = 0; + pulse_timestamp = 0; + + cpr = _ppr; + A_active = 0; + B_active = 0; + I_active = 0; + // index pin + index_pin = _index; // its 0 if not used + + // velocity calculation variables + prev_Th = 0; + pulse_per_second = 0; + prev_pulse_counter = 0; + prev_timestamp_us = _micros(); + + // extern pullup as default + pullup = Pullup::USE_EXTERN; + // enable quadrature encoder by default + quadrature = Quadrature::ON; +} + +// Encoder interrupt callback functions +// A channel +void Encoder::handleA() { + bool A = digitalRead(pinA); + switch (quadrature){ + case Quadrature::ON: + // CPR = 4xPPR + if ( A != A_active ) { + pulse_counter += (A_active == B_active) ? 1 : -1; + pulse_timestamp = _micros(); + A_active = A; + } + break; + case Quadrature::OFF: + // CPR = PPR + if(A && !digitalRead(pinB)){ + pulse_counter++; + pulse_timestamp = _micros(); + } + break; + } +} +// B channel +void Encoder::handleB() { + bool B = digitalRead(pinB); + switch (quadrature){ + case Quadrature::ON: + // // CPR = 4xPPR + if ( B != B_active ) { + pulse_counter += (A_active != B_active) ? 1 : -1; + pulse_timestamp = _micros(); + B_active = B; + } + break; + case Quadrature::OFF: + // CPR = PPR + if(B && !digitalRead(pinA)){ + pulse_counter--; + pulse_timestamp = _micros(); + } + break; + } +} + +// Index channel +void Encoder::handleIndex() { + if(hasIndex()){ + bool I = digitalRead(index_pin); + if(I && !I_active){ + index_found = true; + // align encoder on each index + long tmp = pulse_counter; + // corrent the counter value + pulse_counter = round((double)pulse_counter/(double)cpr)*cpr; + // preserve relative speed + prev_pulse_counter += pulse_counter - tmp; + } + I_active = I; + } +} + + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void Encoder::update() { + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + angle_prev_ts = pulse_timestamp; + long copy_pulse_counter = pulse_counter; + interrupts(); + // TODO: numerical precision issue here if the pulse_counter overflows the angle will be lost + full_rotations = copy_pulse_counter / (int)cpr; + angle_prev = _2PI * ((copy_pulse_counter) % ((int)cpr)) / ((float)cpr); +} + +/* + Shaft angle calculation +*/ +float Encoder::getSensorAngle(){ + return _2PI * (pulse_counter) / ((float)cpr); +} + + + +/* + Shaft velocity calculation + function using mixed time and frequency measurement technique +*/ +float Encoder::getVelocity(){ + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + long copy_pulse_counter = pulse_counter; + long copy_pulse_timestamp = pulse_timestamp; + interrupts(); + // timestamp + long timestamp_us = _micros(); + // sampling time calculation + float Ts = (timestamp_us - prev_timestamp_us) * 1e-6f; + // quick fix for strange cases (micros overflow) + if(Ts <= 0 || Ts > 0.5f) Ts = 1e-3f; + + // time from last impulse + float Th = (timestamp_us - copy_pulse_timestamp) * 1e-6f; + long dN = copy_pulse_counter - prev_pulse_counter; + + // Pulse per second calculation (Eq.3.) + // dN - impulses received + // Ts - sampling time - time in between function calls + // Th - time from last impulse + // Th_1 - time form last impulse of the previous call + // only increment if some impulses received + float dt = Ts + prev_Th - Th; + pulse_per_second = (dN != 0 && dt > Ts/2) ? dN / dt : pulse_per_second; + + // if more than 0.05f passed in between impulses + if ( Th > 0.1f) pulse_per_second = 0; + + // velocity calculation + float velocity = pulse_per_second / ((float)cpr) * (_2PI); + + // save variables for next pass + prev_timestamp_us = timestamp_us; + // save velocity calculation variables + prev_Th = Th; + prev_pulse_counter = copy_pulse_counter; + return velocity; +} + +// getter for index pin +// return -1 if no index +int Encoder::needsSearch(){ + return hasIndex() && !index_found; +} + +// private function used to determine if encoder has index +int Encoder::hasIndex(){ + return index_pin != 0; +} + + +// encoder initialisation of the hardware pins +// and calculation variables +void Encoder::init(){ + + // Encoder - check if pullup needed for your encoder + if(pullup == Pullup::USE_INTERN){ + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + if(hasIndex()) pinMode(index_pin,INPUT_PULLUP); + }else{ + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + if(hasIndex()) pinMode(index_pin,INPUT); + } + + // counter setup + pulse_counter = 0; + pulse_timestamp = _micros(); + // velocity calculation variables + prev_Th = 0; + pulse_per_second = 0; + prev_pulse_counter = 0; + prev_timestamp_us = _micros(); + + // initial cpr = PPR + // change it if the mode is quadrature + if(quadrature == Quadrature::ON) cpr = 4*cpr; + + // we don't call Sensor::init() here because init is handled in Encoder class. +} + +// function enabling hardware interrupts of the for the callback provided +// if callback is not provided then the interrupt is not enabled +void Encoder::enableInterrupts(void (*doA)(), void(*doB)(), void(*doIndex)()){ + // attach interrupt if functions provided + switch(quadrature){ + case Quadrature::ON: + // A callback and B callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, CHANGE); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, CHANGE); + break; + case Quadrature::OFF: + // A callback and B callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, RISING); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, RISING); + break; + } + + // if index used initialize the index interrupt + if(hasIndex() && doIndex != nullptr) attachInterrupt(digitalPinToInterrupt(index_pin), doIndex, CHANGE); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.h new file mode 100644 index 0000000..af6a5ab --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/Encoder.h @@ -0,0 +1,91 @@ +#ifndef ENCODER_LIB_H +#define ENCODER_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/base_classes/Sensor.h" + + +/** + * Quadrature mode configuration structure + */ +enum Quadrature : uint8_t { + ON = 0x00, //!< Enable quadrature mode CPR = 4xPPR + OFF = 0x01 //!< Disable quadrature mode / CPR = PPR +}; + +class Encoder: public Sensor{ + public: + /** + Encoder class constructor + @param encA encoder B pin + @param encB encoder B pin + @param ppr impulses per rotation (cpr=ppr*4) + @param index index pin number (optional input) + */ + Encoder(int encA, int encB , float ppr, int index = 0); + + /** encoder initialise pins */ + void init() override; + /** + * function enabling hardware interrupts for the encoder channels with provided callback functions + * if callback is not provided then the interrupt is not enabled + * + * @param doA pointer to the A channel interrupt handler function + * @param doB pointer to the B channel interrupt handler function + * @param doIndex pointer to the Index channel interrupt handler function + * + */ + void enableInterrupts(void (*doA)() = nullptr, void(*doB)() = nullptr, void(*doIndex)() = nullptr); + + // Encoder interrupt callback functions + /** A channel callback function */ + void handleA(); + /** B channel callback function */ + void handleB(); + /** Index channel callback function */ + void handleIndex(); + + + // pins A and B + int pinA; //!< encoder hardware pin A + int pinB; //!< encoder hardware pin B + int index_pin; //!< index pin + + // Encoder configuration + Pullup pullup; //!< Configuration parameter internal or external pullups + Quadrature quadrature;//!< Configuration parameter enable or disable quadrature mode + float cpr;//!< encoder cpr number + + // Abstract functions of the Sensor class implementation + /** get current angle (rad) */ + float getSensorAngle() override; + /** get current angular velocity (rad/s) */ + float getVelocity() override; + virtual void update() override; + + /** + * returns 0 if it does need search for absolute zero + * 0 - encoder without index + * 1 - ecoder with index + */ + int needsSearch() override; + + private: + int hasIndex(); //!< function returning 1 if encoder has index pin and 0 if not. + + volatile long pulse_counter;//!< current pulse counter + volatile long pulse_timestamp;//!< last impulse timestamp in us + volatile int A_active; //!< current active states of A channel + volatile int B_active; //!< current active states of B channel + volatile int I_active; //!< current active states of Index channel + volatile bool index_found = false; //!< flag stating that the index has been found + + // velocity calculation variables + float prev_Th, pulse_per_second; + volatile long prev_pulse_counter, prev_timestamp_us; +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.cpp new file mode 100644 index 0000000..3d4025f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.cpp @@ -0,0 +1,26 @@ +#include "GenericSensor.h" + + +/* + GenericSensor( float (*readCallback)() ) + - readCallback - pointer to the function which reads the sensor angle. +*/ + +GenericSensor::GenericSensor(float (*readCallback)(), void (*initCallback)()){ + // if function provided add it to the + if(readCallback != nullptr) this->readCallback = readCallback; + if(initCallback != nullptr) this->initCallback = initCallback; +} + +void GenericSensor::init(){ + // if init callback specified run it + if(initCallback != nullptr) this->initCallback(); + this->Sensor::init(); // call base class init +} + +/* + Shaft angle calculation +*/ +float GenericSensor::getSensorAngle(){ + return this->readCallback(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.h new file mode 100644 index 0000000..ba0dfe5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/GenericSensor.h @@ -0,0 +1,31 @@ +#ifndef GENERIC_SENSOR_LIB_H +#define GENERIC_SENSOR_LIB_H + +#include "Arduino.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" +#include "../common/base_classes/Sensor.h" + + +class GenericSensor: public Sensor{ + public: + /** + GenericSensor class constructor + * @param readCallback pointer to the function reading the sensor angle + * @param initCallback pointer to the function initialising the sensor + */ + GenericSensor(float (*readCallback)() = nullptr, void (*initCallback)() = nullptr); + + float (*readCallback)() = nullptr; //!< function pointer to sensor reading + void (*initCallback)() = nullptr; //!< function pointer to sensor initialisation + + void init() override; + + // Abstract functions of the Sensor class implementation + /** get current angle (rad) */ + float getSensorAngle() override; + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.cpp new file mode 100644 index 0000000..38767d5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.cpp @@ -0,0 +1,173 @@ +#include "HallSensor.h" + + +/* + HallSensor(int hallA, int hallB , int cpr, int index) + - hallA, hallB, hallC - HallSensor A, B and C pins + - pp - pole pairs +*/ +HallSensor::HallSensor(int _hallA, int _hallB, int _hallC, int _pp){ + + // hardware pins + pinA = _hallA; + pinB = _hallB; + pinC = _hallC; + + // hall has 6 segments per electrical revolution + cpr = _pp * 6; + + // extern pullup as default + pullup = Pullup::USE_EXTERN; +} + +// HallSensor interrupt callback functions +// A channel +void HallSensor::handleA() { + A_active= digitalRead(pinA); + updateState(); +} +// B channel +void HallSensor::handleB() { + B_active = digitalRead(pinB); + updateState(); +} + +// C channel +void HallSensor::handleC() { + C_active = digitalRead(pinC); + updateState(); +} + +/** + * Updates the state and sector following an interrupt + */ +void HallSensor::updateState() { + long new_pulse_timestamp = _micros(); + + int8_t new_hall_state = C_active + (B_active << 1) + (A_active << 2); + + // glitch avoidance #1 - sometimes we get an interrupt but pins haven't changed + if (new_hall_state == hall_state) { + return; + } + hall_state = new_hall_state; + + int8_t new_electric_sector = ELECTRIC_SECTORS[hall_state]; + static Direction old_direction; + if (new_electric_sector - electric_sector > 3) { + //underflow + direction = Direction::CCW; + electric_rotations += direction; + } else if (new_electric_sector - electric_sector < (-3)) { + //overflow + direction = Direction::CW; + electric_rotations += direction; + } else { + direction = (new_electric_sector > electric_sector)? Direction::CW : Direction::CCW; + } + electric_sector = new_electric_sector; + + // glitch avoidance #2 changes in direction can cause velocity spikes. Possible improvements needed in this area + if (direction == old_direction) { + // not oscilating or just changed direction + pulse_diff = new_pulse_timestamp - pulse_timestamp; + } else { + pulse_diff = 0; + } + + pulse_timestamp = new_pulse_timestamp; + total_interrupts++; + old_direction = direction; + if (onSectorChange != nullptr) onSectorChange(electric_sector); +} + +/** + * Optionally set a function callback to be fired when sector changes + * void onSectorChange(int sector) { + * ... // for debug or call driver directly? + * } + * sensor.attachSectorCallback(onSectorChange); + */ +void HallSensor::attachSectorCallback(void (*_onSectorChange)(int sector)) { + onSectorChange = _onSectorChange; +} + + + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void HallSensor::update() { + // Copy volatile variables in minimal-duration blocking section to ensure no interrupts are missed + noInterrupts(); + angle_prev_ts = pulse_timestamp; + long last_electric_rotations = electric_rotations; + int8_t last_electric_sector = electric_sector; + interrupts(); + angle_prev = ((float)((last_electric_rotations * 6 + last_electric_sector) % cpr) / (float)cpr) * _2PI ; + full_rotations = (int32_t)((last_electric_rotations * 6 + last_electric_sector) / cpr); +} + + + +/* + Shaft angle calculation + TODO: numerical precision issue here if the electrical rotation overflows the angle will be lost +*/ +float HallSensor::getSensorAngle() { + return ((float)(electric_rotations * 6 + electric_sector) / (float)cpr) * _2PI ; +} + +/* + Shaft velocity calculation + function using mixed time and frequency measurement technique +*/ +float HallSensor::getVelocity(){ + noInterrupts(); + long last_pulse_timestamp = pulse_timestamp; + long last_pulse_diff = pulse_diff; + interrupts(); + if (last_pulse_diff == 0 || ((long)(_micros() - last_pulse_timestamp) > last_pulse_diff*2) ) { // last velocity isn't accurate if too old + return 0; + } else { + return direction * (_2PI / (float)cpr) / (last_pulse_diff / 1000000.0f); + } + +} + +// HallSensor initialisation of the hardware pins +// and calculation variables +void HallSensor::init(){ + // initialise the electrical rotations to 0 + electric_rotations = 0; + + // HallSensor - check if pullup needed for your HallSensor + if(pullup == Pullup::USE_INTERN){ + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + pinMode(pinC, INPUT_PULLUP); + }else{ + pinMode(pinA, INPUT); + pinMode(pinB, INPUT); + pinMode(pinC, INPUT); + } + + // init hall_state + A_active= digitalRead(pinA); + B_active = digitalRead(pinB); + C_active = digitalRead(pinC); + updateState(); + + pulse_timestamp = _micros(); + + // we don't call Sensor::init() here because init is handled in HallSensor class. +} + +// function enabling hardware interrupts for the callback provided +// if callback is not provided then the interrupt is not enabled +void HallSensor::enableInterrupts(void (*doA)(), void(*doB)(), void(*doC)()){ + // attach interrupt if functions provided + + // A, B and C callback + if(doA != nullptr) attachInterrupt(digitalPinToInterrupt(pinA), doA, CHANGE); + if(doB != nullptr) attachInterrupt(digitalPinToInterrupt(pinB), doB, CHANGE); + if(doC != nullptr) attachInterrupt(digitalPinToInterrupt(pinC), doC, CHANGE); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.h new file mode 100644 index 0000000..ad50c7d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/HallSensor.h @@ -0,0 +1,98 @@ +#ifndef HALL_SENSOR_LIB_H +#define HALL_SENSOR_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// seq 1 > 5 > 4 > 6 > 2 > 3 > 1 000 001 010 011 100 101 110 111 +const int8_t ELECTRIC_SECTORS[8] = { -1, 0, 4, 5, 2, 1, 3 , -1 }; + +class HallSensor: public Sensor{ + public: + /** + HallSensor class constructor + @param encA HallSensor A pin + @param encB HallSensor B pin + @param encC HallSensor C pin + @param pp pole pairs (e.g hoverboard motor has 15pp and small gimbals often have 7pp) + @param index index pin number (optional input) + */ + HallSensor(int encA, int encB, int encC, int pp); + + /** HallSensor initialise pins */ + void init(); + /** + * function enabling hardware interrupts for the HallSensor channels with provided callback functions + * if callback is not provided then the interrupt is not enabled + * + * @param doA pointer to the A channel interrupt handler function + * @param doB pointer to the B channel interrupt handler function + * @param doIndex pointer to the Index channel interrupt handler function + * + */ + void enableInterrupts(void (*doA)() = nullptr, void(*doB)() = nullptr, void(*doC)() = nullptr); + + // HallSensor interrupt callback functions + /** A channel callback function */ + void handleA(); + /** B channel callback function */ + void handleB(); + /** C channel callback function */ + void handleC(); + + + // pins A and B + int pinA; //!< HallSensor hardware pin A + int pinB; //!< HallSensor hardware pin B + int pinC; //!< HallSensor hardware pin C + + // HallSensor configuration + Pullup pullup; //!< Configuration parameter internal or external pullups + int cpr;//!< HallSensor cpr number + + // Abstract functions of the Sensor class implementation + /** Interrupt-safe update */ + void update() override; + /** get current angle (rad) */ + float getSensorAngle() override; + /** get current angular velocity (rad/s) */ + float getVelocity() override; + + // whether last step was CW (+1) or CCW (-1). + Direction direction; + + void attachSectorCallback(void (*onSectorChange)(int a) = nullptr); + + // the current 3bit state of the hall sensors + volatile int8_t hall_state; + // the current sector of the sensor. Each sector is 60deg electrical + volatile int8_t electric_sector; + // the number of electric rotations + volatile long electric_rotations; + // this is sometimes useful to identify interrupt issues (e.g. weak or no pullup resulting in 1000s of interrupts) + volatile long total_interrupts; + + // variable used to filter outliers - rad/s + float velocity_max = 1000.0f; + + private: + + Direction decodeDirection(int oldState, int newState); + void updateState(); + + volatile unsigned long pulse_timestamp;//!< last impulse timestamp in us + volatile int A_active; //!< current active states of A channel + volatile int B_active; //!< current active states of B channel + volatile int C_active; //!< current active states of C channel + + // function pointer for on sector change call back + void (*onSectorChange)(int sector) = nullptr; + + volatile long pulse_diff; + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.cpp new file mode 100644 index 0000000..d4adad6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.cpp @@ -0,0 +1,42 @@ +#include "MagneticSensorAnalog.h" + +/** MagneticSensorAnalog(uint8_t _pinAnalog, int _min, int _max) + * @param _pinAnalog the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading. Whilst you might expect it to be 0 it is often ~15. Getting this wrong results in a small click once per revolution + * @param _max_raw_count the largest value read. whilst you might expect it to be 2^10 = 1023 it is often ~ 1020. Note: For ESP32 (with 12bit ADC the value will be nearer 4096) + */ +MagneticSensorAnalog::MagneticSensorAnalog(uint8_t _pinAnalog, int _min_raw_count, int _max_raw_count){ + + pinAnalog = _pinAnalog; + + cpr = _max_raw_count - _min_raw_count; + min_raw_count = _min_raw_count; + max_raw_count = _max_raw_count; + + if(pullup == Pullup::USE_INTERN){ + pinMode(pinAnalog, INPUT_PULLUP); + }else{ + pinMode(pinAnalog, INPUT); + } + +} + + +void MagneticSensorAnalog::init(){ + raw_count = getRawCount(); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorAnalog::getSensorAngle(){ + // raw data from the sensor + raw_count = getRawCount(); + return ( (float) (raw_count) / (float)cpr) * _2PI; +} + +// function reading the raw counter of the magnetic sensor +int MagneticSensorAnalog::getRawCount(){ + return analogRead(pinAnalog); +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.h new file mode 100644 index 0000000..6f787b9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorAnalog.h @@ -0,0 +1,51 @@ +#ifndef MAGNETICSENSORANALOG_LIB_H +#define MAGNETICSENSORANALOG_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +/** + * This sensor has been tested with AS5600 running in 'analog mode'. This is where output pin of AS6000 is connected to an analog pin on your microcontroller. + * This approach is very simple but you may more accurate results with MagneticSensorI2C if that is also supported as its skips the DAC -> ADC conversion (ADC supports MagneticSensorI2C) + */ +class MagneticSensorAnalog: public Sensor{ + public: + /** + * MagneticSensorAnalog class constructor + * @param _pinAnalog the pin to read the PWM signal + */ + MagneticSensorAnalog(uint8_t _pinAnalog, int _min = 0, int _max = 0); + + + /** sensor initialise pins */ + void init(); + + int pinAnalog; //!< encoder hardware pin A + + // Encoder configuration + Pullup pullup; + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + /** raw count (typically in range of 0-1023), useful for debugging resolution issues */ + int raw_count; + + private: + int min_raw_count; + int max_raw_count; + int cpr; + int read(); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.cpp new file mode 100644 index 0000000..ac2b273 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.cpp @@ -0,0 +1,156 @@ +#include "MagneticSensorI2C.h" + +/** Typical configuration for the 12bit AMS AS5600 magnetic sensor over I2C interface */ +MagneticSensorI2CConfig_s AS5600_I2C = { + .chip_address = 0x36, + .bit_resolution = 12, + .angle_register = 0x0C, + .data_start_bit = 11 +}; + +/** Typical configuration for the 12bit AMS AS5048 magnetic sensor over I2C interface */ +MagneticSensorI2CConfig_s AS5048_I2C = { + .chip_address = 0x40, // highly configurable. if A1 and A2 are held low, this is probable value + .bit_resolution = 14, + .angle_register = 0xFE, + .data_start_bit = 15 +}; + + +// MagneticSensorI2C(uint8_t _chip_address, float _cpr, uint8_t _angle_register_msb) +// @param _chip_address I2C chip address +// @param _bit_resolution bit resolution of the sensor +// @param _angle_register_msb angle read register +// @param _bits_used_msb number of used bits in msb +MagneticSensorI2C::MagneticSensorI2C(uint8_t _chip_address, int _bit_resolution, uint8_t _angle_register_msb, int _bits_used_msb){ + // chip I2C address + chip_address = _chip_address; + // angle read register of the magnetic sensor + angle_register_msb = _angle_register_msb; + // register maximum value (counts per revolution) + cpr = _powtwo(_bit_resolution); + + // depending on the sensor architecture there are different combinations of + // LSB and MSB register used bits + // AS5600 uses 0..7 LSB and 8..11 MSB + // AS5048 uses 0..5 LSB and 6..13 MSB + // used bits in LSB + lsb_used = _bit_resolution - _bits_used_msb; + // extraction masks + lsb_mask = (uint8_t)( (2 << lsb_used) - 1 ); + msb_mask = (uint8_t)( (2 << _bits_used_msb) - 1 ); + wire = &Wire; +} + +MagneticSensorI2C::MagneticSensorI2C(MagneticSensorI2CConfig_s config){ + chip_address = config.chip_address; + + // angle read register of the magnetic sensor + angle_register_msb = config.angle_register; + // register maximum value (counts per revolution) + cpr = _powtwo(config.bit_resolution); + + int bits_used_msb = config.data_start_bit - 7; + lsb_used = config.bit_resolution - bits_used_msb; + // extraction masks + lsb_mask = (uint8_t)( (2 << lsb_used) - 1 ); + msb_mask = (uint8_t)( (2 << bits_used_msb) - 1 ); + wire = &Wire; +} + +void MagneticSensorI2C::init(TwoWire* _wire){ + + wire = _wire; + + // I2C communication begin + wire->begin(); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorI2C::getSensorAngle(){ + // (number of full rotations)*2PI + current sensor angle + return ( getRawCount() / (float)cpr) * _2PI ; +} + + + +// function reading the raw counter of the magnetic sensor +int MagneticSensorI2C::getRawCount(){ + return (int)MagneticSensorI2C::read(angle_register_msb); +} + +// I2C functions +/* +* Read a register from the sensor +* Takes the address of the register as a uint8_t +* Returns the value of the register +*/ +int MagneticSensorI2C::read(uint8_t angle_reg_msb) { + // read the angle register first MSB then LSB + byte readArray[2]; + uint16_t readValue = 0; + // notify the device that is aboout to be read + wire->beginTransmission(chip_address); + wire->write(angle_reg_msb); + currWireError = wire->endTransmission(false); + + // read the data msb and lsb + wire->requestFrom(chip_address, (uint8_t)2); + for (byte i=0; i < 2; i++) { + readArray[i] = wire->read(); + } + + // depending on the sensor architecture there are different combinations of + // LSB and MSB register used bits + // AS5600 uses 0..7 LSB and 8..11 MSB + // AS5048 uses 0..5 LSB and 6..13 MSB + readValue = ( readArray[1] & lsb_mask ); + readValue += ( ( readArray[0] & msb_mask ) << lsb_used ); + return readValue; +} + +/* +* Checks whether other devices have locked the bus. Can clear SDA locks. +* This should be called before sensor.init() on devices that suffer i2c slaves locking sda +* e.g some stm32 boards with AS5600 chips +* Takes the sda_pin and scl_pin +* Returns 0 for OK, 1 for other master and 2 for unfixable sda locked LOW +*/ +int MagneticSensorI2C::checkBus(byte sda_pin, byte scl_pin) { + + pinMode(scl_pin, INPUT_PULLUP); + pinMode(sda_pin, INPUT_PULLUP); + delay(250); + + if (digitalRead(scl_pin) == LOW) { + // Someone else has claimed master!"); + return 1; + } + + if(digitalRead(sda_pin) == LOW) { + // slave is communicating and awaiting clocks, we are blocked + pinMode(scl_pin, OUTPUT); + for (byte i = 0; i < 16; i++) { + // toggle clock for 2 bytes of data + digitalWrite(scl_pin, LOW); + delayMicroseconds(20); + digitalWrite(scl_pin, HIGH); + delayMicroseconds(20); + } + pinMode(sda_pin, INPUT); + delayMicroseconds(20); + if (digitalRead(sda_pin) == LOW) { + // SDA still blocked + return 2; + } + _delay(1000); + } + // SDA is clear (HIGH) + pinMode(sda_pin, INPUT); + pinMode(scl_pin, INPUT); + + return 0; +} diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.h new file mode 100644 index 0000000..f8b189f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorI2C.h @@ -0,0 +1,84 @@ +#ifndef MAGNETICSENSORI2C_LIB_H +#define MAGNETICSENSORI2C_LIB_H + +#include "Arduino.h" +#include +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +struct MagneticSensorI2CConfig_s { + int chip_address; + int bit_resolution; + int angle_register; + int data_start_bit; +}; +// some predefined structures +extern MagneticSensorI2CConfig_s AS5600_I2C,AS5048_I2C; + +#if defined(TARGET_RP2040) +#define SDA I2C_SDA +#define SCL I2C_SCL +#endif + + +class MagneticSensorI2C: public Sensor{ + public: + /** + * MagneticSensorI2C class constructor + * @param chip_address I2C chip address + * @param bits number of bits of the sensor resolution + * @param angle_register_msb angle read register msb + * @param _bits_used_msb number of used bits in msb + */ + MagneticSensorI2C(uint8_t _chip_address, int _bit_resolution, uint8_t _angle_register_msb, int _msb_bits_used); + + /** + * MagneticSensorI2C class constructor + * @param config I2C config + */ + MagneticSensorI2C(MagneticSensorI2CConfig_s config); + + static MagneticSensorI2C AS5600(); + + /** sensor initialise pins */ + void init(TwoWire* _wire = &Wire); + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + + /** experimental function to check and fix SDA locked LOW issues */ + int checkBus(byte sda_pin , byte scl_pin ); + + /** current error code from Wire endTransmission() call **/ + uint8_t currWireError = 0; + + private: + float cpr; //!< Maximum range of the magnetic sensor + uint16_t lsb_used; //!< Number of bits used in LSB register + uint8_t lsb_mask; + uint8_t msb_mask; + + // I2C variables + uint8_t angle_register_msb; //!< I2C angle register to read + uint8_t chip_address; //!< I2C chip select pins + + // I2C functions + /** Read one I2C register value */ + int read(uint8_t angle_register_msb); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + /* the two wire instance for this sensor */ + TwoWire* wire; + + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.cpp new file mode 100644 index 0000000..cf21164 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.cpp @@ -0,0 +1,117 @@ +#include "MagneticSensorPWM.h" +#include "Arduino.h" + +/** MagneticSensorPWM(uint8_t _pinPWM, int _min, int _max) + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading + * @param _max_raw_count the largest expected reading + */ +MagneticSensorPWM::MagneticSensorPWM(uint8_t _pinPWM, int _min_raw_count, int _max_raw_count){ + + pinPWM = _pinPWM; + + cpr = _max_raw_count - _min_raw_count + 1; + min_raw_count = _min_raw_count; + max_raw_count = _max_raw_count; + + // define if the sensor uses interrupts + is_interrupt_based = false; + + // define as not set + last_call_us = _micros(); +} + + +/** MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks) + * + * Constructor that computes the min and max raw counts based on the PWM frequency and the number of PWM clocks in one period + * + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param freqHz the frequency of the PWM signal, in Hz, e.g. 115, 230, 460 or 920 for the AS5600, depending on the PWM frequency setting + * @param _total_pwm_clocks the total number of PWM clocks in one period, e.g. 4351 for the AS5600 + * @param _min_pwm_clocks the 0 value returned by the sensor, in PWM clocks, e.g. 128 for the AS5600 + * @param _max_pwm_clocks the largest value returned by the sensor, in PWM clocks, e.g. 4223 for the AS5600 + */ +MagneticSensorPWM::MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks){ + + pinPWM = _pinPWM; + + min_raw_count = lroundf(1000000.0f/freqHz/_total_pwm_clocks*_min_pwm_clocks); + max_raw_count = lroundf(1000000.0f/freqHz/_total_pwm_clocks*_max_pwm_clocks); + cpr = max_raw_count - min_raw_count + 1; + + // define if the sensor uses interrupts + is_interrupt_based = false; + + min_elapsed_time = 1.0f/freqHz; // set the minimum time between two readings + + // define as not set + last_call_us = _micros(); +} + + + +void MagneticSensorPWM::init(){ + + // initial hardware + pinMode(pinPWM, INPUT); + raw_count = getRawCount(); + pulse_timestamp = _micros(); + + this->Sensor::init(); // call base class init +} + +// Sensor update function. Safely copy volatile interrupt variables into Sensor base class state variables. +void MagneticSensorPWM::update() { + if (is_interrupt_based) + noInterrupts(); + Sensor::update(); + angle_prev_ts = pulse_timestamp; // Timestamp of actual sample, before the time-consuming PWM communication + if (is_interrupt_based) + interrupts(); +} + +// get current angle (rad) +float MagneticSensorPWM::getSensorAngle(){ + // raw data from sensor + raw_count = getRawCount(); + if (raw_count > max_raw_count) raw_count = max_raw_count; + if (raw_count < min_raw_count) raw_count = min_raw_count; + return( (float) (raw_count - min_raw_count) / (float)cpr) * _2PI; +} + + +// read the raw counter of the magnetic sensor +int MagneticSensorPWM::getRawCount(){ + if (!is_interrupt_based){ // if it's not interrupt based read the value in a blocking way + pulse_timestamp = _micros(); // ideally this should be done right at the rising edge of the pulse + pulse_length_us = pulseIn(pinPWM, HIGH, 1200); // 1200us timeout, should this be configurable? + } + return pulse_length_us; +} + + +void MagneticSensorPWM::handlePWM() { + // unsigned long now_us = ticks(); + unsigned long now_us = _micros(); + + // if falling edge, calculate the pulse length + if (!digitalRead(pinPWM)) { + pulse_length_us = now_us - last_call_us; + pulse_timestamp = last_call_us; // angle was sampled at the rising edge of the pulse, so use that timestamp + } + + // save the currrent timestamp for the next call + last_call_us = now_us; + is_interrupt_based = true; // set the flag to true +} + +// function enabling hardware interrupts of the for the callback provided +// if callback is not provided then the interrupt is not enabled +void MagneticSensorPWM::enableInterrupt(void (*doPWM)()){ + // declare it's interrupt based + is_interrupt_based = true; + + // enable interrupts on pwm input pin + attachInterrupt(digitalPinToInterrupt(pinPWM), doPWM, CHANGE); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.h new file mode 100644 index 0000000..48492c8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorPWM.h @@ -0,0 +1,73 @@ +#ifndef MAGNETICSENSORPWM_LIB_H +#define MAGNETICSENSORPWM_LIB_H + +#include "Arduino.h" +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +// This sensor has been tested with AS5048a running in PWM mode. + +class MagneticSensorPWM: public Sensor{ + public: + /** MagneticSensorPWM(uint8_t _pinPWM, int _min, int _max) + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param _min_raw_count the smallest expected reading + * @param _max_raw_count the largest expected reading + */ + MagneticSensorPWM(uint8_t _pinPWM,int _min = 0, int _max = 0); + /** MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks) + * + * Constructor that computes the min and max raw counts based on the PWM frequency and the number of PWM clocks in one period + * + * @param _pinPWM the pin that is reading the pwm from magnetic sensor + * @param freqHz the frequency of the PWM signal, in Hz, e.g. 115, 230, 460 or 920 for the AS5600, depending on the PWM frequency setting + * @param _total_pwm_clocks the total number of PWM clocks in one period, e.g. 4351 for the AS5600 + * @param _min_pwm_clocks the 0 value returned by the sensor, in PWM clocks, e.g. 128 for the AS5600 + * @param _max_pwm_clocks the largest value returned by the sensor, in PWM clocks, e.g. 4223 for the AS5600 + */ + MagneticSensorPWM(uint8_t _pinPWM, int freqHz, int _total_pwm_clocks, int _min_pwm_clocks, int _max_pwm_clocks); + + // initialize the sensor hardware + void init(); + + int pinPWM; + + // Interrupt-safe update + void update() override; + + // get current angle (rad) + float getSensorAngle() override; + + // pwm handler + void handlePWM(); + void enableInterrupt(void (*doPWM)()); + unsigned long pulse_length_us; + + private: + // raw count (typically in range of 0-1023) + int raw_count; + int min_raw_count; + int max_raw_count; + int cpr; + + // flag saying if the readings are interrupt based or not + bool is_interrupt_based; + + int read(); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + // time tracking variables + unsigned long last_call_us; + // unsigned long pulse_length_us; + unsigned long pulse_timestamp; + + +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.cpp b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.cpp new file mode 100644 index 0000000..52febc3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.cpp @@ -0,0 +1,161 @@ + +#include "MagneticSensorSPI.h" + +/** Typical configuration for the 14bit AMS AS5147 magnetic sensor over SPI interface */ +MagneticSensorSPIConfig_s AS5147_SPI = { + .spi_mode = SPI_MODE1, + .clock_speed = 1000000, + .bit_resolution = 14, + .angle_register = 0x3FFF, + .data_start_bit = 13, + .command_rw_bit = 14, + .command_parity_bit = 15 +}; +// AS5048 and AS5047 are the same as AS5147 +MagneticSensorSPIConfig_s AS5048_SPI = AS5147_SPI; +MagneticSensorSPIConfig_s AS5047_SPI = AS5147_SPI; + +/** Typical configuration for the 14bit MonolithicPower MA730 magnetic sensor over SPI interface */ +MagneticSensorSPIConfig_s MA730_SPI = { + .spi_mode = SPI_MODE0, + .clock_speed = 1000000, + .bit_resolution = 14, + .angle_register = 0x0000, + .data_start_bit = 15, + .command_rw_bit = 0, // not required + .command_parity_bit = 0 // parity not implemented +}; + + +// MagneticSensorSPI(int cs, float _bit_resolution, int _angle_register) +// cs - SPI chip select pin +// _bit_resolution sensor resolution bit number +// _angle_register - (optional) angle read register - default 0x3FFF +MagneticSensorSPI::MagneticSensorSPI(int cs, int _bit_resolution, int _angle_register){ + + chip_select_pin = cs; + // angle read register of the magnetic sensor + angle_register = _angle_register ? _angle_register : DEF_ANGLE_REGISTER; + // register maximum value (counts per revolution) + cpr = _powtwo(_bit_resolution); + spi_mode = SPI_MODE1; + clock_speed = 1000000; + bit_resolution = _bit_resolution; + + command_parity_bit = 15; // for backwards compatibilty + command_rw_bit = 14; // for backwards compatibilty + data_start_bit = 13; // for backwards compatibilty +} + +MagneticSensorSPI::MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs){ + chip_select_pin = cs; + // angle read register of the magnetic sensor + angle_register = config.angle_register ? config.angle_register : DEF_ANGLE_REGISTER; + // register maximum value (counts per revolution) + cpr = _powtwo(config.bit_resolution); + spi_mode = config.spi_mode; + clock_speed = config.clock_speed; + bit_resolution = config.bit_resolution; + + command_parity_bit = config.command_parity_bit; // for backwards compatibilty + command_rw_bit = config.command_rw_bit; // for backwards compatibilty + data_start_bit = config.data_start_bit; // for backwards compatibilty +} + +void MagneticSensorSPI::init(SPIClass* _spi){ + spi = _spi; + // 1MHz clock (AMS should be able to accept up to 10MHz) + settings = SPISettings(clock_speed, MSBFIRST, spi_mode); + //setup pins + pinMode(chip_select_pin, OUTPUT); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + // do any architectures need to set the clock divider for SPI? Why was this in the code? + //spi->setClockDivider(SPI_CLOCK_DIV8); + digitalWrite(chip_select_pin, HIGH); + + this->Sensor::init(); // call base class init +} + +// Shaft angle calculation +// angle is in radians [rad] +float MagneticSensorSPI::getSensorAngle(){ + return (getRawCount() / (float)cpr) * _2PI; +} + +// function reading the raw counter of the magnetic sensor +int MagneticSensorSPI::getRawCount(){ + return (int)MagneticSensorSPI::read(angle_register); +} + +// SPI functions +/** + * Utility function used to calculate even parity of word + */ +byte MagneticSensorSPI::spiCalcEvenParity(word value){ + byte cnt = 0; + byte i; + + for (i = 0; i < 16; i++) + { + if (value & 0x1) cnt++; + value >>= 1; + } + return cnt & 0x1; +} + + /* + * Read a register from the sensor + * Takes the address of the register as a 16 bit word + * Returns the value of the register + */ +word MagneticSensorSPI::read(word angle_register){ + + word command = angle_register; + + if (command_rw_bit > 0) { + command = angle_register | (1 << command_rw_bit); + } + if (command_parity_bit > 0) { + //Add a parity bit on the the MSB + command |= ((word)spiCalcEvenParity(command) << command_parity_bit); + } + + //SPI - begin transaction + spi->beginTransaction(settings); + + //Send the command + digitalWrite(chip_select_pin, LOW); + spi->transfer16(command); + digitalWrite(chip_select_pin,HIGH); + +#if defined(ESP_H) && defined(ARDUINO_ARCH_ESP32) // if ESP32 board + delayMicroseconds(50); // why do we need to delay 50us on ESP32? In my experience no extra delays are needed, on any of the architectures I've tested... +#else + delayMicroseconds(1); // delay 1us, the minimum time possible in plain arduino. 350ns is the required time for AMS sensors, 80ns for MA730, MA702 +#endif + + //Now read the response + digitalWrite(chip_select_pin, LOW); + word register_value = spi->transfer16(0x00); + digitalWrite(chip_select_pin, HIGH); + + //SPI - end transaction + spi->endTransaction(); + + register_value = register_value >> (1 + data_start_bit - bit_resolution); //this should shift data to the rightmost bits of the word + + const static word data_mask = 0xFFFF >> (16 - bit_resolution); + + return register_value & data_mask; // Return the data, stripping the non data (e.g parity) bits +} + +/** + * Closes the SPI connection + * SPI has an internal SPI-device counter, for each init()-call the close() function must be called exactly 1 time + */ +void MagneticSensorSPI::close(){ + spi->end(); +} + + diff --git a/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.h b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.h new file mode 100644 index 0000000..03ebde4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/Simple FOC/src/sensors/MagneticSensorSPI.h @@ -0,0 +1,84 @@ +#ifndef MAGNETICSENSORSPI_LIB_H +#define MAGNETICSENSORSPI_LIB_H + + +#include "Arduino.h" +#include +#include "../common/base_classes/Sensor.h" +#include "../common/foc_utils.h" +#include "../common/time_utils.h" + +#define DEF_ANGLE_REGISTER 0x3FFF + +struct MagneticSensorSPIConfig_s { + int spi_mode; + long clock_speed; + int bit_resolution; + int angle_register; + int data_start_bit; + int command_rw_bit; + int command_parity_bit; +}; +// typical configuration structures +extern MagneticSensorSPIConfig_s AS5147_SPI,AS5048_SPI,AS5047_SPI, MA730_SPI; + +class MagneticSensorSPI: public Sensor{ + public: + /** + * MagneticSensorSPI class constructor + * @param cs SPI chip select pin + * @param bit_resolution sensor resolution bit number + * @param angle_register (optional) angle read register - default 0x3FFF + */ + MagneticSensorSPI(int cs, int bit_resolution, int angle_register = 0); + /** + * MagneticSensorSPI class constructor + * @param config SPI config + * @param cs SPI chip select pin + */ + MagneticSensorSPI(MagneticSensorSPIConfig_s config, int cs); + + /** sensor initialise pins */ + void init(SPIClass* _spi = &SPI); + + // implementation of abstract functions of the Sensor class + /** get current angle (rad) */ + float getSensorAngle() override; + + // returns the spi mode (phase/polarity of read/writes) i.e one of SPI_MODE0 | SPI_MODE1 | SPI_MODE2 | SPI_MODE3 + int spi_mode; + + /* returns the speed of the SPI clock signal */ + long clock_speed; + + + private: + float cpr; //!< Maximum range of the magnetic sensor + // spi variables + int angle_register; //!< SPI angle register to read + int chip_select_pin; //!< SPI chip select pin + SPISettings settings; //!< SPI settings variable + // spi functions + /** Stop SPI communication */ + void close(); + /** Read one SPI register value */ + word read(word angle_register); + /** Calculate parity value */ + byte spiCalcEvenParity(word value); + + /** + * Function getting current angle register value + * it uses angle_register variable + */ + int getRawCount(); + + int bit_resolution; //!< the number of bites of angle data + int command_parity_bit; //!< the bit where parity flag is stored in command + int command_rw_bit; //!< the bit where read/write flag is stored in command + int data_start_bit; //!< the the position of first bit + + SPIClass* spi; +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.github/workflows/ccpp.yml b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.github/workflows/ccpp.yml new file mode 100644 index 0000000..bd74689 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.github/workflows/ccpp.yml @@ -0,0 +1,76 @@ +name: Library Compile +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update + project-type: library + build: + name: Test compile + runs-on: ubuntu-latest + strategy: + matrix: + arduino-boards-fqbn: + - arduino:avr:uno # arudino uno + - arduino:sam:arduino_due_x # arduino due + - arduino:samd:nano_33_iot # samd21 + - adafruit:samd:adafruit_metro_m4 # samd51 + - esp32:esp32:esp32 # esp32 + - esp32:esp32:esp32s2 # esp32s2 + - STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 # stm32 bluepill + - STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE # stm32 nucleo + - arduino:mbed_rp2040:pico # rpi pico + include: + - arduino-boards-fqbn: arduino:avr:uno + sketches-exclude: calibrated mt6816_spi smoothing + required-libraries: Simple FOC + - arduino-boards-fqbn: arduino:sam:arduino_due_x + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: arduino:samd:nano_33_iot + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: arduino:mbed_rp2040:pico + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: adafruit:samd:adafruit_metro_m4 + platform-url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + # - arduino-boards-fqbn: esp32:esp32:esp32doit-devkit-v1 + # platform-url: https://dl.espressif.com/dl/package_esp32_index.json + # required-libraries: Simple FOC + # sketch-names: '**.ino' + - arduino-boards-fqbn: esp32:esp32:esp32 # esp32 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: esp32:esp32:esp32s2 # esp32s2 + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated smoothing + - arduino-boards-fqbn: STMicroelectronics:stm32:GenF1:pnum=BLUEPILL_F103C8 + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + required-libraries: Simple FOC + sketches-exclude: calibrated mt6816_spi smoothing + - arduino-boards-fqbn: STMicroelectronics:stm32:Nucleo_64:pnum=NUCLEO_F411RE + platform-url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json + required-libraries: Simple FOC + sketches-exclude: smoothing + # Do not cancel all jobs / architectures if one job fails + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@master + - name: Compile all examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} + required-libraries: ${{ matrix.required-libraries }} + platform-url: ${{ matrix.platform-url }} + sketch-names: ${{ matrix.sketch-names }} + sketches-exclude: ${{ matrix.sketches-exclude }} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.gitignore b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.gitignore new file mode 100644 index 0000000..ce7f6d0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.gitignore @@ -0,0 +1,2 @@ +.project +.DS_Store diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.piopm b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.piopm new file mode 100644 index 0000000..284f86d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "SimpleFOCDrivers", "version": "1.0.5", "spec": {"owner": "simplefoc", "id": 13496, "name": "SimpleFOCDrivers", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/LICENSE b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/LICENSE new file mode 100644 index 0000000..53dcc14 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Richard Unger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/README.md new file mode 100644 index 0000000..e5f571d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/README.md @@ -0,0 +1,112 @@ +# SimpleFOC Driver and Support Library + +![Library Compile](https://github.com/simplefoc/Arduino-FOC-drivers/workflows/Library%20Compile/badge.svg) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +![Release](https://www.ardu-badge.com/badge/SimpleFOCDrivers.svg?) + +This library contains an assortment of drivers and supporting code for SimpleFOC. + +The intent is to keep the core of SimpleFOC clean, and thus easy to maintain, understand and port to different platforms. In addition to this core, there are various drivers and supporting code which has grown around SimpleFOC, and which we would like to make available to the community. + +## New Release + +v1.0.5 - Released July 2023, for Simple FOC 2.3.1 + +What's changed since 1.0.4? +- Added smoothing sensor by [@dekutree64](https://github.com/dekutree64) +- Added TMD6200 SPI driver by [@YaseenTwati](https://github.com/YaseenTwati) +- Added HybridStepperMotor by [@VIPQualityPost](https://github.com/VIPQualityPost) +- New Settings abstraction to load and save SimpleFOC settings and calibration +- New Settings driver: SAMDNVMSettingsStorage +- SimpleFOCRegisters abstraction, mapping SimpleFOC parameters to virtual "Registers" +- Updated I2CCommander to use the new registers abstraction +- Bugfixes [included](https://github.com/simplefoc/Arduino-FOC-drivers/issues?q=is%3Aissue+milestone%3A1.0.5+) + +## What is included + +What is here? See the sections below. Each driver or function should come with its own more detailed README. + +### Motor/Gate driver ICs + + - [TMC6200 driver](src/drivers/tmc6200/) - SPI driver for Trinamics TMC6200 motor driver IC. + - [DRV8316 driver](src/drivers/drv8316/) - SPI driver for TI's DRV8316 motor driver IC. + +### Encoders + + - [AS5048A SPI driver](src/encoders/as5048a/) - SPI driver for the AMS AS5048A absolute position magnetic rotary encoder IC. + - [AS5047 SPI driver](src/encoders/as5047/) - SPI driver for the AMS AS5047P and AS5047D absolute position magnetic rotary encoder ICs. + - [AS5047U SPI driver](src/encoders/as5047u/) - SPI driver for the AMS AS5047U absolute position magnetic rotary encoder ICs. + - [MA730 SPI driver](src/encoders/ma730/) - SPI driver for the MPS MagAlpha MA730 absolute position magnetic rotary encoder IC. + - [MA730 SSI driver](src/encoders/ma730/) - SSI driver for the MPS MagAlpha MA730 absolute position magnetic rotary encoder IC. + - [AS5145 SSI driver](src/encoders/as5145/) - SSI driver for the AMS AS5145 and AS5045 absolute position magnetic rotary encoder ICs. + - [TLE5012B SPI driver](src/encoders/tle5012b/) - SPI (half duplex) driver for TLE5012B absolute position magnetic rotary encoder IC. + - [STM32 Hardware Encoder](src/encoders/stm32hwencoder/) - Hardware timer based encoder driver for ABI type quadrature encoders. + - [SC60228 SPI driver](src/encoders/sc60228/) - SPI driver for SemiMent SC60288 magnetic encoder IC. + - [MA330 SPI driver](src/encoders/ma330/) - SPI driver for the MPS MagAlpha MA330 absolute position magnetic rotary encoder IC. + - [MT6816 SPI driver](src/encoders/mt6816/) - SPI driver for the MagnTek MT6816 absolute position magnetic rotary encoder IC. + - [MT6701 SSI driver](src/encoders/mt6701/) - SSI driver for the MagnTek MT6701 absolute position magnetic rotary encoder IC. + - [MT6835 SPI driver](src/encoders/mt6835/) - SPI driver for the MagnTek MT6835 21 bit magnetic rotary encoder IC. + - [STM32 PWM sensor driver](src/encoders/stm32pwmsensor/) - STM32 native timer-based driver for PWM angle sensors. + - [SmoothingSensor](src/encoders/smoothing/) - A SimpleFOC Sensor wrapper implementation which adds angle extrapolation. + +### Communications + + - [I2CCommander I2C driver](src/comms/i2c/) - I2C communications protocol and drivers for both controller and target devices, based on register abstraction + - [STM32 SpeedDir Input](src/comms/stm32speeddir/) - Control target velocity with PWM speed and direction inputs + - [SerialBinaryCommander](src/comms/serial/) - Serial communications with binary protocol, based on register abstraction + - [Telemetry](src/comms/telemetry/) - Telemetry abstraction, based on registers + - [SerialASCIITelemetry](src/comms/serial/) - Serial communications with ascii protocol, based on register abstraction + +### Settings + +Load and store SimpleFOC motor settings, based on register abstraction. + + - [SAMD NVM storage driver](src/settings/samd/) - Store settings to the NVM flash memory in your SAMD MCU + - [CAT24 I2C EEPROM storage driver](src/settings/i2c/) - Store settings to CAT24 I2C EEPROMs + +### Utilities + + - [STM32 PWM Input driver](src/utilities/stm32pwm/) - PWM Input driver for STM32 MCUs. Accurately measure PWM inputs with zero MCU overhead. + + +## How to use + +#### Arduino Library Manager +The simplest way to get hold of the library is directly by using Arduino IDE and its integrated Library Manager. +- Open Arduino IDE and start Arduino Library Manager by clicking: `Tools > Manage Libraries...`. +- Search for `Simple FOC drivers` library and install the latest version. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC drivers`. + +#### Using Github website +- Go to the [github repository](https://github.com/simplefoc/Arduino-FOC-drivers) +- Click first on `Clone or Download > Download ZIP`. +- Unzip it and place it in `Arduino Libraries` folder. Windows: `Documents > Arduino > libraries`. +- Reopen Arduino IDE and you should have the library examples in `File > Examples > Simple FOC drivers`. + +#### Using parts + +You can copy parts of the library, for example to minimize your code size, or make it easier to add adaptations of your own. +If you do so, please be sure to adhere to and include the [LICENSE](https://github.com/simplefoc/Arduino-FOC-drivers/LICENSE). + + +## Further Documentation + +Find out more information about the Arduino SimpleFOC project on the [docs website](https://docs.simplefoc.com/) + +## Release History + +What's changed since 1.0.3? +- New Comms/Input: STM32SpeedDirCommander +- New Utility: STM32PWMInput +- Fixed MT6835 driver bugs +- Improved AS5047 driver, fixed bugs +- Improved AS5047U driver, fixed bugs + +What's changed since 1.0.2? +- New Sensor: MT6835 +- Fixed bugs + +What's changed since 1.0.1? +- Calibrated sensor by @MarethyuPrefect +- New Sensors: MT6701, MA330, MT6816 +- Fixed bugs diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino new file mode 100644 index 0000000..be89c74 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_3pwm/drv8316_3pwm.ino @@ -0,0 +1,123 @@ + + + +#include "Arduino.h" +#include +#include +#include +#include "drivers/drv8316/drv8316.h" + + + + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver3PWM driver = DRV8316Driver3PWM(2,3,4,7,false); // use the right pins for your setup! +#define ENABLE_A 0 +#define ENABLE_B 1 +#define ENABLE_C 6 + + +void printDRV8316Status() { + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +} + + + + + +void setup() { + + Serial.begin(115200); + while (!Serial); + delay(1); + Serial.println("Initializing..."); + + pinMode(ENABLE_A, OUTPUT); + digitalWrite(ENABLE_A, 1); // enable + pinMode(ENABLE_B, OUTPUT); + digitalWrite(ENABLE_B, 1); // enable + pinMode(ENABLE_C, OUTPUT); + digitalWrite(ENABLE_C, 1); // enable + + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity_openloop; + motor.voltage_limit = 3; + motor.velocity_limit = 20; + motor.init(); + Serial.println("Init complete..."); + + delay(100); + printDRV8316Status(); +} + + +// velocity set point variable +float target_velocity = 7.0; + + +void loop() { + //delay(100); + //driver.setPwm(7.4/4, 7.4/2, 7.4/4*3); + motor.move(target_velocity); +} + + + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino new file mode 100644 index 0000000..2eb0dd3 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/drivers/drv8316/drv8316_6pwm/drv8316_6pwm.ino @@ -0,0 +1,111 @@ + + + +#include "Arduino.h" +#include +#include +#include +#include "drivers/drv8316/drv8316.h" + + + + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver6PWM driver = DRV8316Driver6PWM(0,1,2,3,4,6,7,false); // use the right pins for your setup! + + + +void printDRV8316Status() { + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +} + + + + + +void setup() { + Serial.begin(115200); + while (!Serial); + delay(1); + Serial.println("Initializing..."); + + driver.voltage_power_supply = 12; + driver.init(); + motor.linkDriver(&driver); + motor.controller = MotionControlType::velocity_openloop; + motor.voltage_limit = 3; + motor.velocity_limit = 20; + motor.init(); + Serial.println("Init complete..."); + + delay(100); + printDRV8316Status(); +} + + +// velocity set point variable +float target_velocity = 7.0; + + +void loop() { + motor.move(target_velocity); +} + + + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino new file mode 100644 index 0000000..6cc6110 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/calibrated/calibrated.ino @@ -0,0 +1,86 @@ +/** + * Torque control example using voltage control loop. + * + * Most of the low-end BLDC driver boards doesn't have current measurement therefore SimpleFOC offers + * you a way to control motor torque by setting the voltage to the motor instead hte current. + * + * This makes the BLDC motor effectively a DC motor, and you can use it in a same way. + */ +#include +#include +#include "encoders/calibrated/CalibratedSensor.h" + +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5048_SPI, PB6); +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// instantiate the calibrated sensor object +CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); + +// voltage set point variable +float target_voltage = 2; + +// instantiate the commander +Commander command = Commander(Serial); + +void doTarget(char* cmd) { command.scalar(&target_voltage, cmd); } + +void setup() { + + SPI.setMISO(PB14); + SPI.setMOSI(PB15); + SPI.setSCLK(PB13); + + sensor.init(); + // Link motor to sensor + motor.linkSensor(&sensor); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + + // set voltage to run calibration + sensor_calibrated.voltage_calibration = 6; + // Running calibration + sensor_calibrated.calibrate(motor); + + //Serial.println("Calibrating Sensor Done."); + // Linking sensor to motor object + motor.linkSensor(&sensor_calibrated); + + // calibrated init FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + Serial.println(F("Motor ready.")); + + Serial.println(F("Set the target voltage using serial terminal:")); + _delay(1000); +} + +void loop() { + + motor.loopFOC(); + motor.move(target_voltage); + command.run(); + motor.monitor(); + +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino new file mode 100644 index 0000000..c1ac1cd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/mt6816/mt6816_spi/mt6816_spi.ino @@ -0,0 +1,106 @@ +/** + * Comprehensive BLDC motor control example using magnetic sensor MT6816 + * + * Using serial terminal user can send motor commands and configure the motor and FOC in real-time: + * - configure PID controller constants + * - change motion control loops + * - monitor motor variabels + * - set target values + * - check all the configuration values + * + * See more info in docs.simplefoc.com/commander_interface + */ +#include +#include +#include + +// magnetic sensor instance - MT6816 SPI mode +MagneticSensorMT6816 sensor = MagneticSensorMT6816(5); + + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(7); +BLDCDriver3PWM driver = BLDCDriver3PWM(32, 25, 26, 33); + +// Inline Current Sense instance +InlineCurrentSense current_sense = InlineCurrentSense(0.01f, 50.0f, 35, 34); + +// commander interface +Commander command = Commander(Serial); +void onMotor(char* cmd){ command.motor(&motor, cmd); } + +void setup() { + + // initialise magnetic sensor hardware + sensor.init(); + // link the motor to the sensor + motor.linkSensor(&sensor); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link driver + motor.linkDriver(&driver); + + // choose FOC modulation + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; + + // set control loop type to be used + motor.controller = MotionControlType::torque; + + // contoller configuration based on the control type + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 20; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 12; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // angle loop velocity limit + motor.velocity_limit = 50; + + // use monitoring with serial for motor init + // monitoring port + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + current_sense.linkDriver(&driver); + current_sense.init(); + current_sense.gain_b *= -1; + current_sense.skip_align = true; + motor.linkCurrentSense(¤t_sense); + + // initialise motor + motor.init(); + // align encoder and start FOC + motor.initFOC(); + + // set the inital target value + motor.target = 2; + + // define the motor id + command.add('A', onMotor, "motor"); + + // Run user commands to configure and the motor (find the full command list in docs.simplefoc.com) + Serial.println(F("Motor commands sketch | Initial motion control > torque/voltage : target 2V.")); + + _delay(1000); +} + + +void loop() { + // iterative setting FOC phase voltage + motor.loopFOC(); + + // iterative function setting the outter loop target + // velocity, position or voltage + // if tatget not set in parameter uses motor.target variable + motor.move(); + + // user communication + command.run(); +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino new file mode 100644 index 0000000..1ff658d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/examples/encoders/smoothing/smoothing.ino @@ -0,0 +1,141 @@ +/** + * + * Hall sensor velocity motion control example, modified to demonstrate usage of SmoothingSensor + * Steps: + * 1) Configure the motor and sensor + * 2) Run the code + * 3) Set the target velocity (in radians per second) from serial terminal + * 4) Try with and without smoothing to see the difference (send E1 and E0 commands from serial terminal) + * + * + * + * NOTE : + * > Specifically for Arduino UNO example code for running velocity motion control using a hall sensor + * > Since Arduino UNO doesn't have enough interrupt pins we have to use software interrupt library PciManager. + * + * > If running this code with Nucleo or Bluepill or any other board which has more than 2 interrupt pins + * > you can supply doC directly to the sensor.enableInterrupts(doA,doB,doC) and avoid using PciManger + * + */ +#include +// software interrupt library +#include +#include +#include +#include + +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(9, 5, 6, 8); +// Stepper motor & driver instance +//StepperMotor motor = StepperMotor(50); +//StepperDriver4PWM driver = StepperDriver4PWM(9, 5, 10, 6, 8); + +// hall sensor instance +HallSensor sensor = HallSensor(2, 3, 4, 11); +// wrapper instance +SmoothingSensor smooth(sensor, motor); + +// Interrupt routine intialisation +// channel A and B callbacks +void doA(){sensor.handleA();} +void doB(){sensor.handleB();} +void doC(){sensor.handleC();} +// If no available hadware interrupt pins use the software interrupt +PciListenerImp listenerIndex(sensor.pinC, doC); + +// velocity set point variable +float target_velocity = 0; +// instantiate the commander +Commander command = Commander(Serial); +void doTarget(char* cmd) { command.scalar(&target_velocity, cmd); } + +void enableSmoothing(char* cmd) { + float enable; + command.scalar(&enable, cmd); + motor.linkSensor(enable == 0 ? (Sensor*)&sensor : (Sensor*)&smooth); +} + +void setup() { + + // initialize sensor sensor hardware + sensor.init(); + sensor.enableInterrupts(doA, doB); //, doC); + // software interrupts + PciManager.registerListener(&listenerIndex); + // set SmoothingSensor phase correction for hall sensors + smooth.phase_correction = -_PI_6; + // link the SmoothingSensor to the motor + motor.linkSensor(&smooth); + + // driver config + // power supply voltage [V] + driver.voltage_power_supply = 12; + driver.init(); + // link the motor and the driver + motor.linkDriver(&driver); + + // aligning voltage [V] + motor.voltage_sensor_align = 3; + + // set motion control loop to be used + motor.controller = MotionControlType::velocity; + + // contoller configuration + // default parameters in defaults.h + + // velocity PI controller parameters + motor.PID_velocity.P = 0.2f; + motor.PID_velocity.I = 2; + motor.PID_velocity.D = 0; + // default voltage_power_supply + motor.voltage_limit = 6; + // jerk control using voltage voltage ramp + // default value is 300 volts per sec ~ 0.3V per millisecond + motor.PID_velocity.output_ramp = 1000; + + // velocity low pass filtering time constant + motor.LPF_velocity.Tf = 0.01f; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + + // initialize motor + motor.init(); + // align sensor and start FOC + motor.initFOC(); + + // add target command T + command.add('T', doTarget, "target voltage"); + + // add smoothing enable/disable command E (send E0 to use hall sensor alone, or E1 to use smoothing) + command.add('E', enableSmoothing, "enable smoothing"); + + Serial.println(F("Motor ready.")); + Serial.println(F("Set the target velocity using serial terminal:")); + _delay(1000); +} + + +void loop() { + // main FOC algorithm function + // the faster you run this function the better + // Arduino UNO loop ~1kHz + // Bluepill loop ~10kHz + motor.loopFOC(); + + // Motion control function + // velocity, position or voltage (defined in motor.controller) + // this function can be run at much lower frequency than loopFOC() function + // You can also use motor.move() and set the motor.target in the code + motor.move(target_velocity); + + // function intended to be used with serial plotter to monitor motor variables + // significantly slowing the execution down!!!! + // motor.monitor(); + + // user communication + command.run(); +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/keywords.txt b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/keywords.txt new file mode 100644 index 0000000..516cc85 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/keywords.txt @@ -0,0 +1,11 @@ +SimpleFOC KEYWORD1 +DRV8316 KEYWORD1 +AS5048A KEYWORD1 +AS5047 KEYWORD1 +AS5145 KEYWORD1 +MA730 KEYWORD1 +MT6835 KEYWORD1 +SC60228 KEYWORD1 +TLE5012B KEYWORD1 +I2CCommander KEYWORD1 +STM32HWEncoder KEYWORD1 diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/library.properties b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/library.properties new file mode 100644 index 0000000..d3565e4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/library.properties @@ -0,0 +1,11 @@ +name=SimpleFOCDrivers +version=1.0.5 +author=Simplefoc +maintainer=Simplefoc +sentence=A library of supporting drivers for SimpleFOC. Motor drivers chips, encoder chips, current sensing and supporting code. +paragraph=SimpleFOC runs BLDC and Stepper motors using the FOC algorithm. This library supports the core SimpleFOC code by adding support for specific hardware like motor driver ICs, encoders, current sensing and other supporting code. +category=Device Control +url=https://docs.simplefoc.com +architectures=* +includes=SimpleFOCDrivers.h +depends=Simple FOC \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/SimpleFOCDrivers.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/SimpleFOCDrivers.h new file mode 100644 index 0000000..689ea30 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/SimpleFOCDrivers.h @@ -0,0 +1,11 @@ + +#ifndef __SIMPLEFOC_DRIVERS_H__ +#define __SIMPLEFOC_DRIVERS_H__ + +// empty header file. In Arduino IDE, include this header to enable the +// IDE to "find" the library. +// Then include the headers for the individual drivers/sensors/utilities +// that you want to use. + +#endif + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/README.md new file mode 100644 index 0000000..4469af6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/README.md @@ -0,0 +1,5 @@ + +# SimpleFOC communications support code + +This folder contains classes to support you communicating between MCUs running SimpleFOC, and other systems. + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp new file mode 100644 index 0000000..44f7c54 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.cpp @@ -0,0 +1,131 @@ + +#include "./RegisterReceiver.h" +#include "BLDCMotor.h" + + +void RegisterReceiver::setRegister(SimpleFOCRegister reg, FOCMotor* motor){ + // write a register value for the given motor + switch(reg) { + case REG_ENABLE: + readByte((uint8_t*)&(motor->enabled)); + break; + case REG_MODULATION_MODE: + readByte((uint8_t*)&(motor->foc_modulation)); + break; + case REG_TORQUE_MODE: + readByte((uint8_t*)&(motor->torque_controller)); + break; + case REG_CONTROL_MODE: + readByte((uint8_t*)&(motor->controller)); + break; + case REG_TARGET: + readFloat(&(motor->target)); + break; + case REG_VEL_PID_P: + readFloat(&(motor->PID_velocity.P)); + break; + case REG_VEL_PID_I: + readFloat(&(motor->PID_velocity.I)); + break; + case REG_VEL_PID_D: + readFloat(&(motor->PID_velocity.D)); + break; + case REG_VEL_LPF_T: + readFloat(&(motor->LPF_velocity.Tf)); + break; + case REG_ANG_PID_P: + readFloat(&(motor->P_angle.P)); + break; + case REG_VEL_LIMIT: + readFloat(&(motor->velocity_limit)); + break; + case REG_VEL_MAX_RAMP: + readFloat(&(motor->PID_velocity.output_ramp)); + break; + + case REG_CURQ_PID_P: + readFloat(&(motor->PID_current_q.P)); + break; + case REG_CURQ_PID_I: + readFloat(&(motor->PID_current_q.I)); + break; + case REG_CURQ_PID_D: + readFloat(&(motor->PID_current_q.D)); + break; + case REG_CURQ_LPF_T: + readFloat(&(motor->LPF_current_q.Tf)); + break; + case REG_CURD_PID_P: + readFloat(&(motor->PID_current_d.P)); + break; + case REG_CURD_PID_I: + readFloat(&(motor->PID_current_d.I)); + break; + case REG_CURD_PID_D: + readFloat(&(motor->PID_current_d.D)); + break; + case REG_CURD_LPF_T: + readFloat(&(motor->LPF_current_d.Tf)); + break; + + case REG_VOLTAGE_LIMIT: + readFloat(&(motor->voltage_limit)); + break; + case REG_CURRENT_LIMIT: + readFloat(&(motor->current_limit)); + break; + case REG_MOTION_DOWNSAMPLE: + readByte((uint8_t*)&(motor->motion_downsample)); + break; + case REG_DRIVER_VOLTAGE_LIMIT: + readFloat(&(((BLDCMotor*)motor)->driver->voltage_limit)); + break; + case REG_PWM_FREQUENCY: + readInt((uint32_t*)&(((BLDCMotor*)motor)->driver->pwm_frequency)); + break; + + case REG_ZERO_ELECTRIC_ANGLE: + readFloat(&(motor->zero_electric_angle)); + break; + case REG_SENSOR_DIRECTION: + readByte((uint8_t*)&(motor->sensor_direction)); + break; + case REG_ZERO_OFFSET: + readFloat(&(motor->sensor_offset)); + break; + case REG_PHASE_RESISTANCE: + readFloat(&(motor->phase_resistance)); + break; + case REG_KV: + readFloat(&(motor->KV_rating)); + break; + case REG_INDUCTANCE: + readFloat(&(motor->phase_inductance)); + break; + case REG_POLE_PAIRS: + readByte((uint8_t*)&(motor->pole_pairs)); + break; + // unknown register or read-only register (no write) or can't handle in superclass + case REG_STATUS: + case REG_ANGLE: + case REG_POSITION: + case REG_VELOCITY: + case REG_SENSOR_ANGLE: + case REG_VOLTAGE_Q: + case REG_VOLTAGE_D: + case REG_CURRENT_Q: + case REG_CURRENT_D: + case REG_CURRENT_A: + case REG_CURRENT_B: + case REG_CURRENT_C: + case REG_CURRENT_ABC: + case REG_SYS_TIME: + case REG_NUM_MOTORS: + case REG_MOTOR_ADDRESS: + case REG_ENABLE_ALL: + case REG_REPORT: + default: + break; + } +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.h new file mode 100644 index 0000000..fc2af1c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterReceiver.h @@ -0,0 +1,15 @@ + +#pragma once + +#include "./SimpleFOCRegisters.h" +#include "common/base_classes/FOCMotor.h" + + + +class RegisterReceiver { +protected: + virtual void setRegister(SimpleFOCRegister reg, FOCMotor* motor); + virtual uint8_t readByte(uint8_t* valueToSet) = 0; + virtual uint8_t readFloat(float* valueToSet) = 0; + virtual uint8_t readInt(uint32_t* valueToSet) = 0; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.cpp new file mode 100644 index 0000000..2a163ce --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.cpp @@ -0,0 +1,193 @@ + +#include "./RegisterSender.h" +#include "common/foc_utils.h" +#include "BLDCMotor.h" + +bool RegisterSender::sendRegister(SimpleFOCRegister reg, FOCMotor* motor){ + // write a register value for the given motor + switch(reg) { + case REG_STATUS: + writeByte(motor->motor_status); + break; + case REG_ENABLE: + writeByte(motor->enabled); + break; + case REG_MODULATION_MODE: + writeByte(motor->foc_modulation); + break; + case REG_TORQUE_MODE: + writeByte(motor->torque_controller); + break; + case REG_CONTROL_MODE: + writeByte(motor->controller); + break; + case REG_TARGET: + writeFloat(motor->target); + break; + case REG_ANGLE: + writeFloat(motor->shaft_angle); + break; + case REG_POSITION: + if (motor->sensor) { + writeInt(motor->sensor->getFullRotations()); + writeFloat(motor->sensor->getMechanicalAngle()); + } + else { + writeInt(motor->shaft_angle/_2PI); + writeFloat(fmod(motor->shaft_angle, _2PI)); + } + break; + case REG_VELOCITY: + writeFloat(motor->shaft_velocity); + break; + case REG_SENSOR_ANGLE: + if (motor->sensor) + writeFloat(motor->sensor->getAngle()); // stored angle + else + writeFloat(motor->shaft_angle); + break; + + case REG_VOLTAGE_Q: + writeFloat(motor->voltage.q); + break; + case REG_VOLTAGE_D: + writeFloat(motor->voltage.d); + break; + case REG_CURRENT_Q: + writeFloat(motor->current.q); + break; + case REG_CURRENT_D: + writeFloat(motor->current.d); + break; + case REG_CURRENT_A: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().a); + else + writeFloat(0.0f); + break; + case REG_CURRENT_B: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().b); + else + writeFloat(0.0f); + break; + case REG_CURRENT_C: + if (motor->current_sense) + writeFloat(motor->current_sense->getPhaseCurrents().c); + else + writeFloat(0.0f); + break; + case REG_CURRENT_ABC: + if (motor->current_sense) { + PhaseCurrent_s currents = motor->current_sense->getPhaseCurrents(); + writeFloat(currents.a); + writeFloat(currents.b); + writeFloat(currents.c); + } + else { + writeFloat(0.0f); + writeFloat(0.0f); + writeFloat(0.0f); + } + break; + case REG_VEL_PID_P: + writeFloat(motor->PID_velocity.P); + break; + case REG_VEL_PID_I: + writeFloat(motor->PID_velocity.I); + break; + case REG_VEL_PID_D: + writeFloat(motor->PID_velocity.D); + break; + case REG_VEL_LPF_T: + writeFloat(motor->LPF_velocity.Tf); + break; + case REG_ANG_PID_P: + writeFloat(motor->P_angle.P); + break; + case REG_VEL_LIMIT: + writeFloat(motor->velocity_limit); + break; + case REG_VEL_MAX_RAMP: + writeFloat(motor->PID_velocity.output_ramp); + break; + + case REG_CURQ_PID_P: + writeFloat(motor->PID_current_q.P); + break; + case REG_CURQ_PID_I: + writeFloat(motor->PID_current_q.I); + break; + case REG_CURQ_PID_D: + writeFloat(motor->PID_current_q.D); + break; + case REG_CURQ_LPF_T: + writeFloat(motor->LPF_current_q.Tf); + break; + case REG_CURD_PID_P: + writeFloat(motor->PID_current_d.P); + break; + case REG_CURD_PID_I: + writeFloat(motor->PID_current_d.I); + break; + case REG_CURD_PID_D: + writeFloat(motor->PID_current_d.D); + break; + case REG_CURD_LPF_T: + writeFloat(motor->LPF_current_d.Tf); + break; + + case REG_VOLTAGE_LIMIT: + writeFloat(motor->voltage_limit); + break; + case REG_CURRENT_LIMIT: + writeFloat(motor->current_limit); + break; + case REG_MOTION_DOWNSAMPLE: + writeByte((uint8_t)motor->motion_downsample); + break; + case REG_DRIVER_VOLTAGE_LIMIT: + writeFloat(((BLDCMotor*)motor)->driver->voltage_limit); + break; + case REG_PWM_FREQUENCY: + writeInt(((BLDCMotor*)motor)->driver->pwm_frequency); + break; + + case REG_ZERO_ELECTRIC_ANGLE: + writeFloat(motor->zero_electric_angle); + break; + case REG_SENSOR_DIRECTION: + writeByte((int8_t)motor->sensor_direction); + break; + case REG_ZERO_OFFSET: + writeFloat(motor->sensor_offset); + break; + case REG_PHASE_RESISTANCE: + writeFloat(motor->phase_resistance); + break; + case REG_KV: + writeFloat(motor->KV_rating); + break; + case REG_INDUCTANCE: + writeFloat(motor->phase_inductance); + break; + case REG_POLE_PAIRS: + writeByte((uint8_t)motor->pole_pairs); + break; + + case REG_SYS_TIME: + // TODO how big is millis()? Same on all platforms? + writeInt((int)millis()); + break; + // unknown register or write only register (no read) or can't handle in superclass + case REG_NUM_MOTORS: + case REG_REPORT: + case REG_MOTOR_ADDRESS: + case REG_ENABLE_ALL: + default: + writeByte(0); // TODO what to send in this case? + return false; + } + return true; +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.h new file mode 100644 index 0000000..b2486a1 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/RegisterSender.h @@ -0,0 +1,18 @@ + +#pragma once + +#include "./SimpleFOCRegisters.h" +#include "common/base_classes/FOCMotor.h" + +/** + * Register sending functionality is shared by Commander and Telemetry implementations. + * Since the code to access all the registers is quite large, it makes sense to abstract it out, + * and also make sure registers are addressed in the same way for all implementations. + */ +class RegisterSender { +protected: + virtual bool sendRegister(SimpleFOCRegister reg, FOCMotor* motor); + virtual uint8_t writeByte(uint8_t value) = 0; + virtual uint8_t writeFloat(float value) = 0; + virtual uint8_t writeInt(uint32_t value) = 0; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h new file mode 100644 index 0000000..6fea825 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/SimpleFOCRegisters.h @@ -0,0 +1,72 @@ + +#pragma once + +#include + + +// this constant is changed each time the registers definition are changed *in an incompatible way*. This means that just adding new registers +// does not change the version, but removing or changing the meaning of existing registers does, or changing the number of an existing register. +#define SIMPLEFOC_REGISTERS_VERSION 0x01 + + + +typedef enum : uint8_t { + REG_STATUS = 0x00, // RO - 1 byte (motor status) + REG_MOTOR_ADDRESS = 0x01, // R/W - 1 byte + REG_REPORT = 0x02, // R/W - Write: variable, Read: variable, up to 32 bytes + REG_ENABLE_ALL = 0x03, // WO - 1 byte + REG_ENABLE = 0x04, // R/W - 1 byte + REG_CONTROL_MODE = 0x05, // R/W - 1 byte + REG_TORQUE_MODE = 0x06, // R/W - 1 byte + REG_MODULATION_MODE = 0x07, // R/W - 1 byte + + REG_TARGET = 0x08, // R/W - float + REG_ANGLE = 0x09, // RO - float + REG_POSITION = 0x10, // RO - int32_t full rotations + float position (0-2PI, in radians) (4 bytes + 4 bytes) + REG_VELOCITY = 0x11, // RO - float + REG_SENSOR_ANGLE = 0x12, // RO - float + + REG_VOLTAGE_Q = 0x20, // RO - float + REG_VOLTAGE_D = 0x21, // RO - float + REG_CURRENT_Q = 0x22, // RO - float + REG_CURRENT_D = 0x23, // RO - float + REG_CURRENT_A = 0x24, // RO - float + REG_CURRENT_B = 0x25, // RO - float + REG_CURRENT_C = 0x26, // RO - float + REG_CURRENT_ABC = 0x27, // RO - 3xfloat = 12 bytes + REG_CURRENT_DC = 0x28, // RO - float + + REG_VEL_PID_P = 0x30, // R/W - float + REG_VEL_PID_I = 0x31, // R/W - float + REG_VEL_PID_D = 0x32, // R/W - float + REG_VEL_LPF_T = 0x33, // R/W - float + REG_ANG_PID_P = 0x34, // R/W - float + REG_VEL_LIMIT = 0x35, // R/W - float + REG_VEL_MAX_RAMP = 0x36, // R/W - float + + REG_CURQ_PID_P = 0x40, // R/W - float + REG_CURQ_PID_I = 0x41, // R/W - float + REG_CURQ_PID_D = 0x42, // R/W - float + REG_CURQ_LPF_T = 0x43, // R/W - float + REG_CURD_PID_P = 0x44, // R/W - float + REG_CURD_PID_I = 0x45, // R/W - float + REG_CURD_PID_D = 0x46, // R/W - float + REG_CURD_LPF_T = 0x47, // R/W - float + + REG_VOLTAGE_LIMIT = 0x50, // R/W - float + REG_CURRENT_LIMIT = 0x51, // R/W - float + REG_MOTION_DOWNSAMPLE = 0x52, // R/W - uint32_t + REG_DRIVER_VOLTAGE_LIMIT = 0x53,// R/W - float + REG_PWM_FREQUENCY = 0x54, // R/W - uint32_t + + REG_ZERO_ELECTRIC_ANGLE = 0x60, // RO - float + REG_SENSOR_DIRECTION = 0x61, // RO - 1 byte + REG_ZERO_OFFSET = 0x62, // R/W - float + REG_POLE_PAIRS = 0x63, // RO - uint32_t + REG_PHASE_RESISTANCE = 0x64, // R/W - float + REG_KV = 0x65, // R/W - float + REG_INDUCTANCE = 0x66, // R/W - float + + REG_NUM_MOTORS = 0x70, // RO - 1 byte + REG_SYS_TIME = 0x71, // RO - uint32_t +} SimpleFOCRegister; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp new file mode 100644 index 0000000..737b2ee --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.cpp @@ -0,0 +1,424 @@ + +#include "I2CCommander.h" + +I2CCommander::I2CCommander(TwoWire* wire) : _wire(wire) { + +}; + +I2CCommander::~I2CCommander(){}; + +void I2CCommander::init(uint8_t address) { + _address = address; +}; + + + +void I2CCommander::addMotor(FOCMotor* motor){ + if (numMotorsavailable()>=numBytes){ + byte* bytes = (byte*)valueToSet; + for (int i=0;iread(); + return true; + } + commanderror = true; + return false; +}; + + + +void I2CCommander::writeFloat(float value){ + _wire->write((byte*)&value, 4); +}; + + + + +void I2CCommander::onReceive(int numBytes){ + lastcommanderror = commanderror; + lastcommandregister = curRegister; + commanderror = false; + if (numBytes>=1) { // set the current register + curRegister = static_cast(_wire->read()); + } + if (numBytes>1) { // read from i2c and update value represented by register as well... + if (!receiveRegister(curMotor, curRegister, numBytes)) + commanderror = true; + } + if (numBytes<1) + commanderror = true; +}; + + + +void I2CCommander::onRequest(){ + commanderror = false; + if (!sendRegister(curMotor, curRegister)) + commanderror = true; +}; + + + + +/* +Reads values from I2C bus and updates the motor's values. + +Currently this isn't really thread-safe, but works ok in practice on 32-bit MCUs. + +Do not use on 8-bit architectures where the 32 bit writes may not be atomic! + +Plan to make this safe: the writes should be buffered, and not actually executed +until in the main loop by calling commander->run(); +the run() method disables interrupts while the updates happen. +*/ +bool I2CCommander::receiveRegister(uint8_t motorNum, uint8_t registerNum, int numBytes) { + int val; + switch (registerNum) { + case REG_MOTOR_ADDRESS: + val = _wire->read(); // reading one more byte is definately ok, since numBytes>1 + if (val>=0 && val=3 && (numBytes&0x01)==1) { // numBytes must be odd, since we have register and n pairs of motor/register numbers + val = (numBytes-1)/2; + if (val>I2CCOMMANDER_MAX_REPORT_REGISTERS) + val = I2CCOMMANDER_MAX_REPORT_REGISTERS; + for (int i=0;iread(); + reportRegisters[i] = _wire->read(); + } + } + else + commanderror = true; + break; + case REG_ENABLE_ALL: + val = _wire->read(); + for (int i=0; i0)?motors[i]->enable():motors[i]->disable(); + break; + case REG_ENABLE: + val = _wire->read(); + (val>0)?motors[motorNum]->enable():motors[motorNum]->disable(); + break; + case REG_CONTROL_MODE: + val = _wire->read(); + if (val>=0 && val<=4) // TODO these enums don't have assigned constants + motors[motorNum]->controller = static_cast(val); + else + commanderror = true; + break; + case REG_TORQUE_MODE: + val = _wire->read(); + if (val>=0 && val<=2) + motors[motorNum]->torque_controller = static_cast(val); + else + commanderror = true; + break; + case REG_MODULATION_MODE: + val = _wire->read(); + if (val>=0 && val<=3) + motors[motorNum]->foc_modulation = static_cast(val); + else + commanderror = true; + break; + case REG_TARGET: + readBytes(&(motors[motorNum]->target), 4); + break; + case REG_VEL_PID_P: + readBytes(&(motors[motorNum]->PID_velocity.P), 4); + break; + case REG_VEL_PID_I: + readBytes(&(motors[motorNum]->PID_velocity.I), 4); + break; + case REG_VEL_PID_D: + readBytes(&(motors[motorNum]->PID_velocity.D), 4); + break; + case REG_VEL_LPF_T: + readBytes(&(motors[motorNum]->LPF_velocity.Tf), 4); + break; + case REG_ANG_PID_P: + readBytes(&(motors[motorNum]->P_angle.P), 4); + break; + case REG_VEL_LIMIT: + readBytes(&(motors[motorNum]->velocity_limit), 4); + break; + case REG_VEL_MAX_RAMP: + readBytes(&(motors[motorNum]->PID_velocity.output_ramp), 4); + break; + case REG_CURQ_PID_P: + readBytes(&(motors[motorNum]->PID_current_q.P), 4); + break; + case REG_CURQ_PID_I: + readBytes(&(motors[motorNum]->PID_current_q.I), 4); + break; + case REG_CURQ_PID_D: + readBytes(&(motors[motorNum]->PID_current_q.D), 4); + break; + case REG_CURQ_LPF_T: + readBytes(&(motors[motorNum]->LPF_current_q.Tf), 4); + break; + case REG_CURD_PID_P: + readBytes(&(motors[motorNum]->PID_current_d.P), 4); + break; + case REG_CURD_PID_I: + readBytes(&(motors[motorNum]->PID_current_d.I), 4); + break; + case REG_CURD_PID_D: + readBytes(&(motors[motorNum]->PID_current_d.D), 4); + break; + case REG_CURD_LPF_T: + readBytes(&(motors[motorNum]->LPF_current_d.Tf), 4); + break; + case REG_VOLTAGE_LIMIT: + readBytes(&(motors[motorNum]->voltage_limit), 4); + break; + case REG_CURRENT_LIMIT: + readBytes(&(motors[motorNum]->current_limit), 4); + break; + case REG_MOTION_DOWNSAMPLE: + readBytes(&(motors[motorNum]->motion_downsample), 4); + break; + case REG_ZERO_OFFSET: + readBytes(&(motors[motorNum]->sensor_offset), 4); + break; + // RO registers + case REG_STATUS: + case REG_ANGLE: + case REG_POSITION: + case REG_VELOCITY: + case REG_SENSOR_ANGLE: + case REG_VOLTAGE_Q: + case REG_VOLTAGE_D: + case REG_CURRENT_Q: + case REG_CURRENT_D: + case REG_CURRENT_A: + case REG_CURRENT_B: + case REG_CURRENT_C: + case REG_CURRENT_ABC: + case REG_ZERO_ELECTRIC_ANGLE: + case REG_SENSOR_DIRECTION: + case REG_POLE_PAIRS: + case REG_PHASE_RESISTANCE: + case REG_NUM_MOTORS: + case REG_SYS_TIME: + default: // unknown register + return false; + } + return true; +} + + + + + +/* + Reads values from motor/sensor and writes them to I2C bus. Intended to be run + from the Wire.onRequest interrupt. + + Assumes atomic 32 bit reads. On 8-bit arduino this assumption does not hold and this + code is not safe on those platforms. You might read "half-written" floats. + + A solution might be to maintain a complete set of shadow registers in the commander + class, and update them in the run() method (which runs with interrupts off). Not sure + of the performance impact of all those 32 bit read/writes though. In any case, since + I use only 32 bit MCUs I'll leave it as an excercise to the one who needs it. ;-) + + On 32 bit platforms the implication is that reads will occur atomically, so data will + be intact, but they can occur at any time during motor updates, so different values might + not be in a fully consistent state (i.e. phase A current might be from the current iteration + but phase B current from the previous iteration). +*/ +bool I2CCommander::sendRegister(uint8_t motorNum, uint8_t registerNum) { + // read the current register + switch(registerNum) { + case REG_STATUS: + _wire->write(curMotor); + _wire->write((uint8_t)lastcommandregister); + _wire->write((uint8_t)lastcommanderror+1); + for (int i=0;(iwrite(motors[i]->motor_status); + } + break; + case REG_MOTOR_ADDRESS: + _wire->write(curMotor); + break; + case REG_REPORT: + for (int i=0;iwrite(motors[motorNum]->enabled); + break; + case REG_MODULATION_MODE: + _wire->write(motors[motorNum]->foc_modulation); + break; + case REG_TORQUE_MODE: + _wire->write(motors[motorNum]->torque_controller); + break; + case REG_CONTROL_MODE: + _wire->write(motors[motorNum]->controller); + break; + + case REG_TARGET: + writeFloat(motors[motorNum]->target); + break; + case REG_ANGLE: + writeFloat(motors[motorNum]->shaft_angle); + break; + case REG_VELOCITY: + writeFloat(motors[motorNum]->shaft_velocity); + break; + case REG_SENSOR_ANGLE: + if (motors[motorNum]->sensor) + writeFloat(motors[motorNum]->sensor->getAngle()); // stored angle + else + writeFloat(motors[motorNum]->shaft_angle); + break; + + case REG_VOLTAGE_Q: + writeFloat(motors[motorNum]->voltage.q); + break; + case REG_VOLTAGE_D: + writeFloat(motors[motorNum]->voltage.d); + break; + case REG_CURRENT_Q: + writeFloat(motors[motorNum]->current.q); + break; + case REG_CURRENT_D: + writeFloat(motors[motorNum]->current.d); + break; + case REG_CURRENT_A: + if (motors[motorNum]->current_sense) // TODO check if current sense can be called from inside this callback function + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().a); + else + writeFloat(0.0f); + break; + case REG_CURRENT_B: + if (motors[motorNum]->current_sense) + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().b); + else + writeFloat(0.0f); + break; + case REG_CURRENT_C: + if (motors[motorNum]->current_sense) + writeFloat(motors[motorNum]->current_sense->getPhaseCurrents().c); + else + writeFloat(0.0f); + break; + case REG_CURRENT_ABC: + if (motors[motorNum]->current_sense) { + PhaseCurrent_s currents = motors[motorNum]->current_sense->getPhaseCurrents(); + writeFloat(currents.a); + writeFloat(currents.b); + writeFloat(currents.c); + } + else { + writeFloat(0.0f); + writeFloat(0.0f); + writeFloat(0.0f); + } + break; + case REG_VEL_PID_P: + writeFloat(motors[motorNum]->PID_velocity.P); + break; + case REG_VEL_PID_I: + writeFloat(motors[motorNum]->PID_velocity.I); + break; + case REG_VEL_PID_D: + writeFloat(motors[motorNum]->PID_velocity.D); + break; + case REG_VEL_LPF_T: + writeFloat(motors[motorNum]->LPF_velocity.Tf); + break; + case REG_ANG_PID_P: + writeFloat(motors[motorNum]->P_angle.P); + break; + case REG_VEL_LIMIT: + writeFloat(motors[motorNum]->velocity_limit); + break; + case REG_VEL_MAX_RAMP: + writeFloat(motors[motorNum]->PID_velocity.output_ramp); + break; + + case REG_CURQ_PID_P: + writeFloat(motors[motorNum]->PID_current_q.P); + break; + case REG_CURQ_PID_I: + writeFloat(motors[motorNum]->PID_current_q.I); + break; + case REG_CURQ_PID_D: + writeFloat(motors[motorNum]->PID_current_q.D); + break; + case REG_CURQ_LPF_T: + writeFloat(motors[motorNum]->LPF_current_q.Tf); + break; + case REG_CURD_PID_P: + writeFloat(motors[motorNum]->PID_current_d.P); + break; + case REG_CURD_PID_I: + writeFloat(motors[motorNum]->PID_current_d.I); + break; + case REG_CURD_PID_D: + writeFloat(motors[motorNum]->PID_current_d.D); + break; + case REG_CURD_LPF_T: + writeFloat(motors[motorNum]->LPF_current_d.Tf); + break; + + case REG_VOLTAGE_LIMIT: + writeFloat(motors[motorNum]->voltage_limit); + break; + case REG_CURRENT_LIMIT: + writeFloat(motors[motorNum]->current_limit); + break; + case REG_MOTION_DOWNSAMPLE: + _wire->write((int)motors[motorNum]->motion_downsample); // TODO int will have different sizes on different platforms + // but using uint32 doesn't compile clean on all, e.g. RP2040 + break; + + case REG_ZERO_ELECTRIC_ANGLE: + writeFloat(motors[motorNum]->zero_electric_angle); + break; + case REG_SENSOR_DIRECTION: + _wire->write((int8_t)motors[motorNum]->sensor_direction); + break; + case REG_ZERO_OFFSET: + writeFloat(motors[motorNum]->sensor_offset); + break; + case REG_PHASE_RESISTANCE: + writeFloat(motors[motorNum]->phase_resistance); + break; + case REG_POLE_PAIRS: + _wire->write((int)motors[motorNum]->pole_pairs); + break; + + case REG_SYS_TIME: + // TODO how big is millis()? Same on all platforms? + _wire->write((int)millis()); + break; + case REG_NUM_MOTORS: + _wire->write(numMotors); + break; + + // unknown register or write only register (no read) + case REG_ENABLE_ALL: + default: + _wire->write(0); // TODO what to send in this case? + return false; + } + return true; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h new file mode 100644 index 0000000..65cefde --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommander.h @@ -0,0 +1,49 @@ + +#ifndef SIMPLEFOC_I2CCOMMANDER_H +#define SIMPLEFOC_I2CCOMMANDER_H + +#include "Arduino.h" +#include "Wire.h" +#include "common/base_classes/FOCMotor.h" +#include "../SimpleFOCRegisters.h" + +#ifndef I2CCOMMANDER_MAX_MOTORS_COMMANDABLE +#define I2CCOMMANDER_MAX_MOTORS_COMMANDABLE 4 +#endif + +#define I2CCOMMANDER_MIN_VELOCITY_FOR_MOTOR_MOVING 0.1f // in rad/s +#define I2CCOMMANDER_MAX_REPORT_REGISTERS 8 + + +class I2CCommander { + public: + I2CCommander(TwoWire* wire = &Wire); + ~I2CCommander(); + + void addMotor(FOCMotor* motor); // first add motors + virtual void init(uint8_t address); // then init + + void onReceive(int numBytes); + void onRequest(); + + protected: + void writeFloat(float value); + bool readBytes(void* valueToSet, uint8_t numBytes); + virtual bool sendRegister(uint8_t motorNum, uint8_t registerNum); + virtual bool receiveRegister(uint8_t motorNum, uint8_t registerNum, int numBytes); + + uint8_t _address; + TwoWire* _wire; + uint8_t numMotors = 0; + uint8_t curMotor = 0; + SimpleFOCRegister curRegister = REG_STATUS; + bool commanderror = false; + bool lastcommanderror = false; + uint8_t lastcommandregister = REG_STATUS; + FOCMotor* motors[I2CCOMMANDER_MAX_MOTORS_COMMANDABLE]; + uint8_t numReportRegisters = 0; + uint8_t reportMotors[I2CCOMMANDER_MAX_REPORT_REGISTERS]; + uint8_t reportRegisters[I2CCOMMANDER_MAX_REPORT_REGISTERS]; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp new file mode 100644 index 0000000..666e009 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.cpp @@ -0,0 +1,59 @@ + +#include "I2CCommanderMaster.h" + + +I2CCommanderMaster::I2CCommanderMaster(int maxMotors) : maxMotors(maxMotors), motors(new I2CRemoteMotor[maxMotors]) { +}; + + + +I2CCommanderMaster::~I2CCommanderMaster(){ +}; + + + +void I2CCommanderMaster::init(){ +}; + + +// TODO handle multiple motors per target +void I2CCommanderMaster::addI2CMotors(uint8_t i2cAddress, uint8_t motorCount, TwoWire *wire){ + for (int i=0;ibeginTransmission(motors[motor].address); + motors[motor].wire->write((uint8_t)registerNum); + motors[motor].wire->write((uint8_t*)data, size); + motors[motor].wire->endTransmission(); + return size; +}; + + +int I2CCommanderMaster::readRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size){ + motors[motor].wire->beginTransmission(motors[motor].address); + int numWrite = motors[motor].wire->write((uint8_t)registerNum); // TODO check return value + motors[motor].wire->endTransmission(); + if (numWrite==1) + return readLastUsedRegister(motor, data, size); + return 0; +}; + + +int I2CCommanderMaster::readLastUsedRegister(int motor, void* data, uint8_t size){ + int numRead = motors[motor].wire->requestFrom(motors[motor].address, size); + if (numRead==size) + motors[motor].wire->readBytes((uint8_t*)data, size); + else { + return 0; + } + return numRead; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h new file mode 100644 index 0000000..9b52ae7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/I2CCommanderMaster.h @@ -0,0 +1,42 @@ +#ifndef SIMPLEFOC_I2CCOMMANDER_H +#define SIMPLEFOC_I2CCOMMANDER_H + +#include +#include + +#include "../SimpleFOCRegisters.h" + +#define I2COMMANDER_DEFAULT_MAX_REMOTE_MOTORS 4 + + +typedef struct { + TwoWire* wire; + uint8_t address; +} I2CRemoteMotor; + + +class I2CCommanderMaster { + + public: + I2CCommanderMaster(int maxMotors = I2COMMANDER_DEFAULT_MAX_REMOTE_MOTORS); + ~I2CCommanderMaster(); + void init(); + void addI2CMotors(uint8_t i2cAddress, uint8_t motorCount, TwoWire *wire = &Wire); + + int writeRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size); + int readRegister(int motor, SimpleFOCRegister registerNum, void* data, uint8_t size); + int readLastUsedRegister(int motor, void* data, uint8_t size); + + // Motor intialization interface for convenience - think about how this will best work + // i.e. which parameters should be set by i2c and which should be hard-coded, and where config info is saved + // TODO bool initializeMotor(int motor); + + private: + int maxMotors; + int numMotors = 0; + I2CRemoteMotor* motors; + +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/README.md new file mode 100644 index 0000000..eac9e34 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/i2c/README.md @@ -0,0 +1,98 @@ + +# I2CCommander + +Implementation of SimpleFOC Commander for I2C communication bus. + +This code takes the point of view that the motor driver (the "muscle") is the I2C target device, and another MCU/CPU (the "brain") is the I2C controller device, which is coordinating one or more motor drivers on the same or different I2C buses. Each motor driver can offer one or more motors for control via the I2CCommander. So fairly flexible setups are possible, with multiple motors per driver, multiple drivers per I2C bus and multiple I2C buses on the brain MCU. + +## Warning + +This is new code, and has not been extensively tested. Your milage may vary. That said, basic use cases have been tested, and we would certainly appreciate feedback and help with testing it out. + +In particular, there are concurrency issues with reading/writing the SimpleFOC motor values from I2C while the motor is running. These should be solved soon in an upcoming version. + +**Do not run on 8-bit MCUs!** The code currently assumes atomic 32 bit reads, so running on Arduino UNO or Nano is unfortunately a no-go. + +## Using + +As would be expected for I2C, each target device needs a unique I2C address on its bus, and setting up and discovering these addresses is out-of-scope for I2CCommander. Setting up and configuring the TwoWire objects (which pins, speed, etc...) is also out of scope and finished, initialized TwoWire objects must be passed to I2CCommander. If you don't specify a different reference, the standard *Wire* object is assumed. + +Communication with the motor drivers happens via a register paradigm. The driver board offers many registers, some of which can be read, some can be written, and some are read/write. The controller MCU sends I2C messages to the target device to read or write a register as desired. The size of the data to be read/written depends on the register, and must be known by the controller. See Registers, below, for more details on the individual registers. + +Since each target motor driver can handle multiple motors, one of the registers contains the currently selected motor. Most of the other registers then operate on the currently selected motor. There are some exceptions, like REG_ENABLE_ALL - which operates on all the motors, or REG_STATUS, which returns stati for all the motors. + +### Target device (motor driver) + +The target device (motor driver) initializes and uses an instance of I2CCommander. Only one instance is needed for all attached motors: + +```c++ +#include "Arduino.h" +#include +#include +#include "SimpleFOCDrivers.h" +#include "comms/i2c/I2CCommander.h" + +// commander instance +uint8_t i2c_addr = 0x60; // can be anything you choose +I2CCommander commander; +// interrupt callbacks +void onReceive(int numBytes) { commander.onReceive(numBytes); } +void onRequest() { commander.onRequest(); } + + +// ... other variables, like sensor, etc... +BLDCMotor motor = BLDCMotor(POLE_PAIRS); + + +void setup() { + + // ...other setup code + + Wire.setClock(400000); // use same speed on controller device + Wire.begin(i2c_addr, true); // initialize i2c in target mode + commander.addMotor(&motor); // add a motor + //commander.addMotor(&motor2); // you could add more than one motor + commander.init(i2c_addr); // initialize commander + Wire.onReceive(onReceive); // connect the interrupt handlers + Wire.onRequest(onRequest); + +} +``` + +### Controller device ("brain" MCU) + +On the controller device, you use an instance of I2CCommanderMaster, which you initialize by adding one or more target devices to it: + +```c++ +#include "Arduino.h" +#include +#include +#include "SimpleFOCDrivers.h" +#include "comms/i2c/I2CCommanderMaster.h" + +#define TARGET_I2C_ADDRESS 0x60 +I2CCommanderMaster commander; + +void setup() { + + // ...other setup code + + Wire.setClock(400000); // use same speed on target device! + Wire.begin(); // initialize i2c in controller mode + commander.addI2CMotors(TARGET_I2C_ADDRESS, 1); // add target device, it has 1 motor + //commander.addI2CMotors(TARGET_I2C_ADDRESS2, 1); // you could add another target device on the same bus + //commander.addI2CMotors(TARGET_I2C_ADDRESS, 1, &wire2); // or on a different i2c bus + commander.init(); // init commander + Wire.onReceive(onReceive); // connect the interrupt handlers + Wire.onRequest(onRequest); + +} + +``` + + +## Extending + +The API is somewhat opinionated, and unlike the standard serial commander, currently does not support hooking in your own variables for reading/writing to them via I2C. This is because I2C is a bit less flexible than Serial. + +If you want to extend I2CCommander please subclass it and override the functions `sendRegister` and `receiveRegister` to add new registers. Use register numbers above 0x80 to prevent collisions with the standard registers. \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/README.md new file mode 100644 index 0000000..2e1b733 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/README.md @@ -0,0 +1,49 @@ + +# Serial communications classes + +Serial communications classes for register-based control and telemetry from SimpleFOC. + +## SerialASCIITelemetry + +:warning: unfinished, untested + +Telemetry class that sends telemetry as ASCII on a serial port. Similar to the classic "monitoring" functionality of SimpleFOC, but allows you to configure telemetry based on most of the defined registers. + +Usage: + +```c++ + +SerialASCIITelemetry telemetry = SerialASCIITelemetry(); // number of float digits to display + +void setup() { + ... + telemetry.addMotor(&motor); + telemetry.setTelemetryRegisters(2, [REG_VELOCITY, REG_VOLTAGE_Q]); + telemetry.init(); + ... +} + +void loop() { + motor.move(); + motor.loopFOC(); + telemetry.run(); +} +``` + +Some options are supported: + +```c++ + telemetry.floatPrecision = 4; // send floats with 4 decimal places + telemetry.min_elapsed_time = 0; // microseconds between sending telemetry + telemetry.downsample = 100; // send every this many loop iterations +``` + + + +## SerialBinaryCommander + +:warning: unfinished, untested! + +Control SimpleFOC via a binary protocol over the serial port. The standard SimpleFOC registers are used. + +TODO document the protocol \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp new file mode 100644 index 0000000..084b940 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.cpp @@ -0,0 +1,65 @@ + +#include "./SerialASCIITelemetry.h" + +SerialASCIITelemetry::SerialASCIITelemetry(int floatPrecision) : Telemetry() { + this->floatPrecision = floatPrecision; +}; + +SerialASCIITelemetry::~SerialASCIITelemetry(){ + +}; + +void SerialASCIITelemetry::init(Print* print){ + this->_print = print; + this->Telemetry::init(); +}; + + + +void SerialASCIITelemetry::sendHeader() { + if (numRegisters > 0) { + writeChar('H'); + writeChar(' '); + for (uint8_t i = 0; i < numRegisters; i++) { + writeByte(registers_motor[i]); + writeChar(':'); + writeByte(registers[i]); + if (i < numRegisters-1) + writeChar(' '); + }; + writeChar('\n'); + }; +}; + + + +void SerialASCIITelemetry::sendTelemetry(){ + if (numRegisters > 0) { + for (uint8_t i = 0; i < numRegisters; i++) { + sendRegister(static_cast(registers[i]), motors[registers_motor[i]]); + if (i < numRegisters-1) + writeChar(' '); + }; + writeChar('\n'); + } +}; + + + +uint8_t SerialASCIITelemetry::writeChar(char value){ + return _print->print(value); +}; + +uint8_t SerialASCIITelemetry::writeByte(uint8_t value){ + return _print->print(value); +}; + + +uint8_t SerialASCIITelemetry::writeFloat(float value){ + return _print->print(value, floatPrecision); +}; + + +uint8_t SerialASCIITelemetry::writeInt(uint32_t value){ + return _print->print(value); +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h new file mode 100644 index 0000000..e218f3b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialASCIITelemetry.h @@ -0,0 +1,27 @@ + +#pragma once + +#include "Arduino.h" +#include "../telemetry/Telemetry.h" + +class SerialASCIITelemetry : public Telemetry { +public: + SerialASCIITelemetry(int floatPrecision = 2); + virtual ~SerialASCIITelemetry(); + + void init(Print* print = &Serial); + +protected: + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + uint8_t writeChar(char value); + + void sendTelemetry() override; + void sendHeader() override; + + Print* _print; + int floatPrecision = 2; +}; + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp new file mode 100644 index 0000000..ffa2406 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.cpp @@ -0,0 +1,153 @@ + +#include "SerialBinaryCommander.h" + + + +SerialBinaryCommander::SerialBinaryCommander(bool echo) : Telemetry(), echo(echo) { + +}; + + + +SerialBinaryCommander::~SerialBinaryCommander(){ + +}; + + + +void SerialBinaryCommander::init(Stream &serial) { + _serial = &serial; + this->Telemetry::init(); +}; + + + +void SerialBinaryCommander::run() { + if (_serial->available()>2) { // TODO make this work with partial packets... + uint8_t command = _serial->read(); + uint8_t registerNum = _serial->read(); + uint8_t motorNum = _serial->read(); + if (command==SERIALBINARY_COMMAND_READ){ + startFrame(registerNum, SERIALBINARY_FRAMETYPE_REGISTER); // TODO call is incorrect + sendRegister(static_cast(registerNum), motors[motorNum]); + endFrame(); + } + else if (command==SERIALBINARY_COMMAND_WRITE) { + setRegister(static_cast(registerNum), motors[motorNum]); + if (echo) { + startFrame(registerNum, SERIALBINARY_FRAMETYPE_REGISTER); // TODO call is incorrect + sendRegister(static_cast(registerNum), motors[motorNum]); + endFrame(); + } + } + } + // and handle the telemetry + this->Telemetry::run(); +}; + + + +uint8_t SerialBinaryCommander::readBytes(void* valueToSet, uint8_t numBytes){ + if (_serial->available()read(); + } + return numBytes; +}; + + + + +uint8_t SerialBinaryCommander::writeBytes(void* valueToSend, uint8_t numBytes){ + uint8_t* value = (uint8_t*)valueToSend; + for (uint8_t i=0; iwrite(value[i]); + } + return numBytes; +}; + + + + + + +void SerialBinaryCommander::startFrame(uint8_t frameSize, uint8_t type){ + _serial->write(0xA5); + _serial->write((uint8_t)((type<<6)|frameSize)); +}; + + + + + +void SerialBinaryCommander::endFrame(){ + _serial->write(0x5A); +}; + + + + +uint8_t SerialBinaryCommander::writeByte(uint8_t value){ + return writeBytes(&value, 1); +}; + + + +uint8_t SerialBinaryCommander::writeFloat(float value){ + return writeBytes(&value, 4); +}; + + + +uint8_t SerialBinaryCommander::writeInt(uint32_t value){ + return writeBytes(&value, 4); +}; + + + + +uint8_t SerialBinaryCommander::readByte(uint8_t* valueToSet){ + return readBytes(valueToSet, 1); +}; + + + +uint8_t SerialBinaryCommander::readFloat(float* valueToSet){ + return readBytes(valueToSet, 4); +}; + + + +uint8_t SerialBinaryCommander::readInt(uint32_t* valueToSet){ + return readBytes(valueToSet, 4); +}; + + + + + + +void SerialBinaryCommander::sendHeader() { + if (numRegisters > 0) { + startFrame(numRegisters*2, TELEMETRY_FRAMETYPE_HEADER); + for (uint8_t i = 0; i < numRegisters; i++) { + writeByte(registers[i]); + writeByte(registers_motor[i]); + }; + endFrame(); + }; +}; + + + +void SerialBinaryCommander::sendTelemetry(){ + if (numRegisters > 0) { + startFrame(frameSize, TELEMETRY_FRAMETYPE_DATA); + for (uint8_t i = 0; i < numRegisters; i++) { + sendRegister(static_cast(registers[i]), motors[registers_motor[i]]); + }; + endFrame(); + } +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h new file mode 100644 index 0000000..f80ccc4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/serial/SerialBinaryCommander.h @@ -0,0 +1,57 @@ + +#pragma once + +#include +#include "../RegisterReceiver.h" +#include "../telemetry/Telemetry.h" + + +#ifndef COMMS_MAX_REPORT_REGISTERS +#define COMMS_MAX_REPORT_REGISTERS 7 +#endif + + +#define SERIALBINARY_FRAMETYPE_REGISTER 0x03 +#define SERIALBINARY_COMMAND_READ 0x00 +#define SERIALBINARY_COMMAND_WRITE 0x80 + + +class SerialBinaryCommander : public Telemetry, public RegisterReceiver { +public: + SerialBinaryCommander(bool echo = false); + virtual ~SerialBinaryCommander(); + + void init(Stream &serial = Serial); + void run(); + + + bool echo; +protected: + virtual void startFrame(uint8_t frameSize, uint8_t type = TELEMETRY_FRAMETYPE_DATA); + virtual void endFrame(); + uint8_t readBytes(void* valueToSet, uint8_t numBytes); + uint8_t writeBytes(void* valueToSend, uint8_t numBytes); + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + void sendTelemetry() override; + void sendHeader() override; + + uint8_t curMotor = 0; + SimpleFOCRegister curRegister = REG_STATUS; + bool commanderror = false; + bool lastcommanderror = false; + uint8_t lastcommandregister = REG_STATUS; + + uint8_t numReportRegisters = 0; + uint8_t reportMotors[COMMS_MAX_REPORT_REGISTERS]; + SimpleFOCRegister reportRegisters[COMMS_MAX_REPORT_REGISTERS]; + + Stream* _serial; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/README.md new file mode 100644 index 0000000..1e36a5a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/README.md @@ -0,0 +1,58 @@ + +# STM32SpeedDirInput + +Control SimpleFOC using PWM speed and direction inputs. + +Based on STM32 timer PWM-Input capabilities, which means this can only be used on STM32 MCUs. It can cover a wide range of PWM frequencies, and runs without MCU overhead in the timer hardware. + + + +## Setup + +The PWM speed input should be connected to either channel 1 or channel 2 of a general purpose or advanced control timer on your STM32 MCU. Suitable timers are: +- Advanced control timers: TIM1, TIM8 +- General purpose timers (32 bit): TIM2, TIM5 +- General purpose timers (16 bit): TIM3, TIM4, TIM9, TIM12 +If in doubt, check in STM32CubeIDE and see if the PWM-Input mode can be enabled (under "Combined Channels") for the timer. + +The optional direction input can be connected to any GPIO pin. By default a high level direction input is associated with a positive velocity value, while a low level direction input results in a negative velocity value. To reverse this, set the option `dir_positive_high = false` + +The direction input is optional - if not provided, you can control the direction from software using the `direction` field. + +The velocity values returned are in the range `min_speed` to `max_speed`, while the input PWM duty cycle should lie within the range `min_pwm` to `max_pwm`. Actual input values smaller than `min_pwm` will be treated as `min_pwm`, values larger than `max_pwm` will be treated as `max_pwm`. The behaviour for 100% or 0% duty cycles is undefined, and they should be avoided. + +## Usage + +Use it like this: + +```c++ +#include "comms/stm32speeddir/STM32SpeedDirInput.h" + +// some example pins - the speed pin has to be on channel 1 or 2 of a timer +#define PIN_SPEED PC6 +#define PIN_DIRECTION PB8 + +STM32SpeedDirInput speed_dir = STM32SpeedDirInput(PIN_SPEED, PIN_DIRECTION); + +float target = 0.0f; + +void setup(){ + ... + + speed_dir.min_speed = 10.0f; // 10rad/s min speed + speed_dir.max_speed = 100.0f; // 100rad/s max speed + speed_dir.min_pwm = 5.0f; // 5% min duty cycle + speed_dir.max_pwm = 95.0f; // 95% max duty cycle + speed_dir.init(); + + ... +} + + +void loop(){ + target = speed_dir.getTargetVelocity(); + motor.move(target); + motor.loopFOC(); +} + +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp new file mode 100644 index 0000000..4bc8bee --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.cpp @@ -0,0 +1,29 @@ + +#include "./STM32SpeedDirInput.h" + +#if defined(_STM32_DEF_) + +STM32SpeedDirInput::STM32SpeedDirInput(int pin_speed, int pin_dir) : STM32PWMInput(pin_speed) { + _pin_speed = pin_speed; + _pin_dir = pin_dir; +}; + +STM32SpeedDirInput::~STM32SpeedDirInput(){}; + +int STM32SpeedDirInput::init(){ + pinMode(_pin_dir, INPUT); + return STM32PWMInput::initialize(); +}; + + +float STM32SpeedDirInput::getTargetVelocity(){ + if (_pin_dir != NOT_SET) + direction = digitalRead(_pin_dir); + float speed = getDutyCyclePercent(); + speed = constrain(speed, min_pwm, max_pwm); + speed = (speed - min_pwm)/(max_pwm - min_pwm) * (max_speed - min_speed) + min_speed; + return (direction == dir_positive_high) ? speed : -speed; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h new file mode 100644 index 0000000..3a3647b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/stm32speeddir/STM32SpeedDirInput.h @@ -0,0 +1,31 @@ + +#pragma once + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + +#include "common/foc_utils.h" +#include "utilities/stm32pwm/STM32PWMInput.h" + +class STM32SpeedDirInput : public STM32PWMInput { + public: + STM32SpeedDirInput(int pin_speed, int pin_dir = NOT_SET); + ~STM32SpeedDirInput(); + + int init(); + float getTargetVelocity(); + + float min_speed = 0; // min speed in rad/s + float max_speed = 100; // max speed in rad/s + float min_pwm = 5.0; // min duty cycle in % + float max_pwm = 95.0; // max duty cycle in % + bool dir_positive_high = true; // true if the direction pin is high when the motor is spinning in the positive direction + bool direction = true; // direction of rotation, default positive + protected: + int _pin_speed; + int _pin_dir; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/README.md new file mode 100644 index 0000000..384098f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/README.md @@ -0,0 +1,42 @@ + +# SimpleFOC Telemetry + +:warning: unfinished, untested + +A flexible abstraction for telemetry (monitoring) of SimpleFOC systems. + +The telemetry implementation is based on the SimpleFOC registers, and allows you to send telemetry for any (readable) register. Telemetry supports multiple motors. + +The concept allows you to choose registers which are then sent by the telemetry automatically, on a regular schedule. + +The method of sending depends on the type of telemetry you add to your program. There are telemetry drivers for: + +- Serial ASCII telemetry +- Serial Binary telemetry +- and more drivers will be added in the future + +Multiple motors can be added to the same telemetry, to monitor several motors at the same time. The registers reported by telemetry can be changed at run-time. Multiple instances of telemetry can be used to monitor different sets of values at different time intervals, or to send to multiple channels at the same time. + +## Usage + +Using telemetry is simple: + +```c++ + +SerialASCIITelemetry telemetry = SerialASCIITelemetry(); +... + +void setup() { + ... + telemetry.addMotor(&motor); + telemetry.setTelemetryRegisters(3, { REG_TARGET, REG_ANGLE, REG_VELOCITY }); + telemetry.init(); + ... +} + +void loop() { + motor.move(); + motor.loopFOC(); + telemetry.run(); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp new file mode 100644 index 0000000..6a6c446 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.cpp @@ -0,0 +1,67 @@ + + +#include "./Telemetry.h" + + + +Telemetry::Telemetry() { + this->numRegisters = 0; +}; + + + +Telemetry::~Telemetry(){ + +}; + + + + + +void Telemetry::setTelemetryRegisters(uint8_t numRegisters, uint8_t* registers, uint8_t* motors){ + if (numRegisters<=TELEMETRY_MAX_REGISTERS) { + this->numRegisters = numRegisters; + for (uint8_t i=0; iregisters[i] = registers[i]; + if (motors!=NULL) + this->registers_motor[i] = motors[i]; + else + this->registers_motor[i] = 0; + } + } +}; + + + +void Telemetry::init() { + headerSent = false; +}; + + + +void Telemetry::run() { + if (numRegisters<1) + return; + if (!headerSent) { + sendHeader(); + headerSent = true; + } + if (downsampleCnt++ < downsample) return; + downsampleCnt = 0; + if (min_elapsed_time > 0) { + long now = _micros(); + if (now - last_run_time < min_elapsed_time) return; + last_run_time = now; + } + sendTelemetry(); +} + + + +void Telemetry::addMotor(FOCMotor* motor) { + if (numMotors < TELEMETRY_MAX_MOTORS) { + motors[numMotors] = motor; + numMotors++; + } +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h new file mode 100644 index 0000000..256036a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/comms/telemetry/Telemetry.h @@ -0,0 +1,53 @@ + +#pragma once + +#include "../SimpleFOCRegisters.h" +#include "../RegisterSender.h" + +#ifndef TELEMETRY_MAX_REGISTERS +#define TELEMETRY_MAX_REGISTERS 8 +#endif + +#ifndef TELEMETRY_MAX_MOTORS +#define TELEMETRY_MAX_MOTORS 4 +#endif + + +#define DEF_TELEMETRY_DOWNSAMPLE 100 + + +typedef enum : uint8_t { + TELEMETRY_FRAMETYPE_DATA = 0x01, + TELEMETRY_FRAMETYPE_HEADER = 0x02 +} TelemetryFrameType; + + + + +class Telemetry : public RegisterSender { +public: + Telemetry(); + virtual ~Telemetry(); + virtual void init(); + void addMotor(FOCMotor* motor); + void setTelemetryRegisters(uint8_t numRegisters, uint8_t* registers, uint8_t* motors = NULL); + void run(); + + uint16_t downsample = DEF_TELEMETRY_DOWNSAMPLE; + uint32_t min_elapsed_time = 0; +protected: + virtual void sendTelemetry() = 0; + virtual void sendHeader() = 0; + + FOCMotor* motors[TELEMETRY_MAX_MOTORS]; + uint8_t numMotors = 0; + + uint8_t numRegisters; + uint8_t registers[TELEMETRY_MAX_REGISTERS]; + uint8_t registers_motor[TELEMETRY_MAX_REGISTERS]; + uint8_t frameSize; + bool headerSent; + long last_run_time = 0; + uint16_t downsampleCnt = 0; +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/README.md new file mode 100644 index 0000000..8d15d0c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/README.md @@ -0,0 +1,175 @@ + +# DRV8316 SimpleFOC driver + +The DRV8316 is a integrated FET, integrated current sensing 3-phase BLDC driver IC from Texas Instruments, including all protections and many cool configuration optons. See https://www.ti.com/product/DRV8316 for more information. + +This driver includes a DRV8316 SPI driver, and specific subclasses for SimpleFOCs BLDCDriver3PWM and BLDCDriver6PWM generic drivers. The code is designed to be "Ardunio compatible" and should work with any of the hardware architectures supported by SimpleFOC. + +## Hardware setup + +To use the DRV8316 you will have to connect the following: + +- GND +- SPI MOSI +- SPI MISO +- SPI CLK +- SPI nCS +- INH_U - connect to motor PWM pin +- INH_V - connect to motor PWM pin +- INH_W - connect to motor PWM pin +- INL_U - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- INL_V - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- INL_W - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation + +Optionally, but probably useful: + +- nFault - digital in, active low +- DRVOFF - digital out + +For current sensting: + +- VREF - either connect to DAC output of the MCU or a fixed voltage reference. +- ISENA - connect to analog in +- ISENB - connect to analog in +- ISENC - connect to analog in + +For current limiting: + +- ILIM (same as VREF) - connect it to a DAC output of the MCU if you want to control the current limit from the MCU + +## Usage + +Usage is quite easy, especially if you already know SimpleFOC. See also the [examples](https://github.com/simplefoc/Arduino-FOC-drivers/examples/drivers/drv8316/) + +```c++ +#include "Arduino.h" +#include +#include +#include +#include "SimpleFOCDrivers.h" +#include "drivers/drv8316/drv8316.h" + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver6PWM driver = DRV8316Driver6PWM(A3,0,A4,1,2,6,7,false); // MKR1010 6-PWM + +//... normal simpleFOC init code... +``` + +Or, for 3-PWM: + +```c++ +#include "Arduino.h" +#include +#include +#include +#include "SimpleFOCDrivers.h" +#include "drivers/drv8316/drv8316.h" + +BLDCMotor motor = BLDCMotor(11); +DRV8316Driver3PWM driver = DRV8316Driver3PWM(A3,A4,2,7,false); // MKR1010 3-PWM +// these are examples, for 3-PWM you could use any output pins as the enable pins. +#define ENABLE_A 0 +#define ENABLE_B 1 +#define ENABLE_C 6 + +void setup() { + + + pinMode(ENABLE_A, OUTPUT); + digitalWrite(ENABLE_A, 1); // enable + pinMode(ENABLE_B, OUTPUT); + digitalWrite(ENABLE_B, 1); // enable + pinMode(ENABLE_C, OUTPUT); + digitalWrite(ENABLE_C, 1); // enable + + +//... normal simpleFOC init code... + + +} + +``` + +You can use the driver's features. In general you can do this at any time, but certain features only make sense at setup-time (e.g. setting the PWM mode, which is handled automatically by the DRV8316Driver3PWM class, or setting the current limit, which you generally want to get done before applying power to the motor). + +Driver usage, for example reading and printing the complete status: + +```c++ + DRV8316Status status = driver.getStatus(); + Serial.println("DRV8316 Status:"); + Serial.print("Fault: "); + Serial.println(status.isFault()); + Serial.print("Buck Error: "); + Serial.print(status.isBuckError()); + Serial.print(" Undervoltage: "); + Serial.print(status.isBuckUnderVoltage()); + Serial.print(" OverCurrent: "); + Serial.println(status.isBuckOverCurrent()); + Serial.print("Charge Pump UnderVoltage: "); + Serial.println(status.isChargePumpUnderVoltage()); + Serial.print("OTP Error: "); + Serial.println(status.isOneTimeProgrammingError()); + Serial.print("OverCurrent: "); + Serial.print(status.isOverCurrent()); + Serial.print(" Ah: "); + Serial.print(status.isOverCurrent_Ah()); + Serial.print(" Al: "); + Serial.print(status.isOverCurrent_Al()); + Serial.print(" Bh: "); + Serial.print(status.isOverCurrent_Bh()); + Serial.print(" Bl: "); + Serial.print(status.isOverCurrent_Bl()); + Serial.print(" Ch: "); + Serial.print(status.isOverCurrent_Ch()); + Serial.print(" Cl: "); + Serial.println(status.isOverCurrent_Cl()); + Serial.print("OverTemperature: "); + Serial.print(status.isOverTemperature()); + Serial.print(" Shutdown: "); + Serial.print(status.isOverTemperatureShutdown()); + Serial.print(" Warning: "); + Serial.println(status.isOverTemperatureWarning()); + Serial.print("OverVoltage: "); + Serial.println(status.isOverVoltage()); + Serial.print("PowerOnReset: "); + Serial.println(status.isPowerOnReset()); + Serial.print("SPI Error: "); + Serial.print(status.isSPIError()); + Serial.print(" Address: "); + Serial.print(status.isSPIAddressError()); + Serial.print(" Clock: "); + Serial.print(status.isSPIClockFramingError()); + Serial.print(" Parity: "); + Serial.println(status.isSPIParityError()); + if (status.isFault()) + driver.clearFault(); + delayMicroseconds(1); // ensure 400ns delay + DRV8316_PWMMode val = driver.getPWMMode(); + Serial.print("PWM Mode: "); + Serial.println(val); + delayMicroseconds(1); // ensure 400ns delay + bool lock = driver.isRegistersLocked(); + Serial.print("Lock: "); + Serial.println(lock); +``` + + +Setting options can be conveniently done via the provided setter methods. All documented registers and options are available via the driver, and the option values can be accessed via enums. + +Leave at least 400ns delay between reading and/or writing options to ensure you don't talk to the DRV8316 too quickly: + +```c++ +driver.setPWM100Frequency(DRV8316_PWM100DUTY::FREQ_40KHz); +delayMicroseconds(1); // ensure 400ns delay +driver.setBuckVoltage(DRV8316_BuckVoltage::VB_5V7); +``` + + +### Current sensing + +TODO... + +### Current limiting + +TODO... + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp new file mode 100644 index 0000000..9586faa --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.cpp @@ -0,0 +1,586 @@ + + +#include "./drv8316.h" + + +void DRV8316Driver3PWM::init(SPIClass* _spi) { + DRV8316Driver::init(_spi); + setRegistersLocked(false); + delayMicroseconds(1); + DRV8316Driver::setPWMMode(DRV8316_PWMMode::PWM3_Mode); + BLDCDriver3PWM::init(); +}; + + +void DRV8316Driver6PWM::init(SPIClass* _spi) { + DRV8316Driver::init(_spi); + setRegistersLocked(false); + delayMicroseconds(1); + DRV8316Driver::setPWMMode(DRV8316_PWMMode::PWM6_Mode); // default mode is 6-PWM + BLDCDriver6PWM::init(); +}; + + + + + + +/* + * SPI setup: + * + * capture on falling, propagate on rising = + * MSB first + * + * 16 bit words + * outgoing: R/W:1 addr:6 parity:1 data:8 + * incoming: status:8 data:8 + * + * on reads, incoming data is content of register being read + * on writes, incomnig data is content of register being written + * + * + */ + +void handleInterrupt() { + +} + +void DRV8316Driver::init(SPIClass* _spi) { + // TODO make SPI speed configurable + spi = _spi; + settings = SPISettings(1000000, MSBFIRST, SPI_MODE1); + + //setup pins + pinMode(cs, OUTPUT); + digitalWrite(cs, HIGH); // switch off + + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + + if (_isset(nFault)) { + pinMode(nFault, INPUT); + // TODO add interrupt handler on the nFault pin if configured + // add configuration for how to handle faults... idea: interrupt handler calls a callback, depending on the type of fault + // consider what would be a useful configuration in practice? What do we want to do on a fault, e.g. over-temperature for example? + + //attachInterrupt(digitalPinToInterrupt(nFault), handleInterrupt, PinStatus::FALLING); + } +}; + + + + +bool DRV8316Driver::getParity(uint16_t data) { + //PARITY = XNOR(CMD, A5..A0, D7..D0) + uint8_t par = 0; + for (int i=0;i<16;i++) { + if (((data)>>i) & 0x0001) + par+=1; + } + return (par&0x01)==0x01; // even number of bits means true +} + + + + +uint16_t DRV8316Driver::readSPI(uint8_t addr) { + digitalWrite(cs, 0); + spi->beginTransaction(settings); + uint16_t data = (((addr<<1) | 0x80)<<8)|0x0000; + if (getParity(data)) + data |= 0x0100; + uint16_t result = spi->transfer16(data); + spi->endTransaction(); + digitalWrite(cs, 1); +// Serial.print("SPI Read Result: "); +// Serial.print(data, HEX); +// Serial.print(" -> "); +// Serial.println(result, HEX); + return result; +} + + +uint16_t DRV8316Driver::writeSPI(uint8_t addr, uint8_t value) { + digitalWrite(cs, 0); + spi->beginTransaction(settings); + uint16_t data = ((addr<<1)<<8)|value; + if (getParity(data)) + data |= 0x0100; + uint16_t result = spi->transfer16(data); + spi->endTransaction(); + digitalWrite(cs, 1); +// Serial.print("SPI Write Result: "); +// Serial.print(data, HEX); +// Serial.print(" -> "); +// Serial.println(result, HEX); + return result; +} + + + +DRV8316Status DRV8316Driver::getStatus() { + IC_Status data; + Status__1 data1; + Status__2 data2; + uint16_t result = readSPI(IC_Status_ADDR); + data.reg = (result & 0x00FF); + delayMicroseconds(1); // delay at least 400ns between operations + result = readSPI(Status__1_ADDR); + data1.reg = (result & 0x00FF); + delayMicroseconds(1); // delay at least 400ns between operations + result = readSPI(Status__2_ADDR); + data2.reg = (result & 0x00FF); + return DRV8316Status(data, data1, data2); +} + + + + + + + + +void DRV8316Driver::clearFault() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.CLR_FLT |= 1; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + + + + + + +bool DRV8316Driver::isRegistersLocked(){ + uint16_t result = readSPI(Control__1_ADDR); + Control__1 data; + data.reg = (result & 0x00FF); + return data.REG_LOCK==REG_LOCK_LOCK; +} +void DRV8316Driver::setRegistersLocked(bool lock){ + uint16_t result = readSPI(Control__1_ADDR); + Control__1 data; + data.reg = (result & 0x00FF); + data.REG_LOCK = lock?REG_LOCK_LOCK:REG_LOCK_UNLOCK; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__1_ADDR, data.reg); +} + + + +DRV8316_PWMMode DRV8316Driver::getPWMMode() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_PWMMode)data.PWM_MODE; +}; +void DRV8316Driver::setPWMMode(DRV8316_PWMMode pwmMode){ + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.PWM_MODE = pwmMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +DRV8316_Slew DRV8316Driver::getSlew() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_Slew)data.SLEW; +}; +void DRV8316Driver::setSlew(DRV8316_Slew slewRate) { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.SLEW = slewRate; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +DRV8316_SDOMode DRV8316Driver::getSDOMode() { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + return (DRV8316_SDOMode)data.SDO_MODE; +}; +void DRV8316Driver::setSDOMode(DRV8316_SDOMode sdoMode) { + uint16_t result = readSPI(Control__2_ADDR); + Control__2 data; + data.reg = (result & 0x00FF); + data.SDO_MODE = sdoMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__2_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isOvertemperatureReporting(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.OTW_REP==OTW_REP_ENABLE; +}; +void DRV8316Driver::setOvertemperatureReporting(bool reportFault){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OTW_REP = reportFault?OTW_REP_ENABLE:OTW_REP_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isSPIFaultReporting(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.SPI_FLT_REP==SPI_FLT_REP_ENABLE; +} +void DRV8316Driver::setSPIFaultReporting(bool reportFault){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.SPI_FLT_REP = reportFault?SPI_FLT_REP_ENABLE:SPI_FLT_REP_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +} + + + +bool DRV8316Driver::isOvervoltageProtection(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return data.OVP_EN==OVP_EN_ENABLE; +}; +void DRV8316Driver::setOvervoltageProtection(bool enabled){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OVP_EN = enabled?OVP_EN_ENABLE:OVP_EN_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_OVP DRV8316Driver::getOvervoltageLevel(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return (DRV8316_OVP)data.OVP_SEL; +}; +void DRV8316Driver::setOvervoltageLevel(DRV8316_OVP voltage){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.OVP_SEL = voltage; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_PWM100DUTY DRV8316Driver::getPWM100Frequency(){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + return (DRV8316_PWM100DUTY)data.PWM_100_DUTY_SEL; +}; +void DRV8316Driver::setPWM100Frequency(DRV8316_PWM100DUTY freq){ + uint16_t result = readSPI(Control__3_ADDR); + Control__3 data; + data.reg = (result & 0x00FF); + data.PWM_100_DUTY_SEL = freq; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__3_ADDR, data.reg); +}; + + + +DRV8316_OCPMode DRV8316Driver::getOCPMode(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPMode)data.OCP_MODE; + +}; +void DRV8316Driver::setOCPMode(DRV8316_OCPMode ocpMode){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_MODE = ocpMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPLevel DRV8316Driver::getOCPLevel(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPLevel)data.OCP_LVL; +}; +void DRV8316Driver::setOCPLevel(DRV8316_OCPLevel amps){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_LVL = amps; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPRetry DRV8316Driver::getOCPRetryTime(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPRetry)data.OCP_RETRY; +}; +void DRV8316Driver::setOCPRetryTime(DRV8316_OCPRetry ms){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_RETRY = ms; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_OCPDeglitch DRV8316Driver::getOCPDeglitchTime(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return (DRV8316_OCPDeglitch)data.OCP_DEG; +}; +void DRV8316Driver::setOCPDeglitchTime(DRV8316_OCPDeglitch ms){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_DEG = ms; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isOCPClearInPWMCycleChange(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return data.OCP_CBC==OCP_CBC_ENABLE; +}; +void DRV8316Driver::setOCPClearInPWMCycleChange(bool enable){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.OCP_CBC = enable?OCP_CBC_ENABLE:OCP_CBC_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isDriverOffEnabled(){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + return data.DRV_OFF==DRV_OFF_ENABLE; +}; +void DRV8316Driver::setDriverOffEnabled(bool enabled){ + uint16_t result = readSPI(Control__4_ADDR); + Control__4 data; + data.reg = (result & 0x00FF); + data.DRV_OFF = enabled?DRV_OFF_ENABLE:DRV_OFF_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__4_ADDR, data.reg); +}; + + + +DRV8316_CSAGain DRV8316Driver::getCurrentSenseGain(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return (DRV8316_CSAGain)data.CSA_GAIN; +}; +void DRV8316Driver::setCurrentSenseGain(DRV8316_CSAGain gain){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.CSA_GAIN = gain; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isActiveSynchronousRectificationEnabled(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return data.EN_ASR==EN_ASR_ENABLE; + +}; +void DRV8316Driver::setActiveSynchronousRectificationEnabled(bool enabled){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.EN_ASR = enabled?EN_ASR_ENABLE:EN_ASR_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isActiveAsynchronousRectificationEnabled(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return data.EN_AAR==EN_AAR_ENABLE; +}; +void DRV8316Driver::setActiveAsynchronousRectificationEnabled(bool enabled){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.EN_AAR = enabled?EN_AAR_ENABLE:EN_AAR_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +DRV8316_Recirculation DRV8316Driver::getRecirculationMode(){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + return (DRV8316_Recirculation)data.ILIM_RECIR; +}; +void DRV8316Driver::setRecirculationMode(DRV8316_Recirculation recirculationMode){ + uint16_t result = readSPI(Control__5_ADDR); + Control__5 data; + data.reg = (result & 0x00FF); + data.ILIM_RECIR = recirculationMode; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__5_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isBuckEnabled(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return data.BUCK_DIS==BUCK_DIS_BUCK_ENABLE; +}; +void DRV8316Driver::setBuckEnabled(bool enabled){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_DIS = enabled?BUCK_DIS_BUCK_ENABLE:BUCK_DIS_BUCK_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_BuckVoltage DRV8316Driver::getBuckVoltage(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return (DRV8316_BuckVoltage)data.BUCK_SEL; +}; +void DRV8316Driver::setBuckVoltage(DRV8316_BuckVoltage volts){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_SEL = volts; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_BuckCurrentLimit DRV8316Driver::getBuckCurrentLimit(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return (DRV8316_BuckCurrentLimit)data.BUCK_CL; +}; +void DRV8316Driver::setBuckCurrentLimit(DRV8316_BuckCurrentLimit mamps){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_CL = mamps; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isBuckPowerSequencingEnabled(){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + return data.BUCK_PS_DIS==BUCK_PS_DIS_ENABLE; + +}; +void DRV8316Driver::setBuckPowerSequencingEnabled(bool enabled){ + uint16_t result = readSPI(Control__6_ADDR); + Control__6 data; + data.reg = (result & 0x00FF); + data.BUCK_PS_DIS = enabled?BUCK_PS_DIS_ENABLE:BUCK_PS_DIS_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__6_ADDR, data.reg); +}; + + + +DRV8316_DelayTarget DRV8316Driver::getDelayTarget(){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + return (DRV8316_DelayTarget)data.DLY_TARGET; +}; +void DRV8316Driver::setDelayTarget(DRV8316_DelayTarget us){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + data.DLY_TARGET = us; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__10_ADDR, data.reg); +}; + + + +bool DRV8316Driver::isDelayCompensationEnabled(){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + return data.DLYCMP_EN==DLYCMP_EN_ENABLE; +}; +void DRV8316Driver::setDelayCompensationEnabled(bool enabled){ + uint16_t result = readSPI(Control__10_ADDR); + Control__10 data; + data.reg = (result & 0x00FF); + data.DLYCMP_EN = enabled?DLYCMP_EN_ENABLE:DLYCMP_EN_DISABLE; + delayMicroseconds(1); // delay at least 400ns between operations + result = writeSPI(Control__10_ADDR, data.reg); +}; + + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h new file mode 100644 index 0000000..dfec32e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316.h @@ -0,0 +1,316 @@ + + +#ifndef SIMPLEFOC_DRV8316 +#define SIMPLEFOC_DRV8316 + + +#include "Arduino.h" +#include +#include +#include + +#include "./drv8316_registers.h" + + +enum DRV8316_PWMMode { + PWM6_Mode = 0b00, + PWM6_CurrentLimit_Mode = 0b01, + PWM3_Mode = 0b10, + PWM3_CurrentLimit_Mode = 0b11 +}; + + +enum DRV8316_SDOMode { + SDOMode_OpenDrain = 0b0, + SDOMode_PushPull = 0b1 +}; + + +enum DRV8316_Slew { + Slew_25Vus = 0b00, + Slew_50Vus = 0b01, + Slew_150Vus = 0b10, + Slew_200Vus = 0b11 +}; + + +enum DRV8316_OVP { + OVP_SEL_32V = 0b0, + OVP_SEL_20V = 0b1 +}; + + +enum DRV8316_PWM100DUTY { + FREQ_20KHz = 0b0, + FREQ_40KHz = 0b1 +}; + + +enum DRV8316_OCPMode { + Latched_Fault = 0b00, + AutoRetry_Fault = 0b01, + ReportOnly = 0b10, + NoAction = 0b11 +}; + + +enum DRV8316_OCPLevel { + Curr_16A = 0b0, + Curr_24A = 0b1 +}; + + +enum DRV8316_OCPRetry { + Retry5ms = 0b0, + Retry500ms = 0b1 +}; + + +enum DRV8316_OCPDeglitch { + Deglitch_0us2 = 0b00, + Deglitch_0us6 = 0b01, + Deglitch_1us1 = 0b10, + Deglitch_1us6 = 0b11 +}; + + +enum DRV8316_CSAGain { + Gain_0V15 = 0b00, + Gain_0V1875 = 0b01, + Gain_0V25 = 0b10, + Gain_0V375 = 0b11 +}; + + +enum DRV8316_Recirculation { + BrakeMode = 0b00, // FETs + CoastMode = 0b01 // Diodes +}; + + +enum DRV8316_BuckVoltage { + VB_3V3 = 0b00, + VB_5V = 0b01, + VB_4V = 0b10, + VB_5V7 = 0b11 +}; + + +enum DRV8316_BuckCurrentLimit { + Limit_600mA = 0b00, + Limit_150mA = 0b01 +}; + + +enum DRV8316_DelayTarget { + Delay_0us = 0x0, + Delay_0us4 = 0x1, + Delay_0us6 = 0x2, + Delay_0us8 = 0x3, + Delay_1us = 0x4, + Delay_1us2 = 0x5, + Delay_1us4 = 0x6, + Delay_1us6 = 0x7, + Delay_1us8 = 0x8, + Delay_2us = 0x9, + Delay_2us2 = 0xA, + Delay_2us4 = 0xB, + Delay_2us6 = 0xC, + Delay_2us8 = 0xD, + Delay_3us = 0xE, + Delay_3us2 = 0xF +}; + + + +class DRV8316ICStatus { +public: + DRV8316ICStatus(IC_Status status) : status(status) {}; + ~DRV8316ICStatus() {}; + + bool isFault() { return status.FAULT==0b1; }; + bool isOverTemperature() { return status.OT==0b1; }; + bool isOverCurrent() { return status.OCP==0b1; }; + bool isOverVoltage() { return status.OVP==0b1; }; + bool isSPIError() { return status.SPI_FLT==0b1; }; + bool isBuckError() { return status.BK_FLT==0b1; }; + bool isPowerOnReset() { return status.NPOR==0b1; }; + + IC_Status status; +}; + + +class DRV8316Status1 { +public: + DRV8316Status1(Status__1 status1) : status1(status1) {}; + ~DRV8316Status1() {}; + + + bool isOverCurrent_Ah() { return status1.OCP_HA==0b1; }; + bool isOverCurrent_Al() { return status1.OCP_LA==0b1; }; + bool isOverCurrent_Bh() { return status1.OCP_HB==0b1; }; + bool isOverCurrent_Bl() { return status1.OCP_LB==0b1; }; + bool isOverCurrent_Ch() { return status1.OCP_HC==0b1; }; + bool isOverCurrent_Cl() { return status1.OCP_LC==0b1; }; + bool isOverTemperatureShutdown() { return status1.OTS==0b1; }; + bool isOverTemperatureWarning() { return status1.OTW==0b1; }; + + Status__1 status1; +}; + + +class DRV8316Status2 { +public: + DRV8316Status2(Status__2 status2) : status2(status2) {}; + ~DRV8316Status2() {}; + + + bool isOneTimeProgrammingError() { return status2.OTP_ERR==0b1; }; + bool isBuckOverCurrent() { return status2.BUCK_OCP==0b1; }; + bool isBuckUnderVoltage() { return status2.BUCK_UV==0b1; }; + bool isChargePumpUnderVoltage() { return status2.VCP_UV==0b1; }; + bool isSPIParityError() { return status2.SPI_PARITY==0b1; }; + bool isSPIClockFramingError() { return status2.SPI_SCLK_FLT==0b1; }; + bool isSPIAddressError() { return status2.SPI_ADDR_FLT==0b1; }; + + Status__2 status2; +}; + + + +class DRV8316Status : public DRV8316ICStatus, public DRV8316Status1, public DRV8316Status2 { + public: + DRV8316Status(IC_Status status, Status__1 status1, Status__2 status2) : DRV8316ICStatus(status), DRV8316Status1(status1), DRV8316Status2(status2) {}; + ~DRV8316Status() {}; +}; + + + + +class DRV8316Driver { + + public: + DRV8316Driver(int cs, bool currentLimit = false, int nFault = NOT_SET) : currentLimit(currentLimit), cs(cs), nFault(nFault), spi(&SPI), settings(1000000, MSBFIRST, SPI_MODE1) {}; + virtual ~DRV8316Driver() {}; + + virtual void init(SPIClass* _spi = &SPI); + + void clearFault(); // TODO check for fault condition methods + + DRV8316Status getStatus(); + + bool isRegistersLocked(); + void setRegistersLocked(bool lock); + + DRV8316_PWMMode getPWMMode(); + void setPWMMode(DRV8316_PWMMode pwmMode); + + DRV8316_Slew getSlew(); + void setSlew(DRV8316_Slew slewRate); + + DRV8316_SDOMode getSDOMode(); + void setSDOMode(DRV8316_SDOMode sdoMode); + + bool isOvertemperatureReporting(); + void setOvertemperatureReporting(bool reportFault); + + bool isSPIFaultReporting(); + void setSPIFaultReporting(bool reportFault); + + bool isOvervoltageProtection(); + void setOvervoltageProtection(bool enabled); + + DRV8316_OVP getOvervoltageLevel(); + void setOvervoltageLevel(DRV8316_OVP voltage); + + DRV8316_PWM100DUTY getPWM100Frequency(); + void setPWM100Frequency(DRV8316_PWM100DUTY freq); + + DRV8316_OCPMode getOCPMode(); + void setOCPMode(DRV8316_OCPMode ocpMode); + + DRV8316_OCPLevel getOCPLevel(); + void setOCPLevel(DRV8316_OCPLevel amps); + + DRV8316_OCPRetry getOCPRetryTime(); + void setOCPRetryTime(DRV8316_OCPRetry ms); + + DRV8316_OCPDeglitch getOCPDeglitchTime(); + void setOCPDeglitchTime(DRV8316_OCPDeglitch ms); + + bool isOCPClearInPWMCycleChange(); + void setOCPClearInPWMCycleChange(bool enable); + + bool isDriverOffEnabled(); + void setDriverOffEnabled(bool enabled); + + DRV8316_CSAGain getCurrentSenseGain(); + void setCurrentSenseGain(DRV8316_CSAGain gain); + + bool isActiveSynchronousRectificationEnabled(); + void setActiveSynchronousRectificationEnabled(bool enabled); + + bool isActiveAsynchronousRectificationEnabled(); + void setActiveAsynchronousRectificationEnabled(bool enabled); + + DRV8316_Recirculation getRecirculationMode(); + void setRecirculationMode(DRV8316_Recirculation recirculationMode); + + bool isBuckEnabled(); + void setBuckEnabled(bool enabled); + + DRV8316_BuckVoltage getBuckVoltage(); + void setBuckVoltage(DRV8316_BuckVoltage volts); + + DRV8316_BuckCurrentLimit getBuckCurrentLimit(); + void setBuckCurrentLimit(DRV8316_BuckCurrentLimit mamps); + + bool isBuckPowerSequencingEnabled(); + void setBuckPowerSequencingEnabled(bool enabled); + + DRV8316_DelayTarget getDelayTarget(); + void setDelayTarget(DRV8316_DelayTarget us); + + bool isDelayCompensationEnabled(); + void setDelayCompensationEnabled(bool enabled); + + private: + uint16_t readSPI(uint8_t addr); + uint16_t writeSPI(uint8_t addr, uint8_t data); + bool getParity(uint16_t data); + + bool currentLimit; + int cs; + int nFault; + SPIClass* spi; + SPISettings settings; +}; + + + +class DRV8316Driver3PWM : public DRV8316Driver, public BLDCDriver3PWM { + + public: + DRV8316Driver3PWM(int phA,int phB,int phC, int cs, bool currentLimit = false, int en = NOT_SET, int nFault = NOT_SET) : + DRV8316Driver(cs, currentLimit, nFault), BLDCDriver3PWM(phA, phB, phC, en) { enable_active_high=false; }; + virtual ~DRV8316Driver3PWM() {}; + + virtual void init(SPIClass* _spi = &SPI) override; + +}; + + + +class DRV8316Driver6PWM : public DRV8316Driver, public BLDCDriver6PWM { + + public: + DRV8316Driver6PWM(int phA_h,int phA_l,int phB_h,int phB_l,int phC_h,int phC_l, int cs, bool currentLimit = false, int en = NOT_SET, int nFault = NOT_SET) : + DRV8316Driver(cs, currentLimit, nFault), BLDCDriver6PWM(phA_h, phA_l, phB_h, phB_l, phC_h, phC_l, en) { enable_active_high=false; }; + virtual ~DRV8316Driver6PWM() {}; + + virtual void init(SPIClass* _spi = &SPI) override; + +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h new file mode 100644 index 0000000..04bca1e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/drv8316/drv8316_registers.h @@ -0,0 +1,184 @@ + + +#ifndef SIMPLEFOC_DRV8316_REGISTERS +#define SIMPLEFOC_DRV8316_REGISTERS + + +#define IC_Status_ADDR 0x0 +#define Status__1_ADDR 0x1 +#define Status__2_ADDR 0x2 +#define Control__1_ADDR 0x3 +#define Control__2_ADDR 0x4 +#define Control__3_ADDR 0x5 +#define Control__4_ADDR 0x6 +#define Control__5_ADDR 0x7 +#define Control__6_ADDR 0x8 +#define Control__10_ADDR 0xC + +#define REG_LOCK_LOCK 0b110 +#define REG_LOCK_UNLOCK 0b011 +#define CLR_FAULT_CLR 0b1 +#define OTW_REP_ENABLE 0b1 +#define OTW_REP_DISABLE 0b0 +#define SPI_FLT_REP_ENABLE 0b0 +#define SPI_FLT_REP_DISABLE 0b1 +#define OVP_EN_ENABLE 0b1 +#define OVP_EN_DISABLE 0b0 +#define OCP_CBC_ENABLE 0b1 +#define OCP_CBC_DISABLE 0b0 +#define DRV_OFF_ENABLE 0b1 +#define DRV_OFF_DISABLE 0b0 +#define EN_ASR_ENABLE 0b1 +#define EN_ASR_DISABLE 0b0 +#define EN_AAR_ENABLE 0b1 +#define EN_AAR_DISABLE 0b0 +#define BUCK_DIS_BUCK_DISABLE 0b1 +#define BUCK_DIS_BUCK_ENABLE 0b0 +#define BUCK_PS_DIS_DISABLE 0b1 +#define BUCK_PS_DIS_ENABLE 0b0 +#define DLYCMP_EN_ENABLE 0b1 +#define DLYCMP_EN_DISABLE 0b0 + + + + +typedef union { + struct { + uint8_t REG_LOCK:3; + uint8_t :5; + }; + uint8_t reg; +} Control__1; + + +typedef union { + struct { + uint8_t CLR_FLT:1; + uint8_t PWM_MODE:2; + uint8_t SLEW:2; + uint8_t SDO_MODE:1; + uint8_t :2; + }; + uint8_t reg; +} Control__2; + + +typedef union { + struct { + uint8_t OTW_REP:1; + uint8_t SPI_FLT_REP:1; + uint8_t OVP_EN:1; + uint8_t OVP_SEL:1; + uint8_t PWM_100_DUTY_SEL:1; + uint8_t :3; + }; + uint8_t reg; +} Control__3; + + +typedef union { + struct { + uint8_t OCP_MODE:2; + uint8_t OCP_LVL:1; + uint8_t OCP_RETRY:1; + uint8_t OCP_DEG:2; + uint8_t OCP_CBC:1; + uint8_t DRV_OFF:1; + }; + uint8_t reg; +} Control__4; + + + + +typedef union { + struct { + uint8_t CSA_GAIN:2; + uint8_t EN_ASR:1; + uint8_t EN_AAR:1; + uint8_t :2; + uint8_t ILIM_RECIR:1; + uint8_t :1; + }; + uint8_t reg; +} Control__5; + + + + +typedef union { + struct { + uint8_t BUCK_DIS:1; + uint8_t BUCK_SEL:2; + uint8_t BUCK_CL:1; + uint8_t BUCK_PS_DIS:1; + uint8_t :2; + }; + uint8_t reg; +} Control__6; + + + + +typedef union { + struct { + uint8_t DLY_TARGET:4; + uint8_t DLYCMP_EN:1; + uint8_t :3; + }; + uint8_t reg; +} Control__10; + + + + +typedef union { + struct { + uint8_t FAULT:1; + uint8_t OT:1; + uint8_t OVP:1; + uint8_t NPOR:1; + uint8_t OCP:1; + uint8_t SPI_FLT:1; + uint8_t BK_FLT:1; + uint8_t :1; + }; + uint8_t reg; +} IC_Status; + + + + +typedef union { + struct { + uint8_t OCP_LA:1; + uint8_t OCP_HA:1; + uint8_t OCP_LB:1; + uint8_t OCP_HB:1; + uint8_t OCP_LC:1; + uint8_t OCP_HC:1; + uint8_t OTS:1; + uint8_t OTW:1; + }; + uint8_t reg; +} Status__1; + + + + +typedef union { + struct { + uint8_t SPI_ADDR_FLT:1; + uint8_t SPI_SCLK_FLT:1; + uint8_t SPI_PARITY:1; + uint8_t VCP_UV:1; + uint8_t BUCK_UV:1; + uint8_t BUCK_OCP:1; + uint8_t OTP_ERR:1; + uint8_t :1; + }; + uint8_t reg; +} Status__2; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/README.md new file mode 100644 index 0000000..b34ba7a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/README.md @@ -0,0 +1,147 @@ + +# TMC6200 SimpleFOC Driver + +by [@YaseenTwati](https://github.com/YaseenTwati) + +The TMC6200 is a Universal high voltage BLDC/PMSM/Servo MOSFET 3-halfbridge gate-driver with in line motor current +sensing. External MOSFETs for up to 100A motor current. + +See https://www.trinamic.com/fileadmin/assets/Products/ICs_Documents/TMC6200_datasheet_Rev1.05.pdf for more information. + +## Hardware setup + +To use the TMC6200 you will have to connect the following: + +- GND +- VCC_IO - 3.3V or 5V +- SPE - pull up to VCC to enable SPI mode +- SPI MOSI ( SDI ) +- SPI MISO ( SDO ) +- SPI CLK +- SPI nCS +- DRV_EN - connect to digital out (or pull up to VCC) +- UH - connect to motor PWM pin +- VH - connect to motor PWM pin +- WH - connect to motor PWM pin +- UL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- VL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation +- WL - connect to motor PWM pin for 6-PWM, or to digital out (or pull up to VCC) for 3-PWM operation + +Optionally, but useful: + +- FAULT - digital in, active high + +For current sensing: + +- CURU - connect to analog in +- CURV - connect to analog in +- CURW - connect to analog in + +## Usage + +Usage is quite easy, especially if you already know SimpleFOC. See also the [examples](https://github.com/simplefoc/Arduino-FOC-drivers/examples/drivers/drv8316/) + +```c++ +#include "Arduino.h" +#include +#include "SimpleFOCDrivers.h" +#include "drivers/tmc6200/TMC6200.hpp" + +BLDCMotor motor = BLDCMotor(15); +TMC6200Driver6PWM driver = DRV8316Driver6PWM(UH, UL, VH, VL, WH, WL, nCS, DRV_EN); + +//... normal simpleFOC init code... +``` + +Or, for 3-PWM: + +```c++ +#include "Arduino.h" +#include +#include "SimpleFOCDrivers.h" +#include "drivers/tmc6200/TMC6200.hpp" + +BLDCMotor motor = BLDCMotor(15); +TMC6200Driver3PWM driver = TMC6200Driver3PWM(UH, MOTOR_VH, MOTOR_WH, nCS, DRV_EN); + +void setup() { + + pinMode(UL, OUTPUT); + pinMode(VL, OUTPUT); + pinMode(WL, OUTPUT); + + digitalWrite(WL, HIGH); + digitalWrite(UL, HIGH); + digitalWrite(VL, HIGH); + + //... normal simpleFOC init code... +} + +``` + +### Validating the SPI Connection +You can validate the SPI connection by checking the value of VERSION field in IOIN register. The value should be 0x10. + +```c++ + if(driver.getInputs().VERSION != TMC6200_VERSION){ + // something is wrong with the spi connection + } +``` + +### Current Sensing +The gain of the internal current amplifiers can be set to 5, 10 or 20 through `setCurrentSenseGain()` + +```c++ + driver.setCurrentSenseGain(TMC6200_AmplificationGain::_5); + //driver.setCurrentSenseGain(TMC6200_AmplificationGain::_10); + //driver.setCurrentSenseGain(TMC6200_AmplificationGain::_20); +``` +The sense amplifiers can also be turned off ( they are on by default ), through `setCurrentSenseAmplifierState()` + +```c++ + driver.setCurrentSenseAmplifierState(false); +``` +### Driver Strength +The strength of the mosfet drivers can be controlled through `setDriverStrength()` + +```c++ + driver.setDriverStrength(TMC6200_DRVStrength::Weak); + //driver.setDriverStrength(TMC6200_DRVStrength::WeakTC); // (medium above OTPW level) + //driver.setDriverStrength(TMC6200_DRVStrength::Medium); + //driver.setDriverStrength(TMC6200_DRVStrength::Strong); +``` + +### Handling Faults +The fault line will go high if a fault occurs such as a short, an interrupt can be used to handle it. +Note that some faults will disable the driver and will require the DRV_EN to be cycled to clear the fault. + +```c++ + // somewhere in setup + attachInterrupt(digitalPinToInterrupt(FAULT), handleFault, RISING); +``` + +```c++ + void handleFault() + { + // you can read the status register to see what happened + TMC6200GStatus status = driver.getStatus(); + Serial.print("hasUShorted: "); Serial.println(status.hasUShorted()); + Serial.print("hasVShorted: "); Serial.println(status.hasVShorted()); + Serial.print("hasWShorted: "); Serial.println(status.hasWShorted()); + Serial.print("isUShortedToGround: "); Serial.println(status.isUShortedToGround()); + Serial.print("isUShortedToSupply: "); Serial.println(status.isUShortedToSupply()); + Serial.print("isVShortedToGround: "); Serial.println(status.isVShortedToGround()); + Serial.print("isVShortedToSupply: "); Serial.println(status.isVShortedToSupply()); + Serial.print("isWShortedToGround: "); Serial.println(status.isWShortedToGround()); + Serial.print("isWShortedToSupply: "); Serial.println(status.isWShortedToSupply()); + Serial.print("isOverTemperaturePreWarning: "); Serial.println(status.isOverTemperaturePreWarning()); + Serial.print("isChargePumpUnderVoltage: "); Serial.println(status.isChargePumpUnderVoltage()); + + // the driver must be cycled to clear the fault + digitalWrite(DRV_EN, LOW); + delayMicrosockets(someSmallDelay); + digitalWrite(DRV_EN, HIGH); + } +``` + +The driver provides other features such as controlling the tolerences of short detection and the BBM cycle time and so on, setting the options can be conveniently done via the provided setter methods. All documented registers and options are available via the driver, and the option values can be accessed via enums. diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp new file mode 100644 index 0000000..659ab5d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.cpp @@ -0,0 +1,221 @@ +#include "TMC6200.hpp" + +void TMC6200Driver::init(SPIClass *_spi) { + spi = _spi; + settings = SPISettings(500000, MSBFIRST, SPI_MODE3); + + pinMode(csPin, OUTPUT); + digitalWrite(csPin, HIGH); +} + +// TMC6200_GCONF ------ + +void TMC6200Driver::setDriverState(bool state) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.DISABLE = state ? 0 : 1; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setPWMMode(TMC6200_PWMMode pwmMode) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.SINGLE_LINE = pwmMode; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setFaultDirect(TMC6200_FaultDirect faultDirect) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.FAULT_DIRECT = faultDirect; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setCurrentSenseAmplifierState(bool state) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.AMPLIFIER_OFF = state ? 0 : 1; + writeRegister(TMC6200_GCONF_REG, gConf.reg); +} + +void TMC6200Driver::setCurrentSenseGain(TMC6200_AmplificationGain amplificationGain) { + TMC6200_GCONF gConf; + gConf.reg = readRegister(TMC6200_GCONF_REG); + + gConf.AMPLIFICATION = amplificationGain; + writeRegister(TMC6200_GCONF_REG, gConf.reg); + + gConf.reg = readRegister(TMC6200_GCONF_REG); +} + +// TMC6200_DRV_CONF ------ + +void TMC6200Driver::setOverTemperatureThreshold(TMC6200_OTSelect otLevel) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.OT_SELECT = otLevel; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +void TMC6200Driver::setDriverStrength(TMC6200_DRVStrength strength) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.DRV_STRENGTH = strength; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +void TMC6200Driver::setBBMCycles(uint8_t clockCycles) { + TMC6200_DRV_CONF drvConf; + drvConf.reg = readRegister(TMC6200_DRV_CONF_REG); + + drvConf.BBMCLKS = clockCycles; + writeRegister(TMC6200_DRV_CONF_REG, drvConf.reg); +} + +// TMC6200_SHORT_CONF ------ + +void TMC6200Driver::setShortDelay(TMC6200_ShortDelay shortDelay) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.SHORT_DELAY = shortDelay; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::shortFilter(TMC6200_ShortFilter shortFilter) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.SHORT_FILTER = shortFilter; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToSupplySensitivityLevel(uint8_t level) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.S2VS_LEVEL = level; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToGroundSensitivityLevel(uint8_t level) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.S2G_LEVEL = level; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortRetries(uint8_t retries) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.RETRY = retries; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setParallelProtect(TMC6200_ParallelProtect parallelProtect) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.PROTECT_PARALLEL = parallelProtect; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToGroundDetectionState(bool state) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.DISABLE_S2G = state ? 0 : 1; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +void TMC6200Driver::setShortToSupplyDetectionState(bool state) { + TMC6200_SHORT_CONF shortConf; + shortConf.reg = readRegister(TMC6200_SHORT_CONF_REG); + + shortConf.DISABLE_S2VS = state ? 0 : 1; + writeRegister(TMC6200_SHORT_CONF_REG, shortConf.reg); +} + +// TMC6200_IOIN ------ + +TMC6200_IOIN TMC6200Driver::getInputs() { + TMC6200_IOIN ioin; + ioin.reg = readRegister(TMC6200_IOIN_REG); + + return ioin; +} + +// TMC6200_GSTAT ------ + +TMC6200GStatus TMC6200Driver::getStatus() { + TMC6200_GSTAT gstat; + gstat.reg = readRegister(TMC6200_GSTAT_REG); + + return {gstat}; +} + +void TMC6200Driver::setStatus(TMC6200_GSTAT status) { + writeRegister(TMC6200_GSTAT_REG, status.reg); +} + +uint32_t TMC6200Driver::readRegister(uint8_t addr) { + digitalWrite(csPin, LOW); + spi->beginTransaction(settings); + + // Address + spi->transfer(TMC_ADDRESS(addr)); + + // 32bit Data + uint32_t value = 0; + value |= (spi->transfer(0x00) << 24); + value |= (spi->transfer(0x00) << 16); + value |= (spi->transfer(0x00) << 8); + value |= (spi->transfer(0x00) << 0); + + spi->end(); + digitalWrite(csPin, HIGH); + + return value; +} + +void TMC6200Driver::writeRegister(uint8_t addr, uint32_t data) { + digitalWrite(csPin, LOW); + spi->beginTransaction(settings); + + // Address + spi->transfer(addr | TMC_WRITE_BIT); + + // 32bit Data + spi->transfer(0xFF & (data >> 24)); + spi->transfer(0xFF & (data >> 16)); + spi->transfer(0xFF & (data >> 8)); + spi->transfer(0xFF & (data >> 0)); + + spi->end(); + digitalWrite(csPin, HIGH); +} + +// -------------------- + +void TMC6200Driver3PWM::init(SPIClass *_spi) { + TMC6200Driver::init(_spi); + delayMicroseconds(1); + TMC6200Driver::setPWMMode(TMC6200_PWMMode::SingleLine); + BLDCDriver3PWM::init(); +}; + + +void TMC6200Driver6PWM::init(SPIClass *_spi) { + TMC6200Driver::init(_spi); + delayMicroseconds(1); + TMC6200Driver::setPWMMode(TMC6200_PWMMode::Individual); + BLDCDriver6PWM::init(); +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp new file mode 100644 index 0000000..7a5215c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include "TMC6200_Registers.hpp" + +#define TMC6200_VERSION 0x10 + +enum TMC6200_PWMMode { + Individual = 0, // 6PWM + SingleLine = 1, // 3PWM +}; + +enum TMC6200_AmplificationGain { + _5 = 0, + _10 = 1, + _Also_10 = 2, // maybe just remove this + _20 = 3, +}; + +enum TMC6200_ShortDelay { + _750nS = 0, + _1500nS = 1, +}; + +enum TMC6200_ShortFilter { + _100nS = 0, + _1uS = 1, + _2uS = 2, + _3uS = 3, +}; + +enum TMC6200_ParallelProtect { + Detected = 0, + All = 1, +}; + +enum TMC6200_FaultDirect { + AtLeastOneBridgeDown = 0, + EachProtectiveAction = 1, +}; + +enum TMC6200_OTSelect { + _150 = 0b00, + _143 = 0b01, + _136 = 0b10, + _120 = 0b11, +}; + +enum TMC6200_DRVStrength { + Weak = 0b00, + WeakTC = 0b01, // (medium above OTPW level) + Medium = 0b10, + Strong = 0b11, +}; + +class TMC6200GStatus { +public: + TMC6200GStatus(TMC6200_GSTAT status) : status(status) {}; + + bool isReset() const { return status.RESET == 0b1; }; + + bool isOverTemperaturePreWarning() const { return status.DRV_OTPW == 0b1; }; + + bool isOverTemperature() const { return status.DRV_OT == 0b1; }; + + bool isChargePumpUnderVoltage() const { return status.UV_CP == 0b1; }; + + bool hasUShorted() const { return status.SHORT_DET_U == 0b1; }; + + bool hasVShorted() const { return status.SHORT_DET_V == 0b1; }; + + bool hasWShorted() const { return status.SHORT_DET_W == 0b1; }; + + bool isUShortedToGround() const { return status.S2GU == 0b1; }; + + bool isUShortedToSupply() const { return status.S2VSU == 0b1; }; + + bool isVShortedToGround() const { return status.S2GV == 0b1; }; + + bool isVShortedToSupply() const { return status.S2VSV == 0b1; }; + + bool isWShortedToGround() const { return status.S2GW == 0b1; }; + + bool isWShortedToSupply() const { return status.S2VSW == 0b1; }; + + TMC6200_GSTAT status; +}; + +class TMC6200Driver { + +public: + TMC6200Driver(int csPin) : csPin(csPin), spi(&SPI), settings(1000000, MSBFIRST, SPI_MODE3) {}; + + virtual ~TMC6200Driver() {}; + + virtual void init(SPIClass *_spi); + + void setDriverState(bool state); + + void setPWMMode(TMC6200_PWMMode pwmMode); + + void setFaultDirect(TMC6200_FaultDirect faultDirect); + + void setCurrentSenseGain(TMC6200_AmplificationGain amplificationGain); + + void setOverTemperatureThreshold(TMC6200_OTSelect otLevel); + + void setDriverStrength(TMC6200_DRVStrength strength); + + void setCurrentSenseAmplifierState(bool state); + + void setShortDelay(TMC6200_ShortDelay shortDelay); + + void shortFilter(TMC6200_ShortFilter shortFilter); + + void setShortToSupplySensitivityLevel(uint8_t level); + + void setShortToGroundSensitivityLevel(uint8_t level); + + void setShortRetries(uint8_t retries); + + void setParallelProtect(TMC6200_ParallelProtect parallelProtect); + + void setShortToGroundDetectionState(bool state); + + void setShortToSupplyDetectionState(bool state); + + void setBBMCycles(uint8_t clockCycles); + + void setStatus(TMC6200_GSTAT status); + + TMC6200GStatus getStatus(); + + TMC6200_IOIN getInputs(); + +private: + uint32_t readRegister(uint8_t addr); + + void writeRegister(uint8_t addr, uint32_t data); + + int csPin; + SPIClass *spi; + SPISettings settings; +}; + + +class TMC6200Driver3PWM : public TMC6200Driver, public BLDCDriver3PWM { + +public: + TMC6200Driver3PWM(int phA, int phB, int phC, int cs, int en = NOT_SET) : + TMC6200Driver(cs), BLDCDriver3PWM(phA, phB, phC, en) {}; + + ~TMC6200Driver3PWM() override = default; + + virtual void init(SPIClass *_spi = &SPI) override; +}; + + +class TMC6200Driver6PWM : public TMC6200Driver, public BLDCDriver6PWM { + +public: + TMC6200Driver6PWM(int phA_h, int phA_l, int phB_h, int phB_l, int phC_h, int phC_l, int cs, int en = NOT_SET) : + TMC6200Driver(cs), BLDCDriver6PWM(phA_h, phA_l, phB_h, phB_l, phC_h, phC_l, en) {}; + + ~TMC6200Driver6PWM() override = default; + + virtual void init(SPIClass *_spi = &SPI) override; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp new file mode 100644 index 0000000..aff9fed --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/drivers/tmc6200/TMC6200_Registers.hpp @@ -0,0 +1,111 @@ +#pragma once + +// ------ + +#define TMC_WRITE_BIT 0x80 +#define TMC_ADDRESS_MASK 0x7F +#define TMC_ADDRESS(x) ((x) & (TMC_ADDRESS_MASK)) + +// ------ + +#define TMC6200_GCONF_REG 0x00 // RW +#define TMC6200_GSTAT_REG 0x01 // RW + WC +#define TMC6200_IOIN_REG 0x04 // R + +#define TMC6200_OTP_PROG_REG 0x06 // not used +#define TMC6200_OTP_READ_REG 0x07 // not used +#define TMC6200_FACTORY_CONF_REG 0x08 // not used + +#define TMC6200_SHORT_CONF_REG 0x09 // RW +#define TMC6200_DRV_CONF_REG 0x0A // RW + +typedef union { + struct { + uint32_t DISABLE: 1; + uint32_t SINGLE_LINE: 1; + uint32_t FAULT_DIRECT: 1; + uint32_t : 1; + uint32_t AMPLIFICATION: 2; + uint32_t AMPLIFIER_OFF: 1; + uint32_t TMC6200_TEST_MODE: 1; + uint32_t : 24; + }; + uint32_t reg; +} TMC6200_GCONF; + +typedef union { + struct { + uint32_t RESET: 1; + uint32_t DRV_OTPW: 1; + uint32_t DRV_OT: 1; + uint32_t UV_CP: 1; + + uint32_t SHORT_DET_U: 1; + uint32_t S2GU: 1; + uint32_t S2VSU: 1; + uint32_t : 1; + + uint32_t SHORT_DET_V: 1; + uint32_t S2GV: 1; + uint32_t S2VSV: 1; + uint32_t : 1; + + uint32_t SHORT_DET_W: 1; + uint32_t S2GW: 1; + uint32_t S2VSW: 1; + uint32_t : 1; + }; + uint32_t reg; +} TMC6200_GSTAT; + +typedef union { + struct { + unsigned UL: 1; + uint32_t UH: 1; + uint32_t VL: 1; + uint32_t VH: 1; + uint32_t WL: 1; + uint32_t WH: 1; + uint32_t DRV_EN: 1; + uint32_t _reserved1: 1; + uint32_t OTPW: 1; + uint32_t OT136C: 1; + uint32_t OT143C: 1; + uint32_t OT150C: 1; + uint32_t _reserved2: 12; + uint32_t VERSION: 8; + }; + + uint32_t reg; +} TMC6200_IOIN; + +typedef union { + struct { + uint32_t S2VS_LEVEL: 4; + uint32_t : 4; + uint32_t S2G_LEVEL: 4; + uint32_t : 4; + uint32_t SHORT_FILTER: 2; + uint32_t : 2; + uint32_t SHORT_DELAY: 1; + uint32_t : 4; + uint32_t RETRY: 2; + uint32_t : 2; + uint32_t PROTECT_PARALLEL: 1; + uint32_t DISABLE_S2G: 1; + uint32_t DISABLE_S2VS: 1; + uint32_t : 2; + }; + uint32_t reg; +} TMC6200_SHORT_CONF; + +typedef union { + struct { + uint32_t BBMCLKS: 5; + uint32_t : 11; + uint32_t OT_SELECT: 2; + uint32_t DRV_STRENGTH: 2; + uint32_t : 12; + }; + uint32_t reg; +} TMC6200_DRV_CONF; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp new file mode 100644 index 0000000..85a4e11 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.cpp @@ -0,0 +1,54 @@ + +#include "./A1334.h" + + + +A1334::A1334(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + + + +A1334::~A1334() { +}; + + + + +void A1334::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +}; + + + + +A1334Angle A1334::readRawAngle() { + uint16_t command = A1334_REG_ANG; + uint16_t cmdResult = spi_transfer16(command); // TODO fast mode + cmdResult = spi_transfer16(command); + A1334Angle result = { .reg = cmdResult }; + // TODO check parity + // errorflag = result.ef; + return result; +}; + + + + +uint16_t A1334::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + return result; +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.h new file mode 100644 index 0000000..798dcc4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/A1334.h @@ -0,0 +1,51 @@ + + +#ifndef __A1334_H__ +#define __A1334_H__ + +#include +#include + + +#define A1334_CPR 4096 +#define A1334_BITORDER MSBFIRST + +static SPISettings A1334SPISettings(8000000, A1334_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + + +typedef union { + struct { + uint16_t angle:12; + uint16_t p:1; + uint16_t nf:1; + uint16_t ef:1; + uint16_t ridc:1; + }; + uint16_t reg; +} A1334Angle; + + +#define A1334_REG_ANG 0x2000 + + + + +class A1334 { +public: + A1334(SPISettings settings = A1334SPISettings, int nCS = -1); + virtual ~A1334(); + + virtual void init(SPIClass* _spi = &SPI); + A1334Angle readRawAngle(); // 10 or 12 bit angle value +protected: + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + int nCS = -1; +}; + + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp new file mode 100644 index 0000000..c793947 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.cpp @@ -0,0 +1,27 @@ + +#include "./MagneticSensorA1334.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorA1334::MagneticSensorA1334(int nCS, SPISettings settings) : A1334(settings, nCS) { + +} + + +MagneticSensorA1334::~MagneticSensorA1334(){ + +} + + +void MagneticSensorA1334::init(SPIClass* _spi) { + this->A1334::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorA1334::getSensorAngle() { + A1334Angle angle_data = readRawAngle(); + float result = ( angle_data.angle / (float)A1334_CPR) * _2PI; + // return the shaft angle + return result; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h new file mode 100644 index 0000000..59ae961 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/a1334/MagneticSensorA1334.h @@ -0,0 +1,24 @@ + +#ifndef __MAGNETIC_SENSOR_A1334_H__ +#define __MAGNETIC_SENSOR_A1334_H__ + +#include "common/base_classes/Sensor.h" +#include "./A1334.h" + + + +class MagneticSensorA1334 : public Sensor, public A1334 { +public: + MagneticSensorA1334(int nCS = -1, SPISettings settings = A1334SPISettings); + virtual ~MagneticSensorA1334(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp new file mode 100644 index 0000000..8be3bd5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.cpp @@ -0,0 +1,102 @@ + +#include "AEAT8800Q24.h" + + +AEAT8800Q24::AEAT8800Q24(int nCS, int pinNSL, SPISettings spiSettings, SPISettings ssiSettings) : nCS(nCS), pinNSL(pinNSL), spiSettings(spiSettings), ssiSettings(ssiSettings) { +}; +AEAT8800Q24::~AEAT8800Q24(){ +}; + +void AEAT8800Q24::init(SPIClass* _spi){ + spi = _spi; + spi->beginTransaction(ssiSettings); +}; + + +float AEAT8800Q24::getCurrentAngle() { + return ((float)readRawAngle())/(float)AEAT8800Q24_CPR * 2.0f * (float)PI; +}; // angle in radians, return current value + + +// reads angle via ssi +uint16_t AEAT8800Q24::readRawAngle() { + // digitalWrite(pinNSL, LOW); //TODO maybe we don't need it... + // // 300ns delay + // delayMicroseconds(1); + uint16_t value = spi->transfer16(0x0000); + uint8_t status = spi->transfer(0x00); + //digitalWrite(pinNSL, HIGH); + // // 200ns delay before next read... + // delayMicroseconds(1); + lastStatus.reg = status; + return value; +}; + +AEAT8800Q24_Status_t AEAT8800Q24::getLastStatus() { + return lastStatus; +} + +uint16_t AEAT8800Q24::getZero(){ + uint8_t value = readRegister(AEAT8800Q24_REG_ZERO_MSB); + return (value<<8)|readRegister(AEAT8800Q24_REG_ZERO_LSB); +}; +void AEAT8800Q24::setZero(uint16_t value){ + writeRegister(AEAT8800Q24_REG_ZERO_MSB, (value>>8)&0xFF); + writeRegister(AEAT8800Q24_REG_ZERO_LSB, value&0xFF); +}; + +AEAT8800Q24_CONF0_t AEAT8800Q24::getConf0(){ + AEAT8800Q24_CONF0_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF0); + return result; +}; +void AEAT8800Q24::setConf0(AEAT8800Q24_CONF0_t value){ + writeRegister(AEAT8800Q24_REG_CONF0, value.reg); +}; + +AEAT8800Q24_CONF1_t AEAT8800Q24::getConf1(){ + AEAT8800Q24_CONF1_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF1); + return result; +}; +void AEAT8800Q24::setConf1(AEAT8800Q24_CONF1_t value){ + writeRegister(AEAT8800Q24_REG_CONF1, value.reg); +}; + +AEAT8800Q24_CONF2_t AEAT8800Q24::getConf2(){ + AEAT8800Q24_CONF2_t result; + result.reg = readRegister(AEAT8800Q24_REG_CONF2); + return result; +}; +void AEAT8800Q24::setConf2(AEAT8800Q24_CONF2_t value){ + writeRegister(AEAT8800Q24_REG_CONF2, value.reg); +}; + + + + +uint16_t AEAT8800Q24::transfer16SPI(uint16_t outValue) { + // delay 1us between switching the CS line to SPI + delayMicroseconds(1); + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->endTransaction(); + spi->beginTransaction(spiSettings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + // delay 1us between switching the CS line to SSI + delayMicroseconds(1); + spi->beginTransaction(ssiSettings); + return value; +}; +uint8_t AEAT8800Q24::readRegister(uint8_t reg) { + uint16_t cmd = 0x8000 | ((reg&0x001F)<<8); + uint16_t value = transfer16SPI(cmd); + return value&0x00FF; +}; +void AEAT8800Q24::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x4000 | ((reg&0x1F)<<8) | value; + /*uint16_t result =*/ transfer16SPI(cmd); +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h new file mode 100644 index 0000000..4acfe6d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/AEAT8800Q24.h @@ -0,0 +1,119 @@ +#ifndef __AEAT8800Q24_H__ +#define __AEAT8800Q24_H__ + + +#include "Arduino.h" +#include "SPI.h" + +#define AEAT8800Q24_CPR 65536 + +#define AEAT8800Q24_REG_CUST0 0x00 +#define AEAT8800Q24_REG_CUST1 0x01 +#define AEAT8800Q24_REG_ZERO_LSB 0x02 +#define AEAT8800Q24_REG_ZERO_MSB 0x03 +#define AEAT8800Q24_REG_CONF0 0x04 +#define AEAT8800Q24_REG_CONF1 0x05 +#define AEAT8800Q24_REG_CONF2 0x06 +#define AEAT8800Q24_REG_CONF_OTP 0x13 +#define AEAT8800Q24_REG_CAL_OTP 0x1B +#define AEAT8800Q24_REG_CUST_OTP 0x11 +#define AEAT8800Q24_REG_CAL 0x17 + +#define AEAT8800Q24_CONF_OTP_ON 0xA3 +#define AEAT8800Q24_CAL_OTP_ON 0xA5 +#define AEAT8800Q24_CUST_OTP_ON 0xA1 +#define AEAT8800Q24_CAL_ON 0x02 +#define AEAT8800Q24_CAL_OFF 0x00 + + +typedef union { + struct { + uint8_t uvw_pwm_mode:1; + uint8_t pwm:2; + uint8_t iwidth:2; + uint8_t uvw:3; + }; + uint8_t reg; +} AEAT8800Q24_CONF0_t; + + +typedef union { + struct { + uint8_t cpr1:4; + uint8_t hys:4; + }; + uint8_t reg; +} AEAT8800Q24_CONF1_t; + + +typedef union { + struct { + uint8_t dir:1; + uint8_t zero_latency:1; + uint8_t abs_res:2; + uint8_t cpr2:4; + }; + uint8_t reg; +} AEAT8800Q24_CONF2_t; + + + +typedef union { + struct { + uint8_t mhi:1; + uint8_t mlo:1; + uint8_t ready:1; + uint8_t parity:1; + uint8_t :4; + }; + uint8_t reg; +} AEAT8800Q24_Status_t; + + +#ifndef MSBFIRST +#define MSBFIRST BitOrder::MSBFIRST +#endif + +static SPISettings AEAT8800Q24SPISettings(1000000, MSBFIRST, SPI_MODE3); // @suppress("Invalid arguments") +static SPISettings AEAT8800Q24SSISettings(4000000, MSBFIRST, SPI_MODE2); // @suppress("Invalid arguments") + + +class AEAT8800Q24 { +public: + AEAT8800Q24(int nCS, int pinNSL = MOSI, SPISettings spiSettings = AEAT8800Q24SPISettings, SPISettings ssiSettings = AEAT8800Q24SSISettings); + virtual ~AEAT8800Q24(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 14bit angle value + AEAT8800Q24_Status_t getLastStatus(); // get status associated with last angle read + + uint16_t getZero(); + void setZero(uint16_t); + + AEAT8800Q24_CONF0_t getConf0(); + void setConf0(AEAT8800Q24_CONF0_t); + + AEAT8800Q24_CONF1_t getConf1(); + void setConf1(AEAT8800Q24_CONF1_t); + + AEAT8800Q24_CONF2_t getConf2(); + void setConf2(AEAT8800Q24_CONF2_t); + +private: + int nCS = -1; + int pinNSL = -1; + SPISettings spiSettings; + SPISettings ssiSettings; + SPIClass* spi; + AEAT8800Q24_Status_t lastStatus; + + uint16_t transfer16SPI(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + void writeRegister(uint8_t reg, uint8_t value); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp new file mode 100644 index 0000000..f1ad32c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.cpp @@ -0,0 +1,24 @@ +#include "./MagneticSensorAEAT8800Q24.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAEAT8800Q24::MagneticSensorAEAT8800Q24(int nCS, int pinNSL, SPISettings spiSettings, SPISettings ssiSettings) : AEAT8800Q24(nCS, pinNSL, spiSettings, ssiSettings) { +}; + + +MagneticSensorAEAT8800Q24::~MagneticSensorAEAT8800Q24(){ +}; + + +void MagneticSensorAEAT8800Q24::init(SPIClass* _spi) { + this->AEAT8800Q24::init(_spi); + this->Sensor::init(); +}; + + +float MagneticSensorAEAT8800Q24::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AEAT8800Q24_CPR) * _2PI; + // return the shaft angle + return angle_data; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h new file mode 100644 index 0000000..01d6e04 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h @@ -0,0 +1,18 @@ +#ifndef __MAGNETICSENSORAEAT8800Q24_H__ +#define __MAGNETICSENSORAEAT8800Q24_H__ + + +#include "common/base_classes/Sensor.h" +#include "./AEAT8800Q24.h" + +class MagneticSensorAEAT8800Q24 : public Sensor, public AEAT8800Q24 { +public: + MagneticSensorAEAT8800Q24(int nCS, int pinNSL = MOSI, SPISettings spiSettings = AEAT8800Q24SPISettings, SPISettings ssiSettings = AEAT8800Q24SSISettings); + virtual ~MagneticSensorAEAT8800Q24(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md new file mode 100644 index 0000000..0afa47d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/aeat8800q24/README.md @@ -0,0 +1,75 @@ +# AEAT-8800-Q24 SimpleFOC driver + +SPI/SSI driver for the absolute position magnetic rotary encoder. This encoder is not supported by the +normal SimpleFOC drivers due to its mixed SPI/SSI mode. + +- access to the configuration registers of the AEAT-8800-Q24, enabling you to set parameters +- angles are read via SSI, with 16 bit (!) precision +- currently only 16 bit resolution is supported, don't lower the resolution + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. nCS pin is +required. + +Note that due to the way SSI and SPI share pins, you can normally only run one of these sensors per SPI bus. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/aeat8800q24/MagneticSensorAEAT8800Q24.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAEAT8800Q24 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAEAT8800Q24 sensor1(SENSOR1_CS, MOSI_pin, mySPISettings, mySSISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 16 bit value + uint16_t raw = sensor1.readRawAngle(); + + // check status + float angle = sensor1.getSensorAngle(); + AEAT8800Q24_Status_t status = sensor1.getLastStatus(); + if (status.mlo) + Serial.println("Sensor magnet low error"); + if (status.mhi) + Serial.println("Sensor magnet high error"); +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp new file mode 100644 index 0000000..9455775 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.cpp @@ -0,0 +1,245 @@ +/* + * AS5047.cpp + * + * Created on: 2 May 2021 + * Author: runger + */ + +#include "./AS5047.h" + +AS5047::AS5047(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5047::~AS5047() { +} + + +void AS5047::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5047::getCurrentAngle(){ + readCorrectedAngle(); + return ((float)readCorrectedAngle())/(float)AS5047_CPR * 2.0f * (float)PI; +} + +float AS5047::getFastAngle(){ + return ((float)readCorrectedAngle())/(float)AS5047_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5047::readRawAngle(){ + uint16_t command = AS5047_ANGLEUNC_REG | AS5047_RW; // set r=1 and parity=0, result is 0x7FFE + uint16_t lastresult = spi_transfer16(command)&AS5047_RESULT_MASK; + return lastresult; +} +uint16_t AS5047::readCorrectedAngle(){ + uint16_t command = AS5047_ANGLECOM_REG | AS5047_PARITY | AS5047_RW; // set r=1 and parity=1, result is 0xFFFF + uint16_t lastresult = spi_transfer16(command)&AS5047_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047::readMagnitude(){ + uint16_t command = AS5047_MAGNITUDE_REG | AS5047_RW; // set r=1, result is 0x7FFD + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + return result; +} + + +bool AS5047::isErrorFlag(){ + return errorflag; +} + + +AS5047Error AS5047::clearErrorFlag(){ + uint16_t command = AS5047_ERROR_REG | AS5047_RW; // set r=1, result is 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + AS5047Error err = { + .framingError = ((result&0x0001)!=0x0000), + .commandInvalid = ((result&0x0002)!=0x0000), + .parityError = ((result&0x0004)!=0x0000) + }; + return err; +} + + +AS5047Settings1 AS5047::readSettings1(){ + uint16_t command = AS5047_SETTINGS1_REG | AS5047_PARITY | AS5047_RW; // set r=1, result is 0xC018 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047Settings1 result = { + .reg = nop() + }; + return result; +} + + +void AS5047::writeSettings1(AS5047Settings1 settings){ + uint16_t command = AS5047_SETTINGS1_REG; // set r=0, result is 0x0018 + spi_transfer16(command); + spi_transfer16(calcParity(settings.reg)); +} + + +AS5047Settings2 AS5047::readSettings2(){ + uint16_t command = AS5047_SETTINGS2_REG | AS5047_RW; // set r=1, result is 0x4019 + spi_transfer16(command); + AS5047Settings2 result = { + .reg = nop() + }; + return result; +} + + +void AS5047::writeSettings2(AS5047Settings2 settings){ + uint16_t command = AS5047_SETTINGS2_REG | AS5047_PARITY; // set r=0, result is 0x8019 + spi_transfer16(command); + spi_transfer16(calcParity(settings.reg)); +} + + + +AS5047Diagnostics AS5047::readDiagnostics(){ + uint16_t command = AS5047_DIAGNOSTICS_REG | AS5047_PARITY | AS5047_RW; // set r=1, result is 0xFFFC + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047Diagnostics result = { + .reg = nop() + }; + return result; +} + + +void AS5047::enablePWM(bool enable){ + AS5047Settings1 settings = readSettings1(); + if (settings.pwmon == enable) return; // no change + settings.pwmon = enable; + writeSettings1(settings); +} + +void AS5047::enableABI(bool enable){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = enable?0:1; + if (settings.uvw_abi == val) return; // no change + settings.uvw_abi = val; + writeSettings1(settings); +} + + +void AS5047::enableDEAC(bool enable){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = enable?0:1; + if (settings.daecdis == val) return; // no change + settings.daecdis = enable?0:1; + writeSettings1(settings); +} + + +void AS5047::useCorrectedAngle(bool useCorrected){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = useCorrected?0:1; + if (settings.dataselect == val) return; // no change + settings.dataselect = val; + writeSettings1(settings); +} + + + +void AS5047::setHysteresis(uint8_t hyst){ + if (hyst>3) hyst = 3; + uint8_t val = 3-hyst; + AS5047Settings2 settings = readSettings2(); + if (settings.hys == val) return; // no change + settings.hys = val; + writeSettings2(settings); +} + + + + +void AS5047::setABIResolution(AS5047ABIRes res){ + AS5047Settings1 settings = readSettings1(); + uint8_t val = (res>>3)&0x01; + if (settings.abibin!=val || settings.uvw_abi!=0) { + settings.abibin = val; + settings.uvw_abi = 0; + writeSettings1(settings); + } + AS5047Settings2 settings2 = readSettings2(); + val = (res&0x07); + if (settings2.abires!=val) { + settings2.abires = val; + writeSettings2(settings2); + } +} + + + +uint16_t AS5047::setZero(uint16_t value){ + uint16_t command = AS5047_ZPOSL_REG | AS5047_PARITY | AS5047_RW; + spi_transfer16(command); + AS5047ZPosL posL = { + .reg = nop() + }; + command = AS5047_ZPOSM_REG | AS5047_PARITY; + spi_transfer16(command); + spi_transfer16(calcParity((value>>6)&0x00FF)); + command = AS5047_ZPOSL_REG; + posL.zposl = value&0x003F; + spi_transfer16(command); + spi_transfer16(calcParity(posL.reg)); + + return getZero(); +} + + +uint16_t AS5047::getZero() { + uint16_t command = AS5047_ZPOSM_REG | AS5047_RW; + spi_transfer16(command); + command = AS5047_ZPOSL_REG | AS5047_PARITY | AS5047_RW; + uint16_t result = spi_transfer16(command); + AS5047ZPosL posL = { + .reg = nop() + }; + return ((result&0x00FF)<<6) | posL.zposl; +} + + +uint16_t AS5047::nop(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5047_RESULT_MASK; +} + + +uint16_t AS5047::calcParity(uint16_t data){ + data = data & 0x7FFF; + int sum = 0; + for (int i=0;i<15;i++) + if ((data>>i)&0x0001) + sum++; + return (sum&0x01)==0x01?(data|0x8000):data; +} + + +uint16_t AS5047::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + // TODO check parity + errorflag = ((result&AS5047_ERRFLG)>0); + return result; +} + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.h new file mode 100644 index 0000000..e3cc80c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/AS5047.h @@ -0,0 +1,167 @@ +/* + * AS5047.h + * + * Created on: 2 May 2021 + * Author: runger + */ + +#ifndef AS5047_H_ +#define AS5047_H_ + +#include "Arduino.h" +#include "SPI.h" + + + + +union AS5047Diagnostics { + struct { + uint16_t agc:8; + uint16_t lf:1; + uint16_t cof:1; + uint16_t magh:1; + uint16_t magl:1; + uint16_t unused:4; + }; + uint16_t reg; +}; + + +union AS5047ZPosM { + struct { + uint16_t zposm:8; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047ZPosL { + struct { + uint16_t zposl:6; + uint16_t comp_l_error_en:1; + uint16_t comp_h_error_en:1; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047Settings1 { + struct { + uint16_t reserved:1; + uint16_t noiseset:1; + uint16_t dir:1; + uint16_t uvw_abi:1; + uint16_t daecdis:1; + uint16_t abibin:1; + uint16_t dataselect:1; + uint16_t pwmon:1; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +union AS5047Settings2 { + struct { + uint16_t uvwpp:3; + uint16_t hys:2; + uint16_t abires:3; + uint16_t unused:8; + }; + uint16_t reg; +}; + + +struct AS5047Error { + bool framingError; + bool commandInvalid; + bool parityError; +}; + + +enum AS5047ABIRes : uint8_t { + AS5047_ABI_1024 = 0b1010, + AS5047_ABI_2048 = 0b1001, + AS5047_ABI_4096 = 0b1000, + AS5047_ABI_100 = 0b0111, + AS5047_ABI_200 = 0b0110, + AS5047_ABI_400 = 0b0101, + AS5047_ABI_800 = 0b0100, + AS5047_ABI_1200 = 0b0011, + AS5047_ABI_1600 = 0b0010, + AS5047_ABI_2000 = 0b0001, + AS5047_ABI_4000 = 0b0000 +}; + + +#define AS5047_CPR 16384 +#define AS5047_ANGLECOM_REG 0x3FFF +#define AS5047_ANGLEUNC_REG 0x3FFE +#define AS5047_MAGNITUDE_REG 0x3FFD +#define AS5047_DIAGNOSTICS_REG 0x3FFC +#define AS5047_ERROR_REG 0x0001 +#define AS5047_PROG_REG 0x0003 +#define AS5047_ZPOSM_REG 0x0016 +#define AS5047_ZPOSL_REG 0x0017 +#define AS5047_SETTINGS1_REG 0x0018 +#define AS5047_SETTINGS2_REG 0x0019 + +#define AS5047_PARITY 0x8000 +#define AS5047_RW 0x4000 +#define AS5047_ERRFLG 0x4000 +#define AS5047_RESULT_MASK 0x3FFF + + +#define AS5047_BITORDER MSBFIRST + +static SPISettings AS5047SPISettings(8000000, AS5047_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5047 { +public: + AS5047(SPISettings settings = AS5047SPISettings, int nCS = -1); + virtual ~AS5047(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readCorrectedAngle(); // 14bit corrected angle value + uint16_t readMagnitude(); // 14bit magnitude value + + bool isErrorFlag(); + AS5047Error clearErrorFlag(); + + AS5047Diagnostics readDiagnostics(); + AS5047Settings1 readSettings1(); + void writeSettings1(AS5047Settings1 settings); + AS5047Settings2 readSettings2(); + void writeSettings2(AS5047Settings2 settings); + void enablePWM(bool enable); + void enableABI(bool enable); + void setABIResolution(AS5047ABIRes res); + void enableDEAC(bool enable); + void useCorrectedAngle(bool useCorrected); + void setHysteresis(uint8_t hyst); + + uint16_t setZero(uint16_t); + uint16_t getZero(); + + uint16_t calcParity(uint16_t data); + +private: + + uint16_t nop(); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; + +}; + +#endif /* AS5047_H_ */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp new file mode 100644 index 0000000..f5f2e5f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.cpp @@ -0,0 +1,29 @@ + +#include "./MagneticSensorAS5047.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5047::MagneticSensorAS5047(int nCS, bool fastMode, SPISettings settings) : AS5047(settings, nCS), fastMode(fastMode) { + +} + + +MagneticSensorAS5047::~MagneticSensorAS5047(){ + +} + + +void MagneticSensorAS5047::init(SPIClass* _spi) { + this->AS5047::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorAS5047::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AS5047_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h new file mode 100644 index 0000000..aae3f54 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/MagneticSensorAS5047.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5047PD_H__ +#define __MAGNETICSENSORAS5047PD_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5047.h" + +class MagneticSensorAS5047 : public Sensor, public AS5047 { +public: + MagneticSensorAS5047(int nCS = -1, bool fastMode = false, SPISettings settings = AS5047SPISettings); + virtual ~MagneticSensorAS5047(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/README.md new file mode 100644 index 0000000..22de36b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047/README.md @@ -0,0 +1,82 @@ +# AS5047 SimpleFOC driver + +While the AS5047 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5047-specific driver includes some optimisations: + +- access to the other registers of the AS5047, including the magnitude value which can be used to check the magnet strength, and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + +This driver should work with AS5047P and AS5047D models. The AS5047U has it's own driver [here](../as5047u/). + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5047/MagneticSensorAS5047.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5047 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5047 sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5047Error error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5047Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp new file mode 100644 index 0000000..bfe70be --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.cpp @@ -0,0 +1,350 @@ +/* + * AS5047U.cpp + * + * Created on: 24 Feb 2022 + * Author: runger + */ + +#include "./AS5047U.h" + +AS5047U::AS5047U(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5047U::~AS5047U() { +} + + +void AS5047U::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5047U::getCurrentAngle(){ + readCorrectedAngle(); + return ((float)readCorrectedAngle())/(float)AS5047U_CPR * 2.0f * (float)PI; +} + +float AS5047U::getFastAngle(){ + return ((float)readCorrectedAngle())/(float)AS5047U_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5047U::readRawAngle(){ + uint16_t command = AS5047U_ANGLEUNC_REG | AS5047U_RW; + uint16_t lastresult = spi_transfer16(command)&AS5047U_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047U::readCorrectedAngle(){ + uint16_t command = AS5047U_ANGLECOM_REG | AS5047U_RW; + uint16_t lastresult = spi_transfer16(command)&AS5047U_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5047U::readMagnitude(){ + uint16_t command = AS5047U_MAGNITUDE_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result; +} + + +uint16_t AS5047U::readVelocity(){ + uint16_t command = AS5047U_VELOCITY_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result; +} + + +bool AS5047U::isErrorFlag(){ + return errorflag; +} + +bool AS5047U::isWarningFlag(){ + return warningflag; +} + + +AS5047UError AS5047U::clearErrorFlag(){ + uint16_t command = AS5047U_ERROR_REG | AS5047U_RW; // set r=1, result is 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UError result; + result.reg = nop16(); + return result; +} + + +AS5047USettings1 AS5047U::readSettings1(){ + uint16_t command = AS5047U_SETTINGS1_REG | AS5047U_RW; // set r=1, result is 0xC018 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings1 result = { + .reg = nop16() + }; + return result; +} + + + + +void AS5047U::writeSettings1(AS5047USettings1 settings){ + writeRegister24(AS5047U_SETTINGS1_REG, settings.reg); +} + + + + +AS5047USettings2 AS5047U::readSettings2(){ + uint16_t command = AS5047U_SETTINGS2_REG | AS5047U_RW; // set r=1, result is 0x4019 + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings2 result = { + .reg = nop16() + }; + return result; +} + + + + + +void AS5047U::writeSettings2(AS5047USettings2 settings){ + writeRegister24(AS5047U_SETTINGS2_REG, settings.reg); +} + + + + + +AS5047USettings3 AS5047U::readSettings3(){ + uint16_t command = AS5047U_SETTINGS3_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047USettings3 result = { + .reg = nop16() + }; + return result; +} + + + + + +void AS5047U::writeSettings3(AS5047USettings3 settings){ + writeRegister24(AS5047U_SETTINGS3_REG, settings.reg); +} + + + + +AS5047UDiagnostics AS5047U::readDiagnostics(){ + uint16_t command = AS5047U_DIAGNOSTICS_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UDiagnostics result = { + .reg = nop16() + }; + return result; +} + + + + +uint8_t AS5047U::readAGC(){ + uint16_t command = AS5047U_AGC_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result & 0x00FF; +}; + + + +uint8_t AS5047U::readECCCHK(){ + uint16_t command = AS5047U_ECCCHK_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop16(); + return result & 0x007F; +}; + + + + +AS5047UDisableSettings AS5047U::readDisableSettings(){ + uint16_t command = AS5047U_DISABLE_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UDisableSettings result = { + .reg = nop16() + }; + return result; +}; + + + +void AS5047U::writeDisableSettings(AS5047UDisableSettings settings){ + writeRegister24(AS5047U_DISABLE_REG, settings.reg); +}; + + + +AS5047UECCSettings AS5047U::readECCSettings(){ + uint16_t command = AS5047U_ECC_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UECCSettings result = { + .reg = nop16() + }; + return result; +}; + + + +void AS5047U::writeECCSettings(AS5047UECCSettings settings){ + writeRegister24(AS5047U_ECC_REG, settings.reg); +}; + + + + + + + +void AS5047U::enablePWM(bool enable, bool pwmOnWPin){ + // AS5047UDisableSettings settings = readDisableSettings(); + // if (settings.uvw_off==1) { + // settings.uvw_off = 0; + // writeDiableSettings(settings); + // } + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = pwmOnWPin?0:1; + settings2.pwm_on = enable; + writeSettings2(settings2); +} + + + + +void AS5047U::enableABI(bool enable){ + AS5047UDisableSettings settings = readDisableSettings(); + settings.abi_off = enable?0:1; + writeDisableSettings(settings); + delayMicroseconds(50); + if (enable) { + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = 0; + writeSettings2(settings2); + } +} + + + +void AS5047U::enableUVW(bool enable){ + AS5047UDisableSettings settings = readDisableSettings(); + settings.uvw_off = enable?0:1; + writeDisableSettings(settings); + if (enable) { + AS5047USettings2 settings2 = readSettings2(); + settings2.uvw_abi = 1; + writeSettings2(settings2); + } +} + + + + +uint16_t AS5047U::getZero(){ + uint16_t command = AS5047U_ZPOSM_REG | AS5047U_RW; + spi_transfer16(command); + command = AS5047U_ZPOSL_REG | AS5047U_RW; + uint16_t result = spi_transfer16(command); + AS5047UZPosL posL = { + .reg = nop16() + }; + return ((result&0x00FF)<<6) | posL.zposl; +} + + + +uint16_t AS5047U::setZero(uint16_t value){ + uint16_t command = AS5047U_ZPOSL_REG | AS5047U_RW; + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5047UZPosL posL = { + .reg = nop16() + }; + posL.zposl = value&0x003F; + writeRegister24(AS5047U_ZPOSL_REG, posL.reg); + writeRegister24(AS5047U_ZPOSM_REG, (value>>6)&0x00FF); + return getZero(); +} + + + + +uint16_t AS5047U::nop16(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5047U_RESULT_MASK; +} + + + +uint16_t AS5047U::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((result&AS5047U_ERROR)>0); + warningflag = ((result&AS5047U_WARNING)>0); + return result; +} + +uint8_t AS5047U::calcCRC(uint16_t data){ + uint8_t crc = 0xC4; // Initial value + for (int i = 0; i < 16; i++) { + if ((crc ^ data) & 0x8000) { + crc = (crc << 1) ^ 0x1D; + } else { + crc <<= 1; + } + data <<= 1; + } + return crc ^ 0xFF; +} + +uint16_t AS5047U::writeRegister24(uint16_t reg, uint16_t data) { + uint8_t buff[3]; + buff[0] = (reg>>8)&0x3F; + buff[1] = reg&0xFF; + buff[2] = calcCRC(reg); + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + spi->transfer(buff, 3); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((buff[0]&0x40)>0); + warningflag = ((buff[0]&0x80)>0); + + buff[0] = (data>>8)&0x3F; + buff[1] = data&0xFF; + buff[2] = calcCRC(data); + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + spi->transfer(buff, 3); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + errorflag = ((buff[0]&0x40)>0); + warningflag = ((buff[0]&0x80)>0); + + delayMicroseconds(50); + + return buff[0]<<8 | buff[1]; +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h new file mode 100644 index 0000000..cb9cf10 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/AS5047U.h @@ -0,0 +1,225 @@ +/* + * AS5047.h + * + * Created on: 24 Feb 2022 + * Author: runger + */ + +#ifndef AS5047U_H_ +#define AS5047U_H_ + +#include "Arduino.h" +#include "SPI.h" + + +union AS5047UDisableSettings { + struct { + uint16_t :9; + uint16_t filter_disable:1; + uint16_t :4; + uint16_t abi_off:1; + uint16_t uvw_off:1; + }; + uint16_t reg; +}; + + + +union AS5047UDiagnostics { + struct { + uint16_t spi_cnt:2; + uint16_t :1; + uint16_t agc_finished:1; + uint16_t off_finished:1; + uint16_t sin_finished:1; + uint16_t cos_finished:1; + uint16_t maghalf_flag:1; + uint16_t comp_h:1; + uint16_t comp_l:1; + uint16_t cordic_ovf:1; + uint16_t loops_finished:1; + uint16_t vdd_mode:1; + }; + uint16_t reg; +}; + + +union AS5047UZPosM { + struct { + uint16_t zposm:8; + }; + uint16_t reg; +}; + + +union AS5047UZPosL { + struct { + uint16_t zposl:6; + uint16_t dia1_en:1; // automotive version only + uint16_t dia2_en:1; // automotive version only + }; + uint16_t reg; +}; + + +union AS5047USettings1 { + struct { + uint16_t k_max:3; + uint16_t k_min:3; + uint16_t dia3_en:1; // not applicable + uint16_t dia4_en:1; // not applicable + }; + uint16_t reg; +}; + + +union AS5047USettings2 { + struct { + uint16_t iwidth:1; + uint16_t noiseset:1; + uint16_t dir:1; + uint16_t uvw_abi:1; + uint16_t daecdis:1; + uint16_t abi_dec:1; + uint16_t data_select:1; + uint16_t pwm_on:1; + }; + uint16_t reg; +}; + + + + +union AS5047USettings3 { + struct { + uint16_t uvwpp:3; + uint16_t hys:2; + uint16_t abires:3; + }; + uint16_t reg; +}; + + + + +union AS5047UECCSettings { + struct { + uint16_t ecc_chsum:7; + uint16_t ecc_en:1; + }; + uint16_t reg; +}; + + + + +struct AS5047UError { + struct { + uint16_t cordic_ovf:1; + uint16_t off_notfinished:1; + uint16_t :1; + uint16_t wdtst:1; + uint16_t crc_error:1; + uint16_t cmd_error:1; + uint16_t frame_error:1; + uint16_t p2ram_error:1; + uint16_t p2ram_warning:1; + uint16_t maghalf:1; + uint16_t agc_warning:1; + }; + uint16_t reg; +}; + + + + + +#define AS5047U_CPR 16384 +#define AS5047U_ANGLECOM_REG 0x3FFF +#define AS5047U_ANGLEUNC_REG 0x3FFE +#define AS5047U_MAGNITUDE_REG 0x3FFD +#define AS5047U_VELOCITY_REG 0x3FFC +#define AS5047U_SIN_REG 0x3FFA +#define AS5047U_COS_REG 0x3FFB +#define AS5047U_AGC_REG 0x3FF9 +#define AS5047U_DIAGNOSTICS_REG 0x3FF5 +#define AS5047U_ERROR_REG 0x0001 +#define AS5047U_PROG_REG 0x0003 +#define AS5047U_ECCCHK_REG 0x00D1 + +#define AS5047U_DISABLE_REG 0x0015 +#define AS5047U_ZPOSM_REG 0x0016 +#define AS5047U_ZPOSL_REG 0x0017 +#define AS5047U_SETTINGS1_REG 0x0018 +#define AS5047U_SETTINGS2_REG 0x0019 +#define AS5047U_SETTINGS3_REG 0x001A +#define AS5047U_ECC_REG 0x001B + +#define AS5047U_WARNING 0x8000 +#define AS5047U_ERROR 0x4000 +#define AS5047U_RW 0x4000 +#define AS5047U_ERRFLG 0xC000 +#define AS5047U_RESULT_MASK 0x3FFF + + +#define AS5047U_BITORDER MSBFIRST + +static SPISettings AS5047USPISettings(8000000, AS5047U_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5047U { +public: + AS5047U(SPISettings settings = AS5047USPISettings, int nCS = -1); + virtual ~AS5047U(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readCorrectedAngle(); // 14bit corrected angle value + uint16_t readMagnitude(); // 14bit magnitude value + uint16_t readVelocity(); // 14bit magnitude value + + bool isErrorFlag(); + bool isWarningFlag(); + AS5047UError clearErrorFlag(); + + AS5047UDiagnostics readDiagnostics(); + uint8_t readAGC(); + uint8_t readECCCHK(); + + AS5047UDisableSettings readDisableSettings(); + void writeDisableSettings(AS5047UDisableSettings settings); + AS5047USettings1 readSettings1(); + void writeSettings1(AS5047USettings1 settings); + AS5047USettings2 readSettings2(); + void writeSettings2(AS5047USettings2 settings); + AS5047USettings3 readSettings3(); + void writeSettings3(AS5047USettings3 settings); + AS5047UECCSettings readECCSettings(); + void writeECCSettings(AS5047UECCSettings settings); + + void enablePWM(bool enable, bool pwmOnWPin = true); + void enableABI(bool enable); + void enableUVW(bool enable); + + uint16_t setZero(uint16_t); + uint16_t getZero(); + +private: + + uint16_t nop16(); + uint16_t spi_transfer16(uint16_t outdata); + uint8_t calcCRC(uint16_t data); + uint16_t writeRegister24(uint16_t reg, uint16_t data); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + bool warningflag = false; + int nCS = -1; + +}; + +#endif /* AS5047U_H_ */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp new file mode 100644 index 0000000..e5fe89a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.cpp @@ -0,0 +1,29 @@ + +#include "./MagneticSensorAS5047U.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5047U::MagneticSensorAS5047U(int nCS, bool fastMode, SPISettings settings) : AS5047U(settings, nCS), fastMode(fastMode) { + +} + + +MagneticSensorAS5047U::~MagneticSensorAS5047U(){ + +} + + +void MagneticSensorAS5047U::init(SPIClass* _spi) { + this->AS5047U::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorAS5047U::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + angle_data = ( angle_data / (float)AS5047U_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h new file mode 100644 index 0000000..e6ef813 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/MagneticSensorAS5047U.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5047U_H__ +#define __MAGNETICSENSORAS5047U_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5047U.h" + +class MagneticSensorAS5047U : public Sensor, public AS5047U { +public: + MagneticSensorAS5047U(int nCS = -1, bool fastMode = false, SPISettings settings = AS5047USPISettings); + virtual ~MagneticSensorAS5047U(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +private: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/README.md new file mode 100644 index 0000000..2a6743c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5047u/README.md @@ -0,0 +1,81 @@ +# AS5047U SimpleFOC driver + +While AS5047U absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5047U-specific driver includes some optimisations: + +- access to the other registers of the AS5047U, including the magnitude value which can be used to check the magnet strength, the velocity register and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5047u/MagneticSensorAS5047U.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5047U sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5047U sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5047UError error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5047Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp new file mode 100644 index 0000000..27ef254 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.cpp @@ -0,0 +1,118 @@ +/* + * AS5048A.cpp + * + * Created on: 8 Mar 2021 + * Author: runger + */ + +#include "AS5048A.h" + +AS5048A::AS5048A(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +} + +AS5048A::~AS5048A() { +} + + +void AS5048A::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + //SPI has an internal SPI-device counter, it is possible to call "begin()" from different devices + spi->begin(); + readRawAngle(); // read an angle +} + +float AS5048A::getCurrentAngle(){ + readRawAngle(); + return ((float)readRawAngle())/(float)AS5048A_CPR * 2.0f * (float)PI; +} + +float AS5048A::getFastAngle(){ + return ((float)readRawAngle())/(float)AS5048A_CPR * 2.0f * (float)PI; +} + + +uint16_t AS5048A::readRawAngle(){ + uint16_t command = AS5048A_ANGLE_REG | AS5048A_PARITY | AS5048A_RW; // set r=1 and parity=1, result ix 0xFFFF + uint16_t lastresult = spi_transfer16(command)&AS5048A_RESULT_MASK; + return lastresult; +} + + +uint16_t AS5048A::readMagnitude(){ + uint16_t command = AS5048A_MAGNITUDE_REG | AS5048A_RW; // set r=1, result ix 0x7FFE + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + return result; +} + + +bool AS5048A::isErrorFlag(){ + return errorflag; +} + + +AS5048Error AS5048A::clearErrorFlag(){ + uint16_t command = AS5048A_ERROR_REG | AS5048A_RW; // set r=1, result ix 0x4001 + /*uint16_t cmdresult =*/ spi_transfer16(command); + uint16_t result = nop(); + AS5048Error err = { + .parityError = ((result&0x0004)!=0x0000), + .commandInvalid = ((result&0x0002)!=0x0000), + .framingError = ((result&0x0001)!=0x0000) + }; + return err; +} + + +AS5048Diagnostics AS5048A::readDiagnostics(){ + uint16_t command = AS5048A_DIAGNOSTICS_REG | AS5048A_RW; // set r=1, result ix 0x7FFD + /*uint16_t cmdresult =*/ spi_transfer16(command); + AS5048Diagnostics result = { + .reg = nop() + }; + return result; +} + + +uint16_t AS5048A::setZero(uint16_t){ + // TODO implement me! + return 0; +} + + +uint16_t AS5048A::enableOneTimeProgramming(){ + // no plans to implement this at the moment. one-time-programming a $10 chip isn't really a "maker" thing to do. The zero + // position can be easily retained in software, stored on the MCU, and thereby the sensor can be reused in another project. + return 0; +} + + +uint16_t AS5048A::programZero(){ + // no plans to implement this at the moment. one-time-programming a $10 chip isn't really a "maker" thing to do. The zero + // position can be easily retained in software, stored on the MCU, and thereby the sensor can be reused in another project. + return 0; +} + +uint16_t AS5048A::nop(){ + uint16_t result = spi_transfer16(0xFFFF); // using 0xFFFF as nop instead of 0x0000, then next call to fastAngle will return an angle + return result&AS5048A_RESULT_MASK; +} + +uint16_t AS5048A::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + // TODO check parity + errorflag = ((result&AS5048A_ERRFLG)>0); + return result; +} + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h new file mode 100644 index 0000000..00a4fe6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/AS5048A.h @@ -0,0 +1,89 @@ +/* + * AS5048A.h + * + * Created on: 8 Mar 2021 + * Author: runger + */ + +#ifndef AS5048A_H_ +#define AS5048A_H_ + +#include "Arduino.h" +#include "SPI.h" + + + + +union AS5048Diagnostics { + struct { + uint16_t agc:8; + uint16_t ocf:1; + uint16_t cof:1; + uint16_t compLow:1; + uint16_t compHigh:1; + }; + uint16_t reg; +}; + + +struct AS5048Error { + bool parityError; + bool commandInvalid; + bool framingError; +}; + + +#define AS5048A_CPR 16384 +#define AS5048A_ANGLE_REG 0x3FFF +#define AS5048A_ERROR_REG 0x0001 +#define AS5048A_PROGCTL_REG 0x0003 +#define AS5048A_OTPHIGH_REG 0x0016 +#define AS5048A_OTPLOW_REG 0x0017 +#define AS5048A_DIAGNOSTICS_REG 0x3FFD +#define AS5048A_MAGNITUDE_REG 0x3FFE +#define AS5048A_PARITY 0x8000 +#define AS5048A_RW 0x4000 +#define AS5048A_ERRFLG 0x4000 +#define AS5048A_RESULT_MASK 0x3FFF + + +#define AS5048_BITORDER MSBFIRST + + +static SPISettings AS5048SPISettings(8000000, AS5048_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class AS5048A { +public: + AS5048A(SPISettings settings = AS5048SPISettings, int nCS = -1); + virtual ~AS5048A(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + float getFastAngle(); // angle in radians, return last value and read new + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readMagnitude(); // 14bit magnitude value + + bool isErrorFlag(); + AS5048Error clearErrorFlag(); + + AS5048Diagnostics readDiagnostics(); + + uint16_t setZero(uint16_t); + uint16_t enableOneTimeProgramming(); + uint16_t programZero(); + +private: + + uint16_t nop(); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; + +}; + +#endif /* AS5048A_H_ */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp new file mode 100644 index 0000000..a58b1b9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.cpp @@ -0,0 +1,26 @@ + +#include "./MagneticSensorAS5048A.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5048A::MagneticSensorAS5048A(int nCS, bool fastMode, SPISettings settings) : AS5048A(settings, nCS), fastMode(fastMode) { + +} + +MagneticSensorAS5048A::~MagneticSensorAS5048A(){ + +} +void MagneticSensorAS5048A::init(SPIClass* _spi) { + this->AS5048A::init(_spi); + this->Sensor::init(); +} + +float MagneticSensorAS5048A::getSensorAngle() { + float angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + + angle_data = ( angle_data / (float)AS5048A_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h new file mode 100644 index 0000000..0f7e2b7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/MagneticSensorAS5048A.h @@ -0,0 +1,20 @@ + +#ifndef __MAGNETICSENSORAS5048A_H__ +#define __MAGNETICSENSORAS5048A_H__ + +#include "common/base_classes/Sensor.h" +#include "./AS5048A.h" + +class MagneticSensorAS5048A : public Sensor, public AS5048A { +public: + MagneticSensorAS5048A(int nCS = -1, bool fastMode = false, SPISettings settings = AS5048SPISettings); + virtual ~MagneticSensorAS5048A(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +protected: + bool fastMode = false; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp new file mode 100644 index 0000000..9f340c4 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.cpp @@ -0,0 +1,50 @@ +/* + * PreciseMagneticSensorAS5048A.cpp + * + * Created on: 1 May 2021 + * Author: runger + */ + +#include +#include "common/foc_utils.h" +#include "common/time_utils.h" + +PreciseMagneticSensorAS5048A::PreciseMagneticSensorAS5048A(int nCS, bool fastMode, SPISettings settings) : AS5048A(settings, nCS), fastMode(fastMode) { } + +PreciseMagneticSensorAS5048A::~PreciseMagneticSensorAS5048A() { } + + +void PreciseMagneticSensorAS5048A::init(SPIClass* _spi) { + this->AS5048A::init(_spi); + // velocity calculation init + current_ts = _micros(); + /*uint16_t angle_data =*/ readRawAngle(); + current_angle = PreciseAngle(readRawAngle(), 0); + getAngle(); +} + + + +float PreciseMagneticSensorAS5048A::getSensorAngle() { + previous_ts = current_ts; + previous_angle = current_angle; + uint16_t angle_data = readRawAngle(); + if (!fastMode) // read again to ensure current value + angle_data = readRawAngle(); + current_ts = _micros(); + current_angle.update(angle_data); + return current_angle.asFloat(); +} + + + +/* +unlike the normal MagneticSensorSPI implementation, this one uses the angle previously read by the last call to getAngle to do its +calculation, and does not directly poll any data from the sensor. +This is an optimisation for speed, based on the assumption that loopFOC() (which calls getAngle()) is invoked at least as often as +move() (which calls getVelocity()). If this is the case, getVelocity() should always have a sufficiently "fresh" value to work with. +If using this function in a different context, simply call getAngle() first to be sure of a fresh angle value. +*/ +float PreciseMagneticSensorAS5048A::getVelocity() { + return current_angle.velocity(previous_angle, (current_ts-previous_ts)); +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h new file mode 100644 index 0000000..389b6f7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/PreciseMagneticSensorAS5048A.h @@ -0,0 +1,33 @@ +/* + * PreciseMagneticSensorAS5048A.h + * + * Created on: 1 May 2021 + * Author: runger + */ + +#ifndef LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ +#define LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ + +#include "common/base_classes/Sensor.h" +#include "./AS5048A.h" +#include "utilities/PreciseAngle.h" + +class PreciseMagneticSensorAS5048A : public Sensor, public AS5048A { +public: + PreciseMagneticSensorAS5048A(int nCS = -1, bool fastMode = false, SPISettings settings = AS5048SPISettings); + virtual ~PreciseMagneticSensorAS5048A(); + + virtual float getSensorAngle() override; + virtual float getVelocity() override; + + virtual void init(SPIClass* _spi = &SPI) override; + +protected: + bool fastMode = false; + PreciseAngle previous_angle = PreciseAngle(); + PreciseAngle current_angle = PreciseAngle(); + unsigned long previous_ts = 0; + unsigned long current_ts = 1; +}; + +#endif /* LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_ENCODERS_AS5048A_PRECISEMAGNETICSENSORAS5048A_H_ */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/README.md new file mode 100644 index 0000000..468994c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5048a/README.md @@ -0,0 +1,86 @@ +# AS5048A SimpleFOC driver + +While AS5048A absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this AS5048A-specific driver includes some optimisations: + +- access to the other registers of the AS5048A, including the magnitude value which can be used to check the magnet strength, and the diagnostics register +- access to the error state of the sensor, and ability to clear errors +- it has a fastMode setting, in which the sensor is sent only 1 command per getAngle() call - the value returned will be from previous getAngle() invocation + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/as5048a/MagneticSensorAS5048A.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorAS5048A sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorAS5048A sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // read the CORDIC magnitude value, a measure of the magnet field strength + float m1 = sensor1.readMagnitude(); + + // check for errors + if (sensor1.isErrorFlag()) { + AS5048Error error = sensor1.clearErrorFlag(); + if (error.parityError) { // also error.framingError, error.commandInvalid + // etc... + } + } + + // get diagnostics + AS5048Diagnostics diagnostics = sensor1.readDiagnostics(); +``` + + +## PreciseMagneticSensorAS5048A + +This is a variant of the sensor that uses [PreciseAngle](../utilities) to represent the angle, allowing a (much) greater range of rotation. + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp new file mode 100644 index 0000000..6d60000 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.cpp @@ -0,0 +1,34 @@ +#include "./MagneticSensorAS5145.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorAS5145::MagneticSensorAS5145(SPISettings settings) : settings(settings) { + +} + + +MagneticSensorAS5145::~MagneticSensorAS5145() { + +} + +void MagneticSensorAS5145::init(SPIClass* _spi) { + this->spi=_spi; + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorAS5145::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( (float)angle_data / AS5145_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorAS5145::readRawAngleSSI() { + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + //uint16_t parity = spi->transfer(0x00); + spi->endTransaction(); + return (value>>3)&0x1FFF; // TODO this isn't what I expected from the datasheet... maybe there's a leading 0 bit? +}; // 12bit angle value diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h new file mode 100644 index 0000000..ee1f34a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/MagneticSensorAS5145.h @@ -0,0 +1,39 @@ + +#ifndef __MAGNETIC_SENSOR_AS5145_H__ +#define __MAGNETIC_SENSOR_AS5145_H__ + +#include "Arduino.h" +#include "SPI.h" +#include "common/base_classes/Sensor.h" + +#ifndef MSBFIRST +#define MSBFIRST BitOrder::MSBFIRST +#endif + +#define AS5145_BITORDER MSBFIRST +#define AS5145_CPR 4096.0f +#define _2PI 6.28318530718f + + +static SPISettings AS5145SSISettings(1000000, AS5145_BITORDER, SPI_MODE2); // @suppress("Invalid arguments") + + +class MagneticSensorAS5145 : public Sensor { +public: + MagneticSensorAS5145(SPISettings settings = AS5145SSISettings); + virtual ~MagneticSensorAS5145(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); + + uint16_t readRawAngleSSI(); + +private: + SPIClass* spi; + SPISettings settings; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/README.md new file mode 100644 index 0000000..f0474b9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5145/README.md @@ -0,0 +1,50 @@ +# AS5145 SimpleFOC driver + +SSI protocol driver for the AMS AS5145 magnetic encoder. Any of the A, B or H variants should work. AS5045 encoders should also be supported. + +Only angle reading is supported, might get to the status bits at a later time. +The SSI protocol is "emulated" using the SPI peripheral. + +Tested with AS5145A on STM32G491 so far. + +## Hardware setup + +Wire the sensor's data (DO) line to the MISO (CIPO) pin, nCS, SCK as normal. Leave the MOSI pin unconnected. + +## Software setup + +``` +#include +#include +#include +#include "encoders/as5145/MagneticSensorAS5145.h" + +MagneticSensorAS5145 sensor; +SPIClass spi_ssi(PB15, PB14, PB13, PB12); + +long ts; + +void setup() { + Serial.begin(115200); + while (!Serial) ; + delay(2000); + + Serial.println("Initializing sensor..."); + + spi_ssi.begin(); + sensor.init(&spi_ssi); + + Serial.println("Sensor initialized."); + + ts = millis(); +} + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp new file mode 100644 index 0000000..84e66e2 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.cpp @@ -0,0 +1,175 @@ + + +#include "./AS5600.h" + + +AS5600::AS5600(uint8_t address) : _address(address) {}; +AS5600::~AS5600() {}; + +void AS5600::init(TwoWire* wire){ + _wire = wire; + _wire->begin(); + if (!closeTransactions) { + setAngleRegister(); + } +}; + + +void AS5600::setAngleRegister() { + _wire->beginTransmission(_address); + if (useHysteresis) + _wire->write(AS5600_REG_ANGLE); + else + _wire->write(AS5600_REG_ANGLE_RAW); + _wire->endTransmission(false); +} + + +uint16_t AS5600::angle() { + uint16_t result = 0; + if (!closeTransactions) { + setAngleRegister(); + } + _wire->requestFrom(_address, (uint8_t)2, (uint8_t)closeTransactions); + result = _wire->read()<<8; + result |= _wire->read(); + return result; +}; + + +uint16_t AS5600::readRawAngle() { + return readRegister(AS5600_REG_ANGLE_RAW, 2); +}; + + +uint16_t AS5600::readAngle() { + return readRegister(AS5600_REG_ANGLE, 2); +}; + + + +uint16_t AS5600::readMagnitude() { + return readRegister(AS5600_REG_MAGNITUDE, 2); +}; + + +AS5600Status AS5600::readStatus() { + AS5600Status result; + result.reg = (uint8_t)readRegister(AS5600_REG_STATUS, 1); // TODO: shift bits around + return result; +}; + + +uint8_t AS5600::readAGC() { + return (uint8_t)readRegister(AS5600_REG_AGC, 1); +}; + + + +AS5600Conf AS5600::readConf() { + AS5600Conf result; + result.reg = readRegister(AS5600_REG_CONF, 2); + return result; +}; + + +uint16_t AS5600::readMang() { + return readRegister(AS5600_REG_MANG, 2); +}; + + +uint16_t AS5600::readMPos() { + return readRegister(AS5600_REG_MPOS, 2); +}; + + +uint16_t AS5600::readZPos() { + return readRegister(AS5600_REG_ZPOS, 2); +}; + + +uint8_t AS5600::readZMCO() { + return (readRegister(AS5600_REG_ZMCO, 1)&0x03); +}; + +uint8_t AS5600::readI2CAddr() { + return (readRegister(AS5600_REG_I2CADDR, 1)>>1); +}; + + +// set registers +void AS5600::setConf(AS5600Conf value) { + // TODO: read before write + writeRegister(AS5600_REG_CONF, value.reg); +}; + + +void AS5600::setMang(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_MANG, value); +}; + + +void AS5600::setMPos(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_MPOS, value); +}; + + +void AS5600::setZPos(uint16_t value) { + // TODO: read before write + writeRegister(AS5600_REG_ZPOS, value); +}; + +void AS5600::setI2CAddr(uint8_t value) { + uint8_t val = (uint8_t)readRegister(AS5600_REG_I2CADDR, 1); + val = (value<<1) | (val&0x01); + writeRegister(AS5600_REG_I2CADDR, val); +}; + +void AS5600::setI2CUpdt(uint8_t value) { + uint8_t val = (uint8_t)readRegister(AS5600_REG_I2CUPDT, 1); + val = (value<<1) | (val&0x01); + writeRegister(AS5600_REG_I2CUPDT, val); +}; + + +void AS5600::burnSettings(){ + writeRegister(AS5600_REG_BURN, 0x40, 1); +} + + + +uint16_t AS5600::readRegister(uint8_t reg, uint8_t len){ + uint16_t result = 0; + _wire->beginTransmission(_address); + _wire->write(reg); + _wire->endTransmission(false); + _wire->requestFrom(_address, len, (uint8_t)closeTransactions); + if (!closeTransactions) { + setAngleRegister(); + } + result = _wire->read(); + if (len == 2) { + result <<= 8; + result |= _wire->read(); + } + return result; +}; + + + +void AS5600::writeRegister(uint8_t reg, uint16_t val, uint8_t len){ + _wire->beginTransmission(_address); + _wire->write(reg); + if (len == 2) { + _wire->write(val>>8); + } + _wire->write(val&0xFF); + _wire->endTransmission(closeTransactions); + if (!closeTransactions) { + setAngleRegister(); + } +}; + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.h new file mode 100644 index 0000000..8518c17 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/AS5600.h @@ -0,0 +1,104 @@ + +#pragma once + + +#include +#include + + +#define AS5600_REG_ZMCO 0x00 +#define AS5600_REG_ZPOS 0x01 +#define AS5600_REG_MPOS 0x03 +#define AS5600_REG_MANG 0x05 +#define AS5600_REG_CONF 0x07 + +#define AS5600_REG_I2CADDR 0x20 +#define AS5600_REG_I2CUPDT 0x21 + +#define AS5600_REG_ANGLE 0x0E +#define AS5600_REG_ANGLE_RAW 0x0C + +#define AS5600_REG_STATUS 0x0B +#define AS5600_REG_AGC 0x1A +#define AS5600_REG_MAGNITUDE 0x1B + +#define AS5600_REG_BURN 0xFF + +#define AS5600_CPR (4096.0f) + + +union AS5600Conf { + struct { + uint16_t pm:2; + uint16_t hyst:2; + uint16_t outs:2; + uint16_t pwmf:2; + uint16_t sf:2; + uint16_t fth:3; + uint16_t wd:1; + uint16_t unused:2; + }; + uint16_t reg; +}; + +union AS5600Status { + struct { + uint8_t unused:3; + uint8_t mh:1; + uint8_t ml:1; + uint8_t md:1; + uint8_t unused2:2; + }; + uint8_t reg; +}; + + + + +class AS5600 { +public: + AS5600(uint8_t address = 0x36); + ~AS5600(); + + virtual void init(TwoWire* wire = &Wire); + + // read an angle, either with or without hysteresis, depending on the useHysteresis flag + // and using fast mode (not closing transactions) if so configured + uint16_t angle(); + + // read registers + uint16_t readRawAngle(); + uint16_t readAngle(); + + uint16_t readMagnitude(); + AS5600Status readStatus(); + uint8_t readAGC(); + + AS5600Conf readConf(); + uint16_t readMang(); + uint16_t readMPos(); + uint16_t readZPos(); + uint8_t readZMCO(); + uint8_t readI2CAddr(); + + // set registers + void setConf(AS5600Conf value); + void setMang(uint16_t value); + void setMPos(uint16_t value); + void setZPos(uint16_t value); + void setI2CAddr(uint8_t value); + void setI2CUpdt(uint8_t value); + + void burnSettings(); + + bool closeTransactions = true; + bool useHysteresis = true; + uint8_t _address; +protected: + TwoWire* _wire; + + void setAngleRegister(); + uint16_t readRegister(uint8_t reg, uint8_t len); + void writeRegister(uint8_t reg, uint16_t val, uint8_t len = 2); +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp new file mode 100644 index 0000000..4cea5cf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.cpp @@ -0,0 +1,18 @@ + + +#include "./MagneticSensorAS5600.h" +#include "common/foc_utils.h" + +MagneticSensorAS5600::MagneticSensorAS5600(uint8_t _address) : AS5600(_address) {}; +MagneticSensorAS5600::~MagneticSensorAS5600() {}; + +void MagneticSensorAS5600::init(TwoWire* wire) { + AS5600::init(wire); + Sensor::init(); +}; + + +float MagneticSensorAS5600::getSensorAngle() { + uint16_t raw = readRawAngle(); + return raw / AS5600_CPR * _2PI; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h new file mode 100644 index 0000000..82a8419 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/MagneticSensorAS5600.h @@ -0,0 +1,22 @@ + +#pragma once + + +#include "./AS5600.h" +#include "common/base_classes/Sensor.h" + + +class MagneticSensorAS5600 : public Sensor, public AS5600 { + +public: + MagneticSensorAS5600(uint8_t _address = 0x36); + ~MagneticSensorAS5600(); + + virtual void init(TwoWire* wire = &Wire); + + virtual float getSensorAngle() override; + +protected: + +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/README.md new file mode 100644 index 0000000..eb0f37a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/as5600/README.md @@ -0,0 +1,131 @@ +# AS5600 SimpleFOC driver + +I2C protocol driver for the AMS AS5600 magnetic encoder (digital potentiometer). Also supports the newer AS5600L variant. + +:warning: work in progress + +## Hardware setup + +Connect the sensor to 3.3V or 5V power as appropriate, and to I2C (SDA and SCL). + +Important: please make sure the direction pin (DIR) is either pulled up to VDD, or down to GND. Do not leave the direction pin floating. + +## Software setup + +The sensor driver is easy to use. + +```c++ +#include +#include +#include +#include "encoders/as5600/MagneticSensorAS5600.h" + +MagneticSensorAS5600 sensor; + +void setup() { + sensor.init(); +} + +long ts; + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` + +You can use a different I2C bus by passing a pointer to its TwoWire object in the init method: + +```c++ +MagneticSensorAS5600 sensor1; +MagneticSensorAS5600 sensor2; +TwoWire myI2C; + +void setup() { + + ... + + sensor1.init(&myI2C); + sensor2.init(&Wire2); +} +``` + +Using the sensor without releasing the bus should give you considerably more speed. Setting closeTransactions to false means the sensor will not release the bus between angle reads, and also will not re-write the register to the device, which boosts angle reading throughput significantly. + +```c++ +MagneticSensorAS5600 sensor; + +void setup() { + sensor.closeTransactions = false; + sensor.init(); +} +``` + +The sensor's other registers are exposed. Note that using the setter functions to set register values only performs a normal write, not a permanent programing of the register. Permanent programming (BURN function) is not supported by this driver. + +```c++ + +MagneticSensorAS5600 sensor; + +void setup() { + sensor.closeTransactions = false; + sensor.init(); + + uint16_t magnitude = sensor.readMagnitude(); + AS5600Status status = sensor.readStatus(); + // print the values or something +} + +``` + +The AS5600L has a default address of 0x40, and you can set the I2C address. To temporarily change the address (resets to default on restart): + +```c++ + +MagneticSensorAS5600 sensor(0x40); // default address of AS5600L is 0x40 + +void setup() { + Serial.begin(115200); + delay(2000); + + sensor.closeTransactions = true; // use normal transactons + sensor.init(); + sensor.setI2CAddr(0x41); // set address to 0x41 + sensor.setI2CUpdt(0x41); // i2c address is now 0x41 + sensor._address = 0x41; + delay(10); + // sensor is using new address + uint16_t magnitude = sensor.readMagnitude(); +} +``` + + +You can program an I2C address permanently (AS5600L only). The address will remain after reset. This programming operation can only be done once. + +```c++ + +MagneticSensorAS5600 sensor(0x40); // default address of AS5600L is 0x40 + +void setup() { + Serial.begin(115200); + delay(2000); + + sensor.closeTransactions = true; // use normal transactons + sensor.init(); + + uint8_t addr = sensor.readI2CAddr(); // read I2C address, should be 0x40 + Serial.print("Current Address: "); + Serial.println(addr); + + sensor.setI2CAddr(0x41); // set address to 0x41 + sensor.burnSettings(); // permanently set new address + Serial.println("Permanently changed address to 0x41. Restart the system."); + while (1); +} + + +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp new file mode 100644 index 0000000..e3d579c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.cpp @@ -0,0 +1,257 @@ +#include "CalibratedSensor.h" + + +// CalibratedSensor() +// sensor - instance of original sensor object +CalibratedSensor::CalibratedSensor(Sensor& wrapped) : _wrapped(wrapped) +{ +}; + + +CalibratedSensor::~CalibratedSensor() +{ + delete calibrationLut; +}; + +// call update of calibrated sensor +void CalibratedSensor::update(){ + _wrapped.update(); + this->Sensor::update(); +}; + +// Retrieve the calibrated sensor angle +void CalibratedSensor::init() { + // assume wrapped sensor has already been initialized + this->Sensor::init(); // call superclass init +} + +// Retrieve the calibrated sensor angle +float CalibratedSensor::getSensorAngle(){ + // raw encoder position e.g. 0-2PI + float rawAngle = _wrapped.getMechanicalAngle(); + + // index of the bucket that rawAngle is part of. + // e.g. rawAngle = 0 --> bucketIndex = 0. + // e.g. rawAngle = 2PI --> bucketIndex = 128. + int bucketIndex = floor(rawAngle/(_2PI/n_lut)); + float remainder = rawAngle - ((_2PI/n_lut)*bucketIndex); + + // Extract the lower and upper LUT value in counts + float y0 = calibrationLut[bucketIndex]; + float y1 = calibrationLut[(bucketIndex+1)%n_lut]; + + // Linear Interpolation Between LUT values y0 and y1 using the remainder + // If remainder = 0, interpolated offset = y0 + // If remainder = 2PI/n_lut, interpolated offset = y1 + float interpolatedOffset = (((_2PI/n_lut)-remainder)/(_2PI/n_lut))*y0 + (remainder/(_2PI/n_lut))*y1; + + // add offset to the raw sensor count. Divide multiply by 2PI/CPR to get radians + float calibratedAngle = rawAngle+interpolatedOffset; + + // return calibrated angle in radians + return calibratedAngle; +} + +void CalibratedSensor::calibrate(BLDCMotor& motor){ + + Serial.println("Starting Sensor Calibration."); + + int _NPP = motor.pole_pairs; // number of pole pairs which is user input + const int n_ticks = 128*_NPP; // number of positions to be sampled per mechanical rotation. Multiple of NPP for filtering reasons (see later) + const int n2_ticks = 40; // increments between saved samples (for smoothing motion) + float deltaElectricalAngle = _2PI*_NPP/(n_ticks*n2_ticks); // Electrical Angle increments for calibration steps + float* error_f = new float[n_ticks](); // pointer to error array rotating forwards + // float* raw_f = new float[n_ticks](); // pointer to raw forward position + float* error_b = new float[n_ticks](); // pointer to error array rotating forwards + // float* raw_b = new float[n_ticks](); // pointer to raw backword position + float* error = new float[n_ticks](); // pointer to error array (average of forward & backward) + float* error_filt = new float[n_ticks](); // pointer to filtered error array (low pass filter) + const int window = 128; // window size for moving average filter of raw error + motor.zero_electric_angle = 0; // Set position sensor offset + + // find natural direction (this is copy of the init code) + // move one electrical revolution forward + for (int i = 0; i <=500; i++ ) { + float angle = _3PI_2 + _2PI * i / 500.0f; + motor.setPhaseVoltage(voltage_calibration, 0, angle); + _wrapped.update(); + _delay(2); + } + // take and angle in the middle + _wrapped.update(); + float mid_angle = _wrapped.getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >=0; i-- ) { + float angle = _3PI_2 + _2PI * i / 500.0f ; + motor.setPhaseVoltage(voltage_calibration, 0, angle); + _wrapped.update(); + _delay(2); + } + _wrapped.update(); + float end_angle = _wrapped.getAngle(); + motor.setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + int directionSensor; + if (mid_angle < end_angle) { + Serial.println("MOT: sensor_direction==CCW"); + directionSensor = -1; + motor.sensor_direction = Direction::CCW; + + } else{ + Serial.println("MOT: sensor_direction==CW"); + directionSensor = 1; + motor.sensor_direction = Direction::CW; + + } + + //Set voltage angle to zero, wait for rotor position to settle + // keep the motor in position while getting the initial positions + motor.setPhaseVoltage(voltage_calibration, 0, elec_angle); + _delay(1000); + _wrapped.update(); + float theta_init = _wrapped.getAngle(); + float theta_absolute_init = _wrapped.getMechanicalAngle(); + + /* + Start Calibration + Loop over electrical angles from 0 to NPP*2PI, once forward, once backward + store actual position and error as compared to electrical angle + */ + + /* + forwards rotation + */ + Serial.println("Rotating forwards"); + int k = 0; + for(int i = 0; i n_ticks-1) { + ind -= n_ticks;} + error_filt[i] += error[ind]/(float)window; + } + mean += error_filt[i]/n_ticks; + } + + // calculate offset index + int index_offset = floor(raw_offset/(_2PI/n_lut)); + + // Build Look Up Table + for (int i = 0; i (n_lut-1)){ + ind -= n_lut; + } + if(ind < 0 ){ + ind += n_lut; + } + calibrationLut[ind] = (float) (error_filt[i*_NPP] - mean); + //Serial.print(ind); + //Serial.print('\t'); + //Serial.println(calibrationLut[ind],5); + _delay(1); + } + + // de-allocate memory + delete error_filt; + delete error; + // delete raw_b; + delete error_b; + // delete raw_f; + delete error_f; + + Serial.println("Sensor Calibration Done."); + +} + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h new file mode 100644 index 0000000..414c53a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/CalibratedSensor.h @@ -0,0 +1,61 @@ +#ifndef __CALIBRATEDSENSOR_H__ +#define __CALIBRATEDSENSOR_H__ + +#include "common/base_classes/Sensor.h" +#include "BLDCMotor.h" +#include "common/base_classes/FOCMotor.h" + + +class CalibratedSensor: public Sensor{ + +public: + // constructor of class with pointer to base class sensor and driver + CalibratedSensor(Sensor& wrapped); + ~CalibratedSensor(); + + /* + Override the update function + */ + virtual void update() override; + + /** + * Calibrate method computes the LUT for the correction + */ + virtual void calibrate(BLDCMotor& motor); + + // voltage to run the calibration: user input + float voltage_calibration = 1; + +protected: + + /** + * getSenorAngle() method of CalibratedSensor class. + * This should call getAngle() on the wrapped instance, and then apply the correction to + * the value returned. + */ + virtual float getSensorAngle() override; + /** + * init method of CaibratedSensor - call after calibration + */ + virtual void init() override; + /** + * delegate instance of Sensor class + */ + Sensor& _wrapped; + + // lut size, currently constan. Perhaps to be made variable by user? + const int n_lut { 128 } ; + // create pointer for lut memory + float* calibrationLut = new float[n_lut](); + + // Init inital angles + float theta_actual { 0.0 }; + float elecAngle { 0.0 }; + float elec_angle { 0.0 }; + float theta_absolute_post { 0.0 }; + float theta_absolute_init { 0.0 }; + float theta_init { 0.0 }; + float avg_elec_angle { 0.0 }; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/README.md new file mode 100644 index 0000000..5768d92 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/calibrated/README.md @@ -0,0 +1,91 @@ +# Calibrated Sensor + +by [@MarethyuPrefect](https://github.com/MarethyuPrefect) + +A SimpleFOC Sensor wrapper implementation which adds sensor eccentricity calibration. + +Please also see our [forum thread](https://community.simplefoc.com/t/simplefoc-sensor-eccentricity-calibration/2212) on this topic. + + +When you mount your (magnetic) sensor on your frame or motor, there will always be a slight misalignment between magnet and sensor (measurement system). This misalignment between center of rotation and the center of the sensor is called the eccentricity error. + +As a result your measurement system output is non-linear with respect to the rotor of the motor. This will cause an error with respect to the ideal torque you attempt to create with the I_q vector as function of the position. You could interpret this as a disturbance on your control loop which you want to minimize for optimal performance. + +This calibration compensates the sensor reading in a feed forward fashion such that your performance improves. + + +## Hardware setup + +Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without calibration first. Once things are working and tuned without sensor calibration, you can add the CalibratedSensor to see if you get an improvement. + +Note that during calibration, the motor is turned through several turns, and should be in an unloaded condition. Please ensure your hardware setup can support the motor rotating through full turns. + + +## Softwate setup + +The CalibratedSensor acts as a wrapper to the actual sensor class. When creating the CalibratedSensor object, provide the real +sensor to the constructor of CalibratedSensor. + +First, initialize the real sensor instance as normal. Then, call calibrate() on the CalibratedSensor instance. Then link the +CalibratedSensor to the motor and call motor.initFOC(). + +The motor will then use the calibrated sensor instance. + + +```c++ +// magnetic sensor instance - SPI +MagneticSensorSPI sensor = MagneticSensorSPI(AS5048_SPI, PB6); +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// instantiate the calibrated sensor, providing the real sensor as a constructor argument +CalibratedSensor sensor_calibrated = CalibratedSensor(sensor); + +void setup() { + sensor.init(); + // Link motor to sensor + motor.linkSensor(&sensor); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + + // set voltage to run calibration + sensor_calibrated.voltage_calibration = 6; + // Running calibration + sensor_calibrated.calibrate(motor); + + //Serial.println("Calibrating Sensor Done."); + // Linking sensor to motor object + motor.linkSensor(&sensor_calibrated); + + // calibrated init FOC + motor.initFOC(); +} +``` + +Please see the more complete [example](https://github.com/simplefoc/Arduino-FOC-drivers/blob/master/examples/encoders/calibrated/sensor_calibration.ino) in our examples directory. + + +## Roadmap + +Possible future improvements we've thought about: + +- Improve memory usage and performance +- Make calibration able to be saved/restored + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp new file mode 100644 index 0000000..6e5eac0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.cpp @@ -0,0 +1,146 @@ +#include "MA330.h" + +MA330::MA330(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + +}; +MA330::~MA330() { + +}; + +void MA330::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } +}; + +float MA330::getCurrentAngle() { + return (readRawAngle() * _2PI)/MA330_CPR; +}; // angle in radians, return current value + +uint16_t MA330::readRawAngle() { + uint16_t angle = transfer16(0x0000); + return angle; +}; // 9-14bit angle value + +uint16_t MA330::getZero() { + uint16_t result = readRegister(MA330_REG_ZERO_POSITION_MSB)<<8; + result |= readRegister(MA330_REG_ZERO_POSITION_LSB); + return result; +}; +uint8_t MA330::getBiasCurrentTrimming() { + return readRegister(MA330_REG_BCT); +}; +bool MA330::isBiasCurrrentTrimmingX() { + return (readRegister(MA330_REG_ET) & 0x01)==0x01; +}; +bool MA330::isBiasCurrrentTrimmingY() { + return (readRegister(MA330_REG_ET) & 0x02)==0x02; +}; +uint16_t MA330::getPulsesPerTurn() { + uint16_t result = readRegister(MA330_REG_ILIP_PPT_LSB)>>6; + result |= ((uint16_t)readRegister(MA330_REG_PPT_MSB))<<2; + return result+1; +}; +uint8_t MA330::getIndexLength() { + return (readRegister(MA330_REG_ILIP_PPT_LSB)>>2)&0x0F; +}; +uint8_t MA330::getNumberPolePairs() { + return (readRegister(MA330_REG_NPP)>>5)&0x07;; +}; +uint8_t MA330::getRotationDirection() { + return (readRegister(MA330_REG_RD)>>7); +}; +uint8_t MA330::getFieldStrengthHighThreshold() { + return (readRegister(MA330_REG_MGLT_MGHT)>>2)&0x07; +}; +uint8_t MA330::getFieldStrengthLowThreshold() { + return (readRegister(MA330_REG_MGLT_MGHT)>>5)&0x07; +}; +uint8_t MA330::getFilterWidth() { + return readRegister(MA330_REG_FW); +}; +uint8_t MA330::getHysteresis() { + return readRegister(MA330_REG_HYS); +}; +FieldStrength MA330::getFieldStrength() { + return (FieldStrength)(readRegister(MA330_REG_MGH_MGL)>>6); +}; + + + +void MA330::setZero(uint16_t value) { + writeRegister(MA330_REG_ZERO_POSITION_MSB, value>>8); + writeRegister(MA330_REG_ZERO_POSITION_LSB, value&0x00FF); +}; +void MA330::setBiasCurrentTrimming(uint8_t value) { + writeRegister(MA330_REG_BCT, value); +}; +void MA330::setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled) { + uint8_t val = Xenabled ? 0x01 : 0x00; + val |= (Yenabled ? 0x02 : 0x00); + writeRegister(MA330_REG_ET, val); +}; +void MA330::setPulsesPerTurn(uint16_t value) { + uint16_t pptVal = value - 1; + writeRegister(MA330_REG_PPT_MSB, pptVal>>2); + uint8_t val = readRegister(MA330_REG_ILIP_PPT_LSB); + val &= 0x3F; + val |= (pptVal&0x03)<<6; + writeRegister(MA330_REG_ILIP_PPT_LSB, val); +}; +void MA330::setIndexLength(uint8_t value) { + uint8_t val = readRegister(MA330_REG_ILIP_PPT_LSB); + val &= 0xC0; + val |= ((value<<2)&0x3F); + writeRegister(MA330_REG_ILIP_PPT_LSB, val); +}; +void MA330::setNumberPolePairs(uint8_t value) { + uint8_t val = readRegister(MA330_REG_NPP); + val &= 0x1F; + val |= (value<<5); + writeRegister(MA330_REG_NPP, val); +}; +void MA330::setRotationDirection(uint8_t value) { + if (value==0) + writeRegister(MA330_REG_RD, 0x00); + else + writeRegister(MA330_REG_RD, 0x80); +}; +void MA330::setFilterWidth(uint8_t value) { + writeRegister(MA330_REG_FW, value); +}; +void MA330::setHysteresis(uint8_t value) { + writeRegister(MA330_REG_HYS, value); +}; +void MA330::setFieldStrengthThresholds(uint8_t high, uint8_t low) { + uint8_t val = (low<<5) | (high<<2); + writeRegister(MA330_REG_MGLT_MGHT, val); +}; + + +uint16_t MA330::transfer16(uint16_t outValue) { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return value; +}; +uint8_t MA330::readRegister(uint8_t reg) { + uint16_t cmd = 0x4000 | ((reg&0x001F)<<8); + uint16_t value = transfer16(cmd); + delayMicroseconds(1); + value = transfer16(0x0000); + return value>>8; +}; +uint8_t MA330::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x8000 | ((reg&0x1F)<<8) | value; + uint16_t result = transfer16(cmd); + delay(20); // 20ms delay required + result = transfer16(0x0000); + return result>>8; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.h new file mode 100644 index 0000000..4b89bc9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MA330.h @@ -0,0 +1,82 @@ +#ifndef __MA330_H__ +#define __MA330_H__ + + +#include "Arduino.h" +#include "SPI.h" + +enum FieldStrength : uint8_t { + FS_NORMAL = 0x00, + FS_LOW = 0x01, + FS_HIGH = 0x02, + FS_ERR = 0x03 // impossible state +}; + + +#define _2PI 6.28318530718f +#define MA330_CPR 65536.0f + +#define MA330_REG_ZERO_POSITION_LSB 0x00 +#define MA330_REG_ZERO_POSITION_MSB 0x01 +#define MA330_REG_BCT 0x02 +#define MA330_REG_ET 0x03 +#define MA330_REG_ILIP_PPT_LSB 0x04 +#define MA330_REG_PPT_MSB 0x05 +#define MA330_REG_MGLT_MGHT 0x06 +#define MA330_REG_NPP 0x07 +#define MA330_REG_RD 0x09 +#define MA330_REG_FW 0x0E +#define MA330_REG_HYS 0x10 +#define MA330_REG_MGH_MGL 0x1B + +#define MA330_BITORDER MSBFIRST + +static SPISettings MA330SPISettings(1000000, MA330_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + +class MA330 { +public: + MA330(SPISettings settings = MA330SPISettings, int nCS = -1); + virtual ~MA330(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 9-14bit angle value + + uint16_t getZero(); + uint8_t getBiasCurrentTrimming(); + bool isBiasCurrrentTrimmingX(); + bool isBiasCurrrentTrimmingY(); + uint16_t getPulsesPerTurn(); + uint8_t getIndexLength(); + uint8_t getNumberPolePairs(); + uint8_t getRotationDirection(); + uint8_t getFilterWidth(); + uint8_t getHysteresis(); + uint8_t getFieldStrengthHighThreshold(); + uint8_t getFieldStrengthLowThreshold(); + FieldStrength getFieldStrength(); + + void setZero(uint16_t); + void setBiasCurrentTrimming(uint8_t); + void setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled); + void setPulsesPerTurn(uint16_t); + void setIndexLength(uint8_t); + void setNumberPolePairs(uint8_t); + void setRotationDirection(uint8_t); + void setFilterWidth(uint8_t); + void setHysteresis(uint8_t); + void setFieldStrengthThresholds(uint8_t high, uint8_t low); + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + + uint16_t transfer16(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + uint8_t writeRegister(uint8_t reg, uint8_t value); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp new file mode 100644 index 0000000..a884065 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.cpp @@ -0,0 +1,26 @@ +#include "./MagneticSensorMA330.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA330::MagneticSensorMA330(int nCS, SPISettings settings) : MA330(settings, nCS) { + +} + + +MagneticSensorMA330::~MagneticSensorMA330(){ + +} + + +void MagneticSensorMA330::init(SPIClass* _spi) { + this->MA330::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorMA330::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)MA330_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h new file mode 100644 index 0000000..1befbdf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/MagneticSensorMA330.h @@ -0,0 +1,19 @@ +#ifndef __MAGNETIC_SENSOR_MA330_H__ +#define __MAGNETIC_SENSOR_MA330_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA330.h" + +class MagneticSensorMA330 : public Sensor, public MA330 { +public: + MagneticSensorMA330(int nCS = -1, SPISettings settings = MA330SPISettings); + virtual ~MagneticSensorMA330(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/README.md new file mode 100644 index 0000000..fcf635d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma330/README.md @@ -0,0 +1,66 @@ +# MA330 SimpleFOC driver + +While MA330 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this MA330-specific driver includes some optimisations: + +- access to the other registers of the MA330 +- this driver directly reads the angle with one call to SPI +- this will halve the number of 16-bit SPI transfers per simpleFOC loop iteration + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MA330/MagneticSensorMA330.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMA330 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorMA330 sensor1(SENSOR1_CS, true, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp new file mode 100644 index 0000000..7a1b7b8 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.cpp @@ -0,0 +1,125 @@ +#include "MA730.h" + +MA730::MA730(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + +}; +MA730::~MA730() { + +}; + +void MA730::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } +}; + +float MA730::getCurrentAngle() { + return (readRawAngle() * _2PI)/MA730_CPR; +}; // angle in radians, return current value + +uint16_t MA730::readRawAngle() { + uint16_t angle = transfer16(0x0000); + return angle; +}; // 14bit angle value + +uint16_t MA730::getZero() { + uint16_t result = readRegister(MA730_REG_ZERO_POSITION_MSB)<<8; + result |= readRegister(MA730_REG_ZERO_POSITION_LSB); + return result; +}; +uint8_t MA730::getBiasCurrentTrimming() { + return readRegister(MA730_REG_BCT); +}; +bool MA730::isBiasCurrrentTrimmingX() { + return (readRegister(MA730_REG_ET) & 0x01)==0x01; +}; +bool MA730::isBiasCurrrentTrimmingY() { + return (readRegister(MA730_REG_ET) & 0x02)==0x02; +}; +uint16_t MA730::getPulsesPerTurn() { + uint16_t result = readRegister(MA730_REG_ILIP_PPT_LSB)>>6; + result |= ((uint16_t)readRegister(MA730_REG_PPT_MSB))<<2; + return result+1; +}; +uint8_t MA730::getIndexLength() { + return (readRegister(MA730_REG_ILIP_PPT_LSB)>>2)&0x0F; +}; +uint8_t MA730::getRotationDirection() { + return (readRegister(MA730_REG_RD)>>7); +}; +uint8_t MA730::getFieldStrengthHighThreshold() { + return (readRegister(MA730_REG_MGLT_MGHT)>>2)&0x07; +}; +uint8_t MA730::getFieldStrengthLowThreshold() { + return (readRegister(MA730_REG_MGLT_MGHT)>>5)&0x07; +}; +FieldStrength MA730::getFieldStrength() { + return (FieldStrength)(readRegister(MA730_REG_MGH_MGL)>>6); +}; + + + +void MA730::setZero(uint16_t value) { + writeRegister(MA730_REG_ZERO_POSITION_MSB, value>>8); + writeRegister(MA730_REG_ZERO_POSITION_LSB, value&0x00FF); +}; +void MA730::setBiasCurrentTrimming(uint8_t value) { + writeRegister(MA730_REG_BCT, value); +}; +void MA730::setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled) { + uint8_t val = Xenabled ? 0x01 : 0x00; + val |= (Yenabled ? 0x02 : 0x00); + writeRegister(MA730_REG_ET, val); +}; +void MA730::setPulsesPerTurn(uint16_t value) { + uint16_t pptVal = value - 1; + writeRegister(MA730_REG_PPT_MSB, pptVal>>2); + uint8_t val = readRegister(MA730_REG_ILIP_PPT_LSB); + val &= 0x3F; + val |= (pptVal&0x03)<<6; + writeRegister(MA730_REG_ILIP_PPT_LSB, val); +}; +void MA730::setIndexLength(uint8_t value) { + uint8_t val = readRegister(MA730_REG_ILIP_PPT_LSB); + val &= 0xC0; + val |= ((value<<2)&0x3F); + writeRegister(MA730_REG_ILIP_PPT_LSB, val); +}; +void MA730::setRotationDirection(uint8_t value) { + if (value==0) + writeRegister(MA730_REG_RD, 0x00); + else + writeRegister(MA730_REG_RD, 0x80); +}; +void MA730::setFieldStrengthThresholds(uint8_t high, uint8_t low) { + uint8_t val = (low<<5) | (high<<2); + writeRegister(MA730_REG_MGLT_MGHT, val); +}; + + +uint16_t MA730::transfer16(uint16_t outValue) { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(outValue); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return value; +}; +uint8_t MA730::readRegister(uint8_t reg) { + uint16_t cmd = 0x4000 | ((reg&0x001F)<<8); + uint16_t value = transfer16(cmd); + delayMicroseconds(1); + value = transfer16(0x0000); + return value>>8; +}; +uint8_t MA730::writeRegister(uint8_t reg, uint8_t value) { + uint16_t cmd = 0x8000 | ((reg&0x1F)<<8) | value; + uint16_t result = transfer16(cmd); + delay(20); // 20ms delay required + result = transfer16(0x0000); + return result>>8; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.h new file mode 100644 index 0000000..56b0b5a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MA730.h @@ -0,0 +1,76 @@ +#ifndef __MA730_H__ +#define __MA730_H__ + + +#include "Arduino.h" +#include "SPI.h" + +enum FieldStrength : uint8_t { + FS_NORMAL = 0x00, + FS_LOW = 0x01, + FS_HIGH = 0x02, + FS_ERR = 0x03 // impossible state +}; + + +#define _2PI 6.28318530718f +#define MA730_CPR 65536.0f + +#define MA730_REG_ZERO_POSITION_LSB 0x00 +#define MA730_REG_ZERO_POSITION_MSB 0x01 +#define MA730_REG_BCT 0x02 +#define MA730_REG_ET 0x03 +#define MA730_REG_ILIP_PPT_LSB 0x04 +#define MA730_REG_PPT_MSB 0x05 +#define MA730_REG_MGLT_MGHT 0x06 +#define MA730_REG_RD 0x09 +#define MA730_REG_MGH_MGL 0x1B + +#define MA730_BITORDER MSBFIRST + +static SPISettings MA730SPISettings(1000000, MA730_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") +static SPISettings MA730SSISettings(4000000, MA730_BITORDER, SPI_MODE1); // @suppress("Invalid arguments") + + +class MA730 { +public: + MA730(SPISettings settings = MA730SPISettings, int nCS = -1); + virtual ~MA730(); + + virtual void init(SPIClass* _spi = &SPI); + + float getCurrentAngle(); // angle in radians, return current value + + uint16_t readRawAngle(); // 14bit angle value + uint16_t readRawAngleSSI(); // 14bit angle value + + uint16_t getZero(); + uint8_t getBiasCurrentTrimming(); + bool isBiasCurrrentTrimmingX(); + bool isBiasCurrrentTrimmingY(); + uint16_t getPulsesPerTurn(); + uint8_t getIndexLength(); + uint8_t getRotationDirection(); + uint8_t getFieldStrengthHighThreshold(); + uint8_t getFieldStrengthLowThreshold(); + FieldStrength getFieldStrength(); + + void setZero(uint16_t); + void setBiasCurrentTrimming(uint8_t); + void setBiasCurrrentTrimmingEnabled(bool Xenabled, bool Yenabled); + void setPulsesPerTurn(uint16_t); + void setIndexLength(uint8_t); + void setRotationDirection(uint8_t); + void setFieldStrengthThresholds(uint8_t high, uint8_t low); + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + + uint16_t transfer16(uint16_t outValue); + uint8_t readRegister(uint8_t reg); + uint8_t writeRegister(uint8_t reg, uint8_t value); +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp new file mode 100644 index 0000000..320618e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.cpp @@ -0,0 +1,26 @@ +#include "./MagneticSensorMA730.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA730::MagneticSensorMA730(int nCS, SPISettings settings) : MA730(settings, nCS) { + +} + + +MagneticSensorMA730::~MagneticSensorMA730(){ + +} + + +void MagneticSensorMA730::init(SPIClass* _spi) { + this->MA730::init(_spi); + this->Sensor::init(); +} + + +float MagneticSensorMA730::getSensorAngle() { + float angle_data = readRawAngle(); + angle_data = ( angle_data / (float)MA730_CPR) * _2PI; + // return the shaft angle + return angle_data; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h new file mode 100644 index 0000000..d579657 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730.h @@ -0,0 +1,19 @@ +#ifndef __MAGNETIC_SENSOR_MA730_H__ +#define __MAGNETIC_SENSOR_MA730_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA730.h" + +class MagneticSensorMA730 : public Sensor, public MA730 { +public: + MagneticSensorMA730(int nCS = -1, SPISettings settings = MA730SPISettings); + virtual ~MagneticSensorMA730(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp new file mode 100644 index 0000000..bf87b2b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.cpp @@ -0,0 +1,34 @@ +#include "./MagneticSensorMA730SSI.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMA730SSI::MagneticSensorMA730SSI(SPISettings settings) : settings(settings) { + +} + + +MagneticSensorMA730SSI::~MagneticSensorMA730SSI() { + +} + +void MagneticSensorMA730SSI::init(SPIClass* _spi) { + this->spi=_spi; + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorMA730SSI::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( angle_data / (float)MA730_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorMA730SSI::readRawAngleSSI() { + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + //uint16_t parity = spi->transfer(0x00); + spi->endTransaction(); + return (value<<1); //>>1)&0x3FFF; +}; // 14bit angle value diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h new file mode 100644 index 0000000..f45071f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/MagneticSensorMA730SSI.h @@ -0,0 +1,25 @@ +#ifndef __MAGNETIC_SENSOR_MA730SSI_H__ +#define __MAGNETIC_SENSOR_MA730SSI_H__ + + +#include "common/base_classes/Sensor.h" +#include "./MA730.h" + +class MagneticSensorMA730SSI : public Sensor { +public: + MagneticSensorMA730SSI(SPISettings settings = MA730SSISettings); + virtual ~MagneticSensorMA730SSI(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); + + uint16_t readRawAngleSSI(); + +private: + SPIClass* spi; + SPISettings settings; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/README.md new file mode 100644 index 0000000..1f88290 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/ma730/README.md @@ -0,0 +1,74 @@ +# MA730 SimpleFOC driver + +While MA730 absolute position magnetic rotary encoder is supported by the standard MagneticSensorSPI driver included in the base distribution, this MA730-specific driver includes some optimisations: + +- access to the other registers of the MA730 +- this driver directly reads the angle with one call to SPI +- this will halve the number of 16-bit SPI transfers per simpleFOC loop iteration + + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/ma730/MagneticSensorMA730.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMA730 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some options: + +```c++ +MagneticSensorMA730 sensor1(SENSOR1_CS, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(&SPI2); +} +``` + +Here's how you can use it: + +```c++ + // update the sensor (only needed if using the sensor without a motor) + sensor1.update(); + + // get the angle, in radians, including full rotations + float a1 = sensor1.getAngle(); + + // get the velocity, in rad/s - note: you have to call getAngle() on a regular basis for it to work + float v1 = sensor1.getVelocity(); + + // get the angle, in radians, no full rotations + float a2 = sensor1.getCurrentAngle(); + + // get the raw 14 bit value + uint16_t raw = sensor1.readRawAngle(); + + // get the field strength + FieldStrength fs = sensor1.getFieldStrength(); + Serial.print("Field strength: "); + Serial.println(fs); + + // set pulses per turn for encoder mode + sensor1.setPulsesPerTurn(999); // set to 999 if we want 1000 PPR == 4000 CPR +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp new file mode 100644 index 0000000..01e8d97 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.cpp @@ -0,0 +1,42 @@ +#include "./MagneticSensorMT6701SSI.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorMT6701SSI::MagneticSensorMT6701SSI(int nCS, SPISettings settings) : settings(settings), nCS(nCS) { + +} + + +MagneticSensorMT6701SSI::~MagneticSensorMT6701SSI() { + +} + +void MagneticSensorMT6701SSI::init(SPIClass* _spi) { + this->spi=_spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } + this->spi->begin(); + this->Sensor::init(); +} + +// check 40us delay between each read? +float MagneticSensorMT6701SSI::getSensorAngle() { + float angle_data = readRawAngleSSI(); + angle_data = ( angle_data / (float)MT6701_CPR ) * _2PI; + // return the shaft angle + return angle_data; +} + + +uint16_t MagneticSensorMT6701SSI::readRawAngleSSI() { + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + uint16_t value = spi->transfer16(0x0000); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + return (value>>MT6701_DATA_POS)&0x3FFF; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h new file mode 100644 index 0000000..32abd1d --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/MagneticSensorMT6701SSI.h @@ -0,0 +1,43 @@ +#ifndef __MAGNETIC_SENSOR_MT6701_SSI_H__ +#define __MAGNETIC_SENSOR_MT6701_SSI_H__ + +#include "Arduino.h" +#include "SPI.h" +#include "common/base_classes/Sensor.h" + + +#define MT6701_CPR 16384.0f + +#define MT6701_BITORDER MSBFIRST + +#if defined(TARGET_RP2040)||defined(ESP_H)||defined(CORE_TEENSY) +#define MT6701_DATA_POS 1 +#else +#define MT6701_DATA_POS 2 +#endif + +// Use SPI mode 2, capture on falling edge. First bit is not valid data, so have to read 25 bits to get a full SSI frame. +// SSI frame is 1 bit ignore, 14 bits angle, 4 bit status and 6 bit CRC. +static SPISettings MT6701SSISettings(1000000, MT6701_BITORDER, SPI_MODE2); // @suppress("Invalid arguments") + + + +class MagneticSensorMT6701SSI : public Sensor { +public: + MagneticSensorMT6701SSI(int nCS = -1, SPISettings settings = MT6701SSISettings); + virtual ~MagneticSensorMT6701SSI(); + + virtual void init(SPIClass* _spi = &SPI); + + float getSensorAngle() override; // angle in radians, return current value + +protected: + uint16_t readRawAngleSSI(); + + SPISettings settings; + SPIClass* spi; + int nCS = -1; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/README.md new file mode 100644 index 0000000..df04808 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6701/README.md @@ -0,0 +1,82 @@ +# MT6701 SimpleFOC driver + +:warning: work in progress... SSI driver is working. I2C not yet complete. + +Due to the peculiarities of its interfaces, this very versatile sensor is not directly supported by SimpleFOC's SPI or I2C magnetic sensor implementations. This folder contains dedicated SimpleFOC sensor drivers for the MT6701 for I2C and SSI. + +Note: the ABZ, UVW and Analog outputs of this sensor are supported by the standard SimpleFOC encoder, hall-sensor or analog sensor classes respectively. + +:warning: Note: the I2C output of this sensor is probably too slow for high performance motor control, but could be useful to program the sensor IC, and to read the absolute angle values for intializing ABZ or UVW modes. + +## Hardware setup + +For I2C, connect the sensor as normal for I2C, using the SDA and SCL lines. + +:warning: Note: to program the sensor via I2C, it has to be operated at 5V. + +For SSI, connect the CSN line to the nCS output of your MCU, the CLK line to the SCLK output of the MCU, and the DO line to the CIPO (MISO) input of the MCU. + +In my tests, the sensor was not able to work correctly together with other SPI devices on the same bus, but your experience might differ from mine. + +## Software setup + +### SSI code sample + +```c++ +#include "Arduino.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MT6701/MagneticSensorMT6701SSI.h" + + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorMT6701SSI sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +To use a custom SPI bus: + +```c++ +void setup() { + sensor1.init(&SPI2); +} +``` + + + +### I2C code sample + +I2C usage is quite simple: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/MT6701/MagneticSensorMT6701I2C.h" + +MagneticSensorMT6701I2C sensor1(); + + +void setup() { + sensor1.init(); +} + +``` + +If you've programmed a different I2C address or want to use a different I2C bus you can: + +```c++ +#define I2C_ADDR 0x70 +MagneticSensorMT6701I2C sensor1(I2C_ADDR); + + +void setup() { + sensor1.init(&Wire2); +} +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp new file mode 100644 index 0000000..d874bad --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.cpp @@ -0,0 +1,56 @@ + +#include "MT6816.h" + +MT6816::MT6816(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { +}; + +MT6816::~MT6816() { +}; + +void MT6816::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + spi->begin(); + } +}; + +uint16_t MT6816::readRawAngle() { + uint16_t angle_data = 0; + angle_data = spi_transfer16(MT6816_READ_REG_03) << 8; + angle_data |= spi_transfer16(MT6816_READ_REG_04); + + if ((angle_data & MT6816_NO_MAGNET_WARNING_BIT) == MT6816_NO_MAGNET_WARNING_BIT) { + this->no_magnetic_reading = true; + } else { + this->no_magnetic_reading = false; + } + + if (!this->parityCheck(angle_data)) { + return 0; + } + + return (angle_data >> 2); +} + +bool MT6816::parityCheck(uint16_t data) { + data ^= data >> 8; + data ^= data >> 4; + data ^= data >> 2; + data ^= data >> 1; + + return (~data) & 1; +} + +uint16_t MT6816::spi_transfer16(uint16_t outdata) { + if (nCS>=0) + digitalWrite(nCS, 0); + spi->beginTransaction(settings); + uint16_t result = spi->transfer16(outdata); + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, 1); + + return result; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h new file mode 100644 index 0000000..68b448a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MT6816.h @@ -0,0 +1,40 @@ + +#ifndef MT6816_H +#define MT6816_H + +#include "Arduino.h" +#include "SPI.h" + +#define _2PI 6.28318530718f +#define MT6816_CPR 16384.0f + +#define MT6816_READ_REG_03 0x8300 +#define MT6816_READ_REG_04 0x8400 + +#define MT6816_NO_MAGNET_WARNING_BIT 0x0002 +#define MT6816_BITORDER MSBFIRST + + +static SPISettings MT6816SPISettings(1000000, MT6816_BITORDER, SPI_MODE3); + +class MT6816 { +public: + MT6816(SPISettings settings = MT6816SPISettings, int nCS = -1); + virtual ~MT6816(); + + virtual void init(SPIClass* _spi = &SPI); + uint16_t readRawAngle(); + bool isNoMagneticReading() { + return no_magnetic_reading; + } + +private: + bool parityCheck(uint16_t data); + uint16_t spi_transfer16(uint16_t outdata); + SPIClass* spi; + SPISettings settings; + bool no_magnetic_reading = false; + int nCS = -1; +}; + +#endif /* MT6816_H */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp new file mode 100644 index 0000000..4167d78 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.cpp @@ -0,0 +1,26 @@ + +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "MagneticSensorMT6816.h" + + +MagneticSensorMT6816::MagneticSensorMT6816(int nCS, SPISettings settings) : MT6816(settings, nCS) { +} + +MagneticSensorMT6816::~MagneticSensorMT6816(){ +} + +void MagneticSensorMT6816::init(SPIClass* _spi) { + this->MT6816::init(_spi); + this->Sensor::init(); +} + +float MagneticSensorMT6816::getSensorAngle() { + uint16_t raw_angle_data = readRawAngle(); + + if (this->MT6816::isNoMagneticReading()) { + return 0; + } + + return static_cast(raw_angle_data) / MT6816_CPR * _2PI; +} \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h new file mode 100644 index 0000000..51628fc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6816/MagneticSensorMT6816.h @@ -0,0 +1,17 @@ + +#ifndef MAGNETICSENSOR_MT6816_H +#define MAGNETICSENSOR_MT6816_H + +#include "common/base_classes/Sensor.h" +#include "MT6816.h" + +class MagneticSensorMT6816 : public Sensor, public MT6816 { +public: + MagneticSensorMT6816(int nCS = -1, SPISettings settings = MT6816SPISettings); + virtual ~MagneticSensorMT6816(); + + virtual float getSensorAngle() override; + virtual void init(SPIClass* _spi = &SPI); +}; + +#endif /* MAGNETICSENSOR_MT6816_H */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp new file mode 100644 index 0000000..aeaa200 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.cpp @@ -0,0 +1,278 @@ + +#include "./MT6835.h" +#include "common/foc_utils.h" + + +MT6835::MT6835(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + +MT6835::~MT6835() { + // nix +}; + + + +void MT6835::init(SPIClass* _spi) { + spi = _spi; + if (nCS >= 0) { + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + } + spi->begin(); +}; + + + + +float MT6835::getCurrentAngle(){ + return readRawAngle21() / (float)MT6835_CPR * _2PI; +}; + + + +uint32_t MT6835::readRawAngle21(){ + uint8_t data[6]; // transact 48 bits + data[0] = (MT6835_OP_ANGLE<<4); + data[1] = MT6835_REG_ANGLE1; + data[2] = 0; + data[3] = 0; + data[4] = 0; + data[5] = 0; + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + spi->transfer(data, 6); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + laststatus = data[4]&0x07; + return (data[2] << 13) | (data[3] << 5) | (data[4] >> 3); +}; + + +uint8_t MT6835::getStatus(){ + return laststatus; +}; + + +bool MT6835::setZeroFromCurrentPosition(){ + MT6835Command cmd; + cmd.cmd = MT6835_OP_ZERO; + cmd.addr = 0x000; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; + + +/** + * Wait 6s after calling this method + */ +bool MT6835::writeEEPROM(){ + delay(1); // wait at least 1ms + MT6835Command cmd; + cmd.cmd = MT6835_OP_PROG; + cmd.addr = 0x000; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; + + + + + +uint8_t MT6835::getBandwidth(){ + MT6835Options5 opts = { .reg = readRegister(MT6835_REG_OPTS5) }; + return opts.bw; +}; +void MT6835::setBandwidth(uint8_t bw){ + MT6835Options5 opts = { .reg = readRegister(MT6835_REG_OPTS5) }; + opts.bw = bw; + writeRegister(MT6835_REG_OPTS5, opts.reg); +}; + +uint8_t MT6835::getHysteresis(){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + return opts.hyst; +}; +void MT6835::setHysteresis(uint8_t hyst){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + opts.hyst = hyst; + setOptions3(opts); +}; + +uint8_t MT6835::getRotationDirection(){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + return opts.rot_dir; +}; +void MT6835::setRotationDirection(uint8_t dir){ + MT6835Options3 opts = { .reg = getOptions3().reg }; + opts.rot_dir = dir; + setOptions3(opts); +}; + + +uint16_t MT6835::getABZResolution(){ + uint8_t hi = readRegister(MT6835_REG_ABZ_RES1); + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return (hi << 6) | lo.abz_res_low; +}; +void MT6835::setABZResolution(uint16_t res){ + uint8_t hi = (res >> 6); + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.abz_res_low = (res & 0x3F); + writeRegister(MT6835_REG_ABZ_RES1, hi); + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +bool MT6835::isABZEnabled(){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return lo.abz_off==0; +}; +void MT6835::setABZEnabled(bool enabled){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.abz_off = enabled?0:1; + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +bool MT6835::isABSwapped(){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + return lo.ab_swap==1; +}; +void MT6835::setABSwapped(bool swapped){ + MT6835ABZRes lo = { + .reg = readRegister(MT6835_REG_ABZ_RES2) + }; + lo.ab_swap = swapped?1:0; + writeRegister(MT6835_REG_ABZ_RES2, lo.reg); +}; + + + +uint16_t MT6835::getZeroPosition(){ + uint8_t hi = readRegister(MT6835_REG_ZERO1); + MT6835Options0 lo = { + .reg = readRegister(MT6835_REG_ZERO2) + }; + return (hi << 4) | lo.zero_pos_low; +}; +void MT6835::setZeroPosition(uint16_t pos){ + uint8_t hi = (pos >> 4); + MT6835Options0 lo = { + .reg = readRegister(MT6835_REG_ZERO2) + }; + lo.zero_pos_low = pos & 0x0F; + writeRegister(MT6835_REG_ZERO1, hi); + writeRegister(MT6835_REG_ZERO2, lo.reg); +}; + + + +MT6835Options1 MT6835::getOptions1(){ + MT6835Options1 result = { + .reg = readRegister(MT6835_REG_OPTS1) + }; + return result; +}; +void MT6835::setOptions1(MT6835Options1 opts){ + writeRegister(MT6835_REG_OPTS1, opts.reg); +}; + + + +MT6835Options2 MT6835::getOptions2(){ + MT6835Options2 result = { + .reg = readRegister(MT6835_REG_OPTS2) + }; + return result; +}; +void MT6835::setOptions2(MT6835Options2 opts){ + MT6835Options2 val = getOptions2(); + val.nlc_en = opts.nlc_en; + val.pwm_fq = opts.pwm_fq; + val.pwm_pol = opts.pwm_pol; + val.pwm_sel = opts.pwm_sel; + writeRegister(MT6835_REG_OPTS2, val.reg); +}; + + + +MT6835Options3 MT6835::getOptions3(){ + MT6835Options3 result = { + .reg = readRegister(MT6835_REG_OPTS3) + }; + return result; +}; +void MT6835::setOptions3(MT6835Options3 opts){ + MT6835Options3 val = getOptions3(); + val.rot_dir = opts.rot_dir; + val.hyst = opts.hyst; + writeRegister(MT6835_REG_OPTS3, val.reg); +}; + + + +MT6835Options4 MT6835::getOptions4(){ + MT6835Options4 result = { + .reg = readRegister(MT6835_REG_OPTS4) + }; + return result; +}; +void MT6835::setOptions4(MT6835Options4 opts){ + MT6835Options4 val = getOptions4(); + val.gpio_ds = opts.gpio_ds; + val.autocal_freq = opts.autocal_freq; + writeRegister(MT6835_REG_OPTS4, val.reg); +}; + + + +uint32_t swap_bytes(uint32_t net) +{ + return __builtin_bswap32(net); +} + + + + + +void MT6835::transfer24(MT6835Command* outValue) { + uint32_t buff = swap_bytes(outValue->val); + if (nCS >= 0) + digitalWrite(nCS, LOW); + spi->beginTransaction(settings); + spi->transfer(&buff, 3); + spi->endTransaction(); + if (nCS >= 0) + digitalWrite(nCS, HIGH); + outValue->val = swap_bytes(buff); +}; +uint8_t MT6835::readRegister(uint16_t reg) { + MT6835Command cmd; + cmd.cmd = MT6835_OP_READ; + cmd.addr = reg; + transfer24(&cmd); + return cmd.data; +}; +bool MT6835::writeRegister(uint16_t reg, uint8_t value) { + MT6835Command cmd; + cmd.cmd = MT6835_OP_WRITE; + cmd.addr = reg; + cmd.data = value; + transfer24(&cmd); + return cmd.data == MT6835_WRITE_ACK; +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h new file mode 100644 index 0000000..5751901 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MT6835.h @@ -0,0 +1,215 @@ + + +#pragma once + + + +#include "Arduino.h" +#include "SPI.h" + +#define MT6835_OP_READ 0b0011 +#define MT6835_OP_WRITE 0b0110 +#define MT6835_OP_PROG 0b1100 +#define MT6835_OP_ZERO 0b0101 +#define MT6835_OP_ANGLE 0b1010 + +#define MT6835_CMD_MASK 0b111100000000000000000000 +#define MT6835_ADDR_MASK 0b000011111111111100000000 +#define MT6835_DATA_MASK 0b000000000000000011111111 + +#define MT6835_CPR 2097152 + +#define MT6835_STATUS_OVERSPEED 0x01 +#define MT6835_STATUS_WEAKFIELD 0x02 +#define MT6835_STATUS_UNDERVOLT 0x04 + +#define MT6835_WRITE_ACK 0x55 + +#define MT6835_REG_USERID 0x001 + +#define MT6835_REG_ANGLE1 0x003 +#define MT6835_REG_ANGLE2 0x004 +#define MT6835_REG_ANGLE3 0x005 +#define MT6835_REG_ANGLE4 0x006 + +#define MT6835_REG_ABZ_RES1 0x007 +#define MT6835_REG_ABZ_RES2 0x008 + +#define MT6835_REG_ZERO1 0x009 +#define MT6835_REG_ZERO2 0x00A + +#define MT6835_REG_OPTS0 0x00A +#define MT6835_REG_OPTS1 0x00B +#define MT6835_REG_OPTS2 0x00C +#define MT6835_REG_OPTS3 0x00D +#define MT6835_REG_OPTS4 0x00E +#define MT6835_REG_OPTS5 0x011 + +// NLC table, 192 bytes +#define MT6835_REG_NLC_BASE 0x013 + +#define MT6835_REG_CAL_STATUS 0x113 + + +union MT6835ABZRes { + struct { + uint8_t ab_swap:1; + uint8_t abz_off:1; + uint8_t abz_res_low:6; + }; + uint8_t reg; +}; + + + +union MT6835Options0 { + struct { + uint8_t z_pul_wid:3; + uint8_t z_edge:1; + uint8_t zero_pos_low:4; + }; + uint8_t reg; +}; + + + +union MT6835Options1 { + struct { + uint8_t uvw_res:4; + uint8_t uvw_off:1; + uint8_t uvw_mux:1; + uint8_t z_phase:2; + }; + uint8_t reg; +}; + + + +union MT6835Options2 { + struct { + uint8_t pwm_sel:3; + uint8_t pwm_pol:1; + uint8_t pwm_fq:1; + uint8_t nlc_en:1; + uint8_t reserved:2; + }; + uint8_t reg; +}; + + + +union MT6835Options3 { + struct { + uint8_t hyst:3; + uint8_t rot_dir:1; + uint8_t reserved:4; + }; + uint8_t reg; +}; + + + +union MT6835Options4 { + struct { + uint8_t reserved:4; + uint8_t autocal_freq:3; + uint8_t gpio_ds:1; + }; + uint8_t reg; +}; + + + +union MT6835Options5 { + struct { + uint8_t bw:3; + uint8_t reserved:5; + }; + uint8_t reg; +}; + + + + +union MT6835Command { + struct { + uint32_t unused:8; + uint32_t data:8; + uint32_t addr:12; + uint32_t cmd:4; + }; + uint32_t val; +}; + + + + +#define MT6835_BITORDER MSBFIRST + +static SPISettings MT6835SPISettings(1000000, MT6835_BITORDER, SPI_MODE3); // @suppress("Invalid arguments") + + + + +class MT6835 { +public: + MT6835(SPISettings settings = MT6835SPISettings, int nCS = -1); + virtual ~MT6835(); + + virtual void init(SPIClass* _spi = &SPI); + + + float getCurrentAngle(); // angle in radians, return current value + + uint32_t readRawAngle21(); // up to 21bit precision angle value + + + uint8_t getBandwidth(); + void setBandwidth(uint8_t bw); + + uint8_t getHysteresis(); + void setHysteresis(uint8_t hyst); + + uint8_t getRotationDirection(); + void setRotationDirection(uint8_t dir); + + uint16_t getABZResolution(); + void setABZResolution(uint16_t res); + + bool isABZEnabled(); + void setABZEnabled(bool enabled); + + bool isABSwapped(); + void setABSwapped(bool swapped); + + uint16_t getZeroPosition(); + void setZeroPosition(uint16_t pos); + + MT6835Options1 getOptions1(); + void setOptions1(MT6835Options1 opts); + + MT6835Options2 getOptions2(); + void setOptions2(MT6835Options2 opts); + + MT6835Options3 getOptions3(); + void setOptions3(MT6835Options3 opts); + + MT6835Options4 getOptions4(); + void setOptions4(MT6835Options4 opts); + + uint8_t getStatus(); + + bool setZeroFromCurrentPosition(); + bool writeEEPROM(); // wait 6s after calling this method + +private: + SPIClass* spi; + SPISettings settings; + int nCS = -1; + uint8_t laststatus = 0; + + void transfer24(MT6835Command* outValue); + uint8_t readRegister(uint16_t reg); + bool writeRegister(uint16_t reg, uint8_t value); + +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp new file mode 100644 index 0000000..73c8bbc --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.cpp @@ -0,0 +1,24 @@ + +#include "MagneticSensorMT6835.h" + + +MagneticSensorMT6835::MagneticSensorMT6835(int nCS, SPISettings settings) : Sensor(), MT6835(settings, nCS) { + // nix +}; + + +MagneticSensorMT6835::~MagneticSensorMT6835() { + // nix +}; + + +float MagneticSensorMT6835::getSensorAngle() { + return getCurrentAngle(); +}; + + +void MagneticSensorMT6835::init(SPIClass* _spi) { + this->MT6835::init(_spi); + this->Sensor::init(); +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h new file mode 100644 index 0000000..e67ab0a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/MagneticSensorMT6835.h @@ -0,0 +1,17 @@ + +#pragma once + +#include "common/base_classes/Sensor.h" +#include "./MT6835.h" + +class MagneticSensorMT6835 : public Sensor, public MT6835 { +public: + MagneticSensorMT6835(int nCS = -1, SPISettings settings = MT6835SPISettings); + virtual ~MagneticSensorMT6835(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI); +}; + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/README.md new file mode 100644 index 0000000..66cbb33 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/mt6835/README.md @@ -0,0 +1,64 @@ +# MT6835 SimpleFOC driver + +Driver for the MagnTek MT6835 precision magnetic rotary encoder. + +This sensor features support for up to 21 bit resolution (!) and speeds up to 120,000RPM. While its full precision requires calibration using an external calibration system, it is impressively precise even in uncalibrated state, and it offers an internal self-calibration mode whose precision is somewhere between the other two. + +It has ABZ, UVW and SPI interfaces, and this driver is for its SPI inteface. You can use its ABZ interface with our Encoder classes, but due to the high resolution the interrupt-based Encoder might cause high MCU load. You can use its UVW interface with our Hall Sensor classes. + +# Hardware setup + +Connect the sensor to an SPI bus on your MCU. Pay attention to the voltage levels needed by your PCBs. The nCS line should have a pull-up resistor. + +# Software setup + +Usage example: + +```c++ +#include + +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" + +#include "encoders/mt6835/MagneticSensorMT6835.h" + +#define SENSOR_nCS PB6 + +SPISettings myMT6835SPISettings(1000000, MT6835_BITORDER, SPI_MODE3); +MagneticSensorMT6835 sensor = MagneticSensorMT6835(SENSOR_nCS, myMT6835SPISettings); + +long ts; + +void setup() { + sensor.init(); + ts = millis(); +} + +void loop() { + sensor.update(); + long now = millis(); + if (now - ts > 1000) { + ts = now; + SimpleFOCDebug::print("A: "); + SimpleFOCDebug::print(sensor.getAngle()); + SimpleFOCDebug::print(" V: "); + SimpleFOCDebug::println(sensor.getVelocity()); + } + delay(10); +} + +``` + +Set the zero position: + +```c++ +uint16_t pos = sensor.getZeroPosition(); // current value +sensor.setZeroFromCurrentPosition(); // set zero to current position +``` + +Set the ABZ resolution (needed if you want to use ABZ as the default is 1): + +```c++ +sensor.setABZResolution(2048); +``` + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp new file mode 100644 index 0000000..a127358 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.cpp @@ -0,0 +1,24 @@ + +#include "./MagneticSensorSC60228.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + +MagneticSensorSC60228::MagneticSensorSC60228(int nCS, SPISettings settings) : SC60228(settings, nCS){ + // nix +}; +MagneticSensorSC60228::~MagneticSensorSC60228(){ }; + + + +float MagneticSensorSC60228::getSensorAngle(){ + SC60228Angle angle_data = readRawAngle(); + float result = ( angle_data.angle / (float)SC60228_CPR ) * _2PI; + return result; +}; + + + +void MagneticSensorSC60228::init(SPIClass* _spi){ + this->SC60228::init(_spi); + this->Sensor::init(); +}; \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h new file mode 100644 index 0000000..7720182 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/MagneticSensorSC60228.h @@ -0,0 +1,20 @@ +#ifndef __MAGNETICSENSORSC60228_H__ +#define __MAGNETICSENSORSC60228_H__ + + +#include "common/base_classes/Sensor.h" +#include "./SC60228.h" + + +class MagneticSensorSC60228 : public Sensor, public SC60228 { +public: + MagneticSensorSC60228(int nCS = -1, SPISettings settings = SC60228SPISettings); + virtual ~MagneticSensorSC60228(); + + virtual float getSensorAngle() override; + + virtual void init(SPIClass* _spi = &SPI) override; +}; + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/README.md new file mode 100644 index 0000000..49f7901 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/README.md @@ -0,0 +1,47 @@ +# SC60228 SimpleFOC driver + +Driver for the Semiment 12bit magnetic encoder IC SC60288. According to specs it should support 12 bit accuracy, 10 bit effective accuracy and up to 20kRPM. + +Link to [Datasheet](https://semiment.com/wp-content/uploads/2021/04/SC60228_EN-VA1.0.pdf) + +The encoder and this driver were tested with a small gimbal motor and MKR1000 (SAMD21 MCU). + +## Hardware setup + +Connect as per normal for your SPI bus. No special hardware setup is needed to use this driver. + +## Software setup + +Its actually easier to use than the standard SPI sensor class, because it is less generic: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/sc60228/MagneticSensorSC60228.h" + +#define SENSOR1_CS 5 // some digital pin that you're using as the nCS pin +MagneticSensorSC60228 sensor1(SENSOR1_CS); + + +void setup() { + sensor1.init(); +} +``` + +Set some SPI options: + +```c++ +SPISettings mySPISettings(1000000, SC60228_BITORDER, SPI_MODE0); // lower speed to 1Mhz +MagneticSensorSC60228 sensor1(SENSOR1_CS, mySPISettings); +``` + +Use another SPI bus: + +```c++ +void setup() { + sensor1.init(SPI2); +} +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp new file mode 100644 index 0000000..4c63920 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.cpp @@ -0,0 +1,57 @@ + +#include "./SC60228.h" + + +SC60228::SC60228(SPISettings settings, int nCS) : settings(settings), nCS(nCS) { + // nix +}; + +SC60228::~SC60228() { }; + +void SC60228::init(SPIClass* _spi) { + spi = _spi; + if (nCS>=0) + pinMode(nCS, OUTPUT); + digitalWrite(nCS, HIGH); + spi->begin(); + readRawAngle(); +}; + + + +SC60228Angle SC60228::readRawAngle(){ + SC60228Angle result; + result.reg = spi_transfer16(0x0000); + errorflag = (result.err==1); + // TODO check parity + // Serial.print("0x"); + // Serial.print(result.angle, HEX); + // Serial.print(" - 0x"); + // Serial.println(result.reg, HEX); + return result; +}; + + + +bool SC60228::isError() { + return errorflag; +}; + + + +uint16_t SC60228::spi_transfer16(uint16_t outdata){ + uint16_t result; + if (nCS>=0) + digitalWrite(nCS, LOW); + // min delay here: 250ns + spi->beginTransaction(settings); + result = spi->transfer16(outdata); + // min delay here: clock period / 2 + spi->endTransaction(); + if (nCS>=0) + digitalWrite(nCS, HIGH); + // min delay until next read: 250ns + return result; +}; + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h new file mode 100644 index 0000000..03b4e50 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/sc60228/SC60228.h @@ -0,0 +1,50 @@ + +#ifndef __SC60228_H__ +#define __SC60228_H__ + +#include "Arduino.h" +#include "SPI.h" + + +typedef union { + struct { + uint16_t parity:1; + uint16_t df2:1; + uint16_t df1:1; + uint16_t err:1; + uint16_t angle:12; + }; + uint16_t reg; +} SC60228Angle; + + +#define SC60228_CPR 4096 +#define SC60228_BITORDER MSBFIRST + +static SPISettings SC60228SPISettings(8000000, SC60228_BITORDER, SPI_MODE0); // @suppress("Invalid arguments") + + + +class SC60228 { +public: + SC60228(SPISettings settings = SC60228SPISettings, int nCS = -1); + virtual ~SC60228(); + + virtual void init(SPIClass* _spi = &SPI); + + SC60228Angle readRawAngle(); // 12bit angle value + error flag + bool isError(); // true if the last call to readRawAngle() returned an error + +protected: + uint16_t spi_transfer16(uint16_t outdata); + + SPIClass* spi; + SPISettings settings; + bool errorflag = false; + int nCS = -1; +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/README.md new file mode 100644 index 0000000..733284c --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/README.md @@ -0,0 +1,69 @@ +# Smoothing Sensor + +by [@dekutree64](https://github.com/dekutree64) + +A SimpleFOC Sensor wrapper implementation which adds angle extrapolation + +Please also see our [forum thread](https://community.simplefoc.com/t/smoothingsensor-experimental-sensor-angle-extrapoltion/3105) on this topic. + + +SmoothingSensor is a wrapper class which is inserted inbetween a sensor and motor to provide better quality angle readings from sensors that are low resolution or are slow to communicate. It provides no improvement for sensors that are high resolution and quick to update. It uses the timestamp of the last angle reading from the sensor and the low-pass filtered velocity from the motor to predict what the angle should be at the current time. + + +## Hardware setup + +Connect your sensor as usual. Make sure the sensor is working 'normally' i.e. without smoothing first. Once things are working and tuned without smoothing, you can add the SmoothingSensor to see if you get an improvement. + + +## Softwate setup + +The SmoothingSensor acts as a wrapper to the actual sensor class. When creating the SmoothingSensor object, provide the real sensor to the constructor of SmoothingSensor. Initialize the real sensor instance as normal. Then link the SmoothingSensor to the motor and call motor.initFOC(). + + +```c++ +// BLDC motor & driver instance +BLDCMotor motor = BLDCMotor(11); +BLDCDriver3PWM driver = BLDCDriver3PWM(PB4,PC7,PB10,PA9); +// real sensor instance +MagneticSensorPWM sensor = MagneticSensorPWM(2, 4, 904); +// instantiate the smoothing sensor, providing the real sensor as a constructor argument +SmoothingSensor smooth = SmoothingSensor(sensor, motor); + +void doPWM(){sensor.handlePWM();} + +void setup() { + sensor.init(); + sensor.enableInterrupt(doPWM); + // Link motor to sensor + motor.linkSensor(&smooth); + // power supply voltage + driver.voltage_power_supply = 20; + driver.init(); + motor.linkDriver(&driver); + // aligning voltage + motor.voltage_sensor_align = 8; + motor.voltage_limit = 20; + // set motion control loop to be used + motor.controller = MotionControlType::torque; + + // use monitoring with serial + Serial.begin(115200); + // comment out if not needed + motor.useMonitoring(Serial); + motor.monitor_variables = _MON_VEL; + motor.monitor_downsample = 10; // default 10 + + // initialize motor + motor.init(); + motor.initFOC(); +} +``` + + +## Roadmap + +Possible future improvements we've thought about: + +- Add extrapolation of acceleration as well +- Switch to open loop control at very low speed with hall sensors, which otherwise move in steps even with smoothing. + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp new file mode 100644 index 0000000..ef0d322 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.cpp @@ -0,0 +1,57 @@ +#include "SmoothingSensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" + + +SmoothingSensor::SmoothingSensor(Sensor& s, const FOCMotor& m) : _wrapped(s), _motor(m) +{ +} + + +void SmoothingSensor::update() { + // Update sensor, with optional downsampling of update rate + if(sensor_cnt++ >= sensor_downsample) { + sensor_cnt = 0; + _wrapped.update(); + } + + // Copy state variables from the sensor + angle_prev = _wrapped.angle_prev; + angle_prev_ts = _wrapped.angle_prev_ts; + full_rotations = _wrapped.full_rotations; + + // Perform angle prediction, using low-pass filtered velocity. But don't advance more than + // pi/3 (equivalent to one step of block commutation) from the last true angle reading. + float dt = (_micros() - angle_prev_ts) * 1e-6f; + angle_prev += _motor.sensor_direction * _constrain(_motor.shaft_velocity * dt, -_PI_3 / _motor.pole_pairs, _PI_3 / _motor.pole_pairs); + + // Apply phase correction if needed + if (phase_correction != 0) { + if (_motor.shaft_velocity < -0) angle_prev -= _motor.sensor_direction * phase_correction / _motor.pole_pairs; + else if (_motor.shaft_velocity > 0) angle_prev += _motor.sensor_direction * phase_correction / _motor.pole_pairs; + } + + // Handle wraparound of the projected angle + if (angle_prev < 0) full_rotations -= 1, angle_prev += _2PI; + else if (angle_prev >= _2PI) full_rotations += 1, angle_prev -= _2PI; +} + + +float SmoothingSensor::getVelocity() { + return _wrapped.getVelocity(); +} + + +int SmoothingSensor::needsSearch() { + return _wrapped.needsSearch(); +} + + +float SmoothingSensor::getSensorAngle() { + return _wrapped.getSensorAngle(); +} + + +void SmoothingSensor::init() { + _wrapped.init(); +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h new file mode 100644 index 0000000..c10c2a0 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/smoothing/SmoothingSensor.h @@ -0,0 +1,46 @@ +#ifndef SMOOTHING_SENSOR_H +#define SMOOTHING_SENSOR_H + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/Sensor.h" + +/** + SmoothingSensor is a wrapper class which is inserted inbetween a sensor and motor to provide better + quality angle readings from sensors that are low resolution or are slow to communicate. It provides + no improvement for sensors that are high resolution and quick to update. + It uses the timestamp of the last angle reading from the sensor and the low-pass filtered velocity + from the motor to predict what the angle should be at the current time. +*/ + +class SmoothingSensor : public Sensor +{ + public: + /** + SmoothingSensor class constructor + @param s Wrapped sensor + @param m Motor that the SmoothingSensor will be linked to + */ + SmoothingSensor(Sensor& s, const FOCMotor& m); + + void update() override; + float getVelocity() override; + int needsSearch() override; + + // For sensors with slow communication, use these to poll less often + unsigned int sensor_downsample = 0; // parameter defining the ratio of downsampling for sensor update + unsigned int sensor_cnt = 0; // counting variable for downsampling + + // For hall sensors, the predicted angle is always 0 to 60 electrical degrees ahead of where it would be without + // smoothing, so offset it backward by 30 degrees (set this to -PI_6) to get the average in phase with the rotor + float phase_correction = 0; + + protected: + float getSensorAngle() override; + void init() override; + + Sensor& _wrapped; + const FOCMotor& _motor; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md new file mode 100644 index 0000000..1bdb1ca --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/README.md @@ -0,0 +1,49 @@ +# SimpleFOC Driver for STM32 hardware encoder + +This encoder driver uses the STM32 timer hardware to track the A/B impulses, which is much more efficient (and will support higher speeds) than the generic interrupt-based solution used by the standard encoder driver. + +Big thank you to @conroy-cheers for originally contributing this code in pull request #114 to the simplefoc repository. Due its hardware-specific nature we moved the code to this drivers repository. + +## Warning + +This code has not been tested much! Use at your own risk, your milage may vary. +I have tested on: STM32F401CC/TIM4-PB6,PB7 + +## Hardware setup + +The ABI signals of your encoder will have to be connected to the STM32 MCU in such a way that all signals are connected to the same timer. Consult your STM32 chip's datasheet for more information on which pins connect to which timer. + +Not all of the timers support the encoder mode, so check the datasheet which timers can be used. + +An excellent option can be to use the STM32CubeIDE software's pin assignment view to quickly check which pins connect to which timer. + +Note that the index (I) pin is currently not used. + + +## Software setup + +There is no need for interrupt functions or their configuration with this encoder class, so usage is quite simple: + +```c++ +#include "Arduino.h" +#include "Wire.h" +#include "SPI.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "encoders/stm32hwencoder/STM32HWEncoder.h" + + +#define ENCODER_PPR 500 +#define ENCODER_PIN_A PB6 +#define ENCODER_PIN_B PB7 +#define ENCODER_PIN_I PC13 + + +STM32HWEncoder encoder = STM32HWEncoder(ENCODER_PPR, ENCODER_PIN_A, ENCODER_PIN_B, ENCODER_PIN_I); + + +void setup() { + encoder.init(); +} + +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp new file mode 100644 index 0000000..0525e3f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.cpp @@ -0,0 +1,78 @@ +#include "STM32HWEncoder.h" + +#if defined(_STM32_DEF_) + +/* + HardwareEncoder(int cpr) +*/ +STM32HWEncoder::STM32HWEncoder(unsigned int _ppr, int8_t pinA, int8_t pinB, int8_t pinI) { + cpr = _ppr * 4; // 4x for quadrature + _pinA = digitalPinToPinName(pinA); + _pinB = digitalPinToPinName(pinB); + _pinI = digitalPinToPinName(pinI); +} + +/* + Shaft angle calculation +*/ +float STM32HWEncoder::getSensorAngle() { + return _2PI * encoder_handle.Instance->CNT / static_cast(cpr); +} + +// getter for index pin +int STM32HWEncoder::needsSearch() { return false; } + +// private function used to determine if encoder has index +int STM32HWEncoder::hasIndex() { return 0; } + +// encoder initialisation of the hardware pins +void STM32HWEncoder::init() { + // GPIO configuration + TIM_TypeDef *InstanceA = (TIM_TypeDef *)pinmap_peripheral(_pinA, PinMap_TIM); + TIM_TypeDef *InstanceB = (TIM_TypeDef *)pinmap_peripheral(_pinB, PinMap_TIM); + if (InstanceA != InstanceB) { + initialized = false; + return; + } + pinmap_pinout(_pinA, PinMap_TIM); + pinmap_pinout(_pinB, PinMap_TIM); + + // set up timer for encoder + encoder_handle.Init.Period = cpr - 1; + encoder_handle.Init.Prescaler = 0; + encoder_handle.Init.ClockDivision = 0; + encoder_handle.Init.CounterMode = TIM_COUNTERMODE_UP; + encoder_handle.Init.RepetitionCounter = 0; + encoder_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + + TIM_Encoder_InitTypeDef encoder_config; + + encoder_config.EncoderMode = TIM_ENCODERMODE_TI12; + + encoder_config.IC1Polarity = TIM_ICPOLARITY_RISING; + encoder_config.IC1Selection = TIM_ICSELECTION_DIRECTTI; + encoder_config.IC1Prescaler = TIM_ICPSC_DIV1; + encoder_config.IC1Filter = 0; + + encoder_config.IC2Polarity = TIM_ICPOLARITY_RISING; + encoder_config.IC2Selection = TIM_ICSELECTION_DIRECTTI; + encoder_config.IC2Prescaler = TIM_ICPSC_DIV1; + encoder_config.IC2Filter = 0; + + encoder_handle.Instance = InstanceA; // e.g. TIM4; + enableTimerClock(&encoder_handle); + + if (HAL_TIM_Encoder_Init(&encoder_handle, &encoder_config) != HAL_OK) { + initialized = false; + return; + } + + if (HAL_TIM_Encoder_Start(&encoder_handle, TIM_CHANNEL_1) != HAL_OK) { + initialized = false; + return; + } + + initialized = true; +} + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h new file mode 100644 index 0000000..98b14e5 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32hwencoder/STM32HWEncoder.h @@ -0,0 +1,36 @@ + +#pragma once + +#include + + +#if defined(_STM32_DEF_) + +#include +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" + +class STM32HWEncoder : public Sensor { + public: + /** + Encoder class constructor + @param ppr impulses per rotation (cpr=ppr*4) + */ + explicit STM32HWEncoder(unsigned int ppr, int8_t pinA, int8_t pinB, int8_t pinI=-1); + + void init() override; + int needsSearch() override; + int hasIndex(); // !< function returning 1 if encoder has index pin and 0 if not. + + bool initialized = false; + uint32_t cpr; //!< encoder cpr number + PinName _pinA, _pinB, _pinI; + + protected: + float getSensorAngle() override; + + TIM_HandleTypeDef encoder_handle; + +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md new file mode 100644 index 0000000..fd98be9 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/README.md @@ -0,0 +1,46 @@ + +# STM32MagneticSensorPWM + +STM32 MCU specific PWM sensor class. This class uses the STM32's hardware timers to precisely capture the PWM input signal, and doesn't use interrupts or have MCU overhead. + +:warning: this code is not yet well tested. + +## Setup + +Please use an advanced control or general purpose timer pin on your STM32. Connect the sensor's PWM output to the MCU's input pin. Usually, pull-ups or pull-downs are not needed, but check your sensor's datasheet. + +:warning: only tested on 16 bit timers. Code changes may be needed to make it work on 32 but timers. Avoid using TIM2 and TIM5 unless you want to test it. + +The sensor needs the values `min_ticks` and `max-ticks` to be configured correctly to convert the PWM input into an angle. These values will depend on the sensor, but also on the MCU's timer clock speed. + +To print the current tick value, use: + +``` +sensor.getDutyCycleTicks(); +``` + +By rotating the motor through several full turns while printing the ticks to the screen you will be able to determine the correct values empirically. + +## Usage + +``` +STM32MagneticSensorPWM sensor = STM32MagneticSensorPWM(PB7, 412, 6917); // sample values, yours will be different + +void setup() { + ... + sensor.init(); + ... +} +``` + +To use alternate function timers, set the PinName directly: + +``` +sensor._pin = PB_7_ALT1; // manually set a PinName to use alternate timer function +``` + +PWM sensor may have a slow update time (not more often than once per PWM-period, e.g. at the PWM frequency): + +``` +sensor.min_elapsed_time = 0.001; // 1ms minimum sample time for velocity +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp new file mode 100644 index 0000000..3c3e1d6 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.cpp @@ -0,0 +1,35 @@ + +#include "./STM32MagneticSensorPWM.h" + +#if defined(_STM32_DEF_) + +#include "common/foc_utils.h" + + +STM32MagneticSensorPWM::STM32MagneticSensorPWM(int pin, uint32_t _min_ticks, uint32_t _max_ticks) : STM32PWMInput(pin), max_ticks(_max_ticks), min_ticks(_min_ticks) { + +}; + + + +STM32MagneticSensorPWM::~STM32MagneticSensorPWM(){}; + + + +void STM32MagneticSensorPWM::init(){ + initialized = (STM32PWMInput::initialize()==0); + if(initialized) + Sensor::init(); +}; + + + +float STM32MagneticSensorPWM::getSensorAngle(){ + uint32_t ticks = getDutyCycleTicks(); + return (ticks - min_ticks) * _2PI / (max_ticks - min_ticks); +}; + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h new file mode 100644 index 0000000..3e5d335 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/stm32pwmsensor/STM32MagneticSensorPWM.h @@ -0,0 +1,28 @@ + +#pragma once + + +#include + + +#if defined(_STM32_DEF_) + +#include "common/base_classes/Sensor.h" +#include "utilities/stm32pwm/STM32PWMInput.h" + + +class STM32MagneticSensorPWM : public Sensor, public STM32PWMInput { + public: + STM32MagneticSensorPWM(int pin, uint32_t _min_ticks = 0, uint32_t _max_ticks = 0x0FFF); + ~STM32MagneticSensorPWM(); + + void init() override; + + uint32_t max_ticks = 0x0FFF; + uint32_t min_ticks = 0; + bool initialized = false; + protected: + float getSensorAngle() override; +}; + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp new file mode 100644 index 0000000..5947209 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.cpp @@ -0,0 +1,18 @@ + +#include "./MagneticSensorTLE5012B.h" + +#if defined(_STM32_DEF_) + +MagneticSensorTLE5012B::MagneticSensorTLE5012B(int data, int sck, int nCS) : TLE5012B(data, sck, nCS) { }; +MagneticSensorTLE5012B::~MagneticSensorTLE5012B(){ }; + +void MagneticSensorTLE5012B::init() { + this->TLE5012B::init(); + this->Sensor::init(); +}; + +float MagneticSensorTLE5012B::getSensorAngle() { + return getCurrentAngle(); +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h new file mode 100644 index 0000000..712c896 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/MagneticSensorTLE5012B.h @@ -0,0 +1,25 @@ + +#ifndef __MAGNETIC_SENSOR_TLE5012B_H__ +#define __MAGNETIC_SENSOR_TLE5012B_H__ + +#include "./STM32TLE5012B.h" + +#if defined(_STM32_DEF_) + +#include "common/base_classes/Sensor.h" + + + + +class MagneticSensorTLE5012B : public Sensor, public TLE5012B { + public: + MagneticSensorTLE5012B(int data, int sck, int nCS); + ~MagneticSensorTLE5012B(); + virtual void init() override; + virtual float getSensorAngle() override; +}; + + + +#endif +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/README.md new file mode 100644 index 0000000..3ce22c2 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/README.md @@ -0,0 +1,91 @@ +# SimpleFOC Driver for TLE5012B encoder for STM32 MCUs + +This driver code is compatible with STM32 MCUs. Based loosely on code from [here](https://github.com/xerootg/btt-s42b-simplefoc), with many thanks to @xerootg + +## Hardware setup + +Connect the data pin of the sensor to the MOSI (COPI) line of the MCU. The SCLK and nCS lines are connected as normal. + +## Software setup + +Sample code: + +``` +#include +#include +#include +#include "encoders/tle5012b/MagneticSensorTLE5012B.h" + +MagneticSensorTLE5012B sensor(PB15,PB13,PB12); + +long ts; + +void setup() { + Serial.begin(115200); + while (!Serial) ; + delay(2000); + + Serial.println("Initializing sensor..."); + + sensor.init(); + + Serial.println("Sensor initialized."); + + ts = millis(); +} + +void loop() { + sensor.update(); + if (millis() - ts > 1000) { + Serial.println(sensor.getAngle(), 3); + ts = millis(); + } + delay(1); +} +``` + +## Other MCUs + +Infineon provides a driver library compatible with Atmel AVR (Arduino UNO, Nano, etc...) and Infineon XMC MCUs. If you have one of those MCUs, you can use this library in conjunction with the GenericSensor class in SimpleFOC. + +``` +#include +#include + +Tle5012Ino Tle5012Sensor = Tle5012Ino(); +errorTypes checkError = NO_ERROR; + +float readMySensor(){ + // read my sensor + // return the angle value in radians in between 0 and 2PI + + Tle5012Sensor.getAngleValue(d); + d = (d + 180); + if ( d + 40.9 > 360 ) { + d = (d + 40.9) - 360; + } + else + d = d + 40.9; + + d = d * 0.0174533; + return d; +} +void initMySensor(){ + // do the init + checkError = Tle5012Sensor.begin(); +} + +// empty constructor +GenericSensor sensor = GenericSensor(readMySensor, initMySensor); + +void setup(){ +... + //init sensor and use it further with foc... + sensor.init(); +... +} +``` + +Other MCUs are currently not supported. The problem is that the sensor uses SPI in half-duplex mode, which is not supported by Arduino framework. So for each MCU type a custom driver has to be written to get the half-duplex SPI communication to work. There are currently no plans to support other MCUs on our side, but we will gladly accept pull requests :-) + +If you can find other libraries for other MCUs, you can use the GenericSensor approach to integrate them. \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp new file mode 100644 index 0000000..7fd89c7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.cpp @@ -0,0 +1,159 @@ + +#include "STM32TLE5012B.h" + +#if defined(_STM32_DEF_) + + +#include "utility/spi_com.h" +extern "C" uint32_t spi_getClkFreqInst(SPI_TypeDef *spi_inst); + + + +TLE5012B::TLE5012B(int data, int sck, int nCS, uint32_t freq) { + _data = data; + _sck = sck; + _nCS = nCS; + _freq = freq; +}; + +TLE5012B::~TLE5012B() { + +}; + + +void TLE5012B::init() { + pinMode(_nCS, OUTPUT); + digitalWrite(_nCS, HIGH); + + // initialize pins + GPIO_InitTypeDef gpio; + gpio.Pin = digitalPinToBitMask(_data); + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Speed = GPIO_SPEED_FREQ_HIGH; + HAL_GPIO_Init(digitalPinToPort(_data), &gpio); + gpio.Pin = digitalPinToBitMask(_sck); + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Speed = GPIO_SPEED_FREQ_HIGH; + HAL_GPIO_Init(digitalPinToPort(_sck), &gpio); + + SPI_TypeDef *spi_data = (SPI_TypeDef*)pinmap_peripheral(digitalPinToPinName(_data), PinMap_SPI_MOSI); + SPI_TypeDef *spi_sclk = (SPI_TypeDef*)pinmap_peripheral(digitalPinToPinName(_sck), PinMap_SPI_SCLK); + SPI_TypeDef *spi_inst = (SPI_TypeDef*)pinmap_merge_peripheral(spi_data, spi_sclk); + + pinmap_pinout(digitalPinToPinName(_data), PinMap_SPI_MOSI); + pinmap_pinout(digitalPinToPinName(_sck), PinMap_SPI_SCLK); + + #if defined SPI1_BASE + if (spi_inst == SPI1) { + __HAL_RCC_SPI1_CLK_ENABLE(); + __HAL_RCC_SPI1_FORCE_RESET(); + __HAL_RCC_SPI1_RELEASE_RESET(); + } + #endif + + #if defined SPI2_BASE + if (spi_inst == SPI2) { + __HAL_RCC_SPI2_CLK_ENABLE(); + __HAL_RCC_SPI2_FORCE_RESET(); + __HAL_RCC_SPI2_RELEASE_RESET(); + } + #endif + + #if defined SPI3_BASE + if (spi_inst == SPI3) { + __HAL_RCC_SPI3_CLK_ENABLE(); + __HAL_RCC_SPI3_FORCE_RESET(); + __HAL_RCC_SPI3_RELEASE_RESET(); + } + #endif + + #if defined SPI4_BASE + if (spi_inst == SPI4) { + __HAL_RCC_SPI4_CLK_ENABLE(); + __HAL_RCC_SPI4_FORCE_RESET(); + __HAL_RCC_SPI4_RELEASE_RESET(); + } + #endif + + #if defined SPI5_BASE + if (spi_inst == SPI5) { + __HAL_RCC_SPI5_CLK_ENABLE(); + __HAL_RCC_SPI5_FORCE_RESET(); + __HAL_RCC_SPI5_RELEASE_RESET(); + } + #endif + + #if defined SPI6_BASE + if (spi_inst == SPI6) { + __HAL_RCC_SPI6_CLK_ENABLE(); + __HAL_RCC_SPI6_FORCE_RESET(); + __HAL_RCC_SPI6_RELEASE_RESET(); + } + #endif + + _spi.Instance = spi_inst; + _spi.Init.Direction = SPI_DIRECTION_1LINE; + _spi.Init.Mode = SPI_MODE_MASTER; + _spi.Init.DataSize = SPI_DATASIZE_8BIT; + _spi.Init.CLKPolarity = SPI_POLARITY_LOW; + _spi.Init.CLKPhase = SPI_PHASE_2EDGE; + _spi.Init.NSS = SPI_NSS_SOFT; + _spi.Init.FirstBit = SPI_FIRSTBIT_MSB; + _spi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; + _spi.Init.CRCPolynomial = 7; + _spi.Init.TIMode = SPI_TIMODE_DISABLE; + #if defined(SPI_NSS_PULSE_DISABLE) + _spi.Init.NSSPMode = SPI_NSS_PULSE_DISABLE; + #endif + + uint32_t spi_freq = spi_getClkFreqInst(spi_inst); + if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV2_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV4_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV8_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV16_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV32_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV64_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; + } else if (_freq >= (spi_freq / SPI_SPEED_CLOCK_DIV128_MHZ)) { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_128; + } else { + _spi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; + } + + if (HAL_SPI_Init(&_spi) != HAL_OK) { + // setup error + Serial.println("TLE5012B setup error"); + } +}; + +uint16_t TLE5012B::readRawAngle() { + uint8_t data[4]; + readBytes(TLE5012B_ANGLE_REG, data, 2); + return (((uint16_t)data[0] << 8) | data[1]) & 0x7FFF; +}; + + +float TLE5012B::getCurrentAngle() { + return ((float)readRawAngle())/TLE5012B_CPR * _2PI; +}; // angle in radians, return current value + + +void TLE5012B::readBytes(uint16_t reg, uint8_t *data, uint8_t len) { + digitalWrite(_nCS, LOW); + + reg |= TLE5012B_READ_REGISTER + (len>>1); + uint8_t txbuffer[2] = { (uint8_t)(reg >> 8), (uint8_t)(reg & 0x00FF) }; + HAL_SPI_Transmit(&_spi, txbuffer, 2, 100); // TODO check return value for error, timeout + //delayMicroseconds(1); + HAL_SPI_Receive(&_spi, data, len + 2, 100); + + digitalWrite(_nCS, HIGH); +}; + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h new file mode 100644 index 0000000..6ae6c65 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/encoders/tle5012b/STM32TLE5012B.h @@ -0,0 +1,45 @@ +#ifndef __TLE5012B_H__ +#define __TLE5012B_H__ + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + + +#define _2PI 6.28318530718f +#define TLE5012B_CPR 32768.0f + +#define TLE5012B_READ_REGISTER 0x8000 + +enum TLE5012B_Register : uint16_t { + TLE5012B_ANGLE_REG = 0x0020, + TLE5012B_SPEED_REG = 0x0030 +}; + + + +class TLE5012B { + public: + TLE5012B(int data, int sck, int nCS, uint32_t freq = 1000000); + ~TLE5012B(); + virtual void init(); + + uint16_t readRawAngle(); + float getCurrentAngle(); // angle in radians, return current value + private: + + void readBytes(uint16_t reg, uint8_t *data, uint8_t len); + + int _data; + int _sck; + int _nCS; + uint32_t _freq; + SPI_HandleTypeDef _spi; +}; + + + + +#endif + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp new file mode 100644 index 0000000..b7fea27 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.cpp @@ -0,0 +1,548 @@ +#include "HybridStepperMotor.h" +#include "./communication/SimpleFOCDebug.h" + +// HybridStepperMotor(int pp) +// - pp - pole pair number +// - R - motor phase resistance +// - KV - motor kv rating (rmp/v) +// - L - motor phase inductance [H] +HybridStepperMotor::HybridStepperMotor(int pp, float _R, float _KV, float _inductance) + : FOCMotor() +{ + // number od pole pairs + pole_pairs = pp; + // save phase resistance number + phase_resistance = _R; + // save back emf constant KV = 1/K_bemf + // usually used rms + KV_rating = _KV * _SQRT2; + // save phase inductance + phase_inductance = _inductance; + + // torque control type is voltage by default + // current and foc_current not supported yet + torque_controller = TorqueControlType::voltage; +} + +/** + Link the driver which controls the motor +*/ +void HybridStepperMotor::linkDriver(BLDCDriver *_driver) +{ + driver = _driver; +} + +// init hardware pins +void HybridStepperMotor::init() +{ + if (!driver || !driver->initialized) + { + motor_status = FOCMotorStatus::motor_init_failed; + SIMPLEFOC_DEBUG("MOT: Init not possible, driver not initialized"); + return; + } + motor_status = FOCMotorStatus::motor_initializing; + SIMPLEFOC_DEBUG("MOT: Init"); + + // sanity check for the voltage limit configuration + if (voltage_limit > driver->voltage_limit) + voltage_limit = driver->voltage_limit; + // constrain voltage for sensor alignment + if (voltage_sensor_align > voltage_limit) + voltage_sensor_align = voltage_limit; + + // update the controller limits + if (_isset(phase_resistance)) + { + // velocity control loop controls current + PID_velocity.limit = current_limit; + } + else + { + PID_velocity.limit = voltage_limit; + } + P_angle.limit = velocity_limit; + + _delay(500); + // enable motor + SIMPLEFOC_DEBUG("MOT: Enable driver."); + enable(); + _delay(500); + + motor_status = FOCMotorStatus::motor_uncalibrated; +} + +// disable motor driver +void HybridStepperMotor::disable() +{ + // set zero to PWM + driver->setPwm(0, 0, 0); + // disable driver + driver->disable(); + // motor status update + enabled = 0; +} +// enable motor driver +void HybridStepperMotor::enable() +{ + // disable enable + driver->enable(); + // set zero to PWM + driver->setPwm(0, 0, 0); + // motor status update + enabled = 1; +} + +/** + * FOC functions + */ +int HybridStepperMotor::initFOC() +{ + int exit_flag = 1; + + motor_status = FOCMotorStatus::motor_calibrating; + + // sensor and motor alignment - can be skipped + // by setting motor.sensor_direction and motor.zero_electric_angle + _delay(500); + if (sensor) + { + exit_flag *= alignSensor(); + // added the shaft_angle update + sensor->update(); + shaft_angle = sensor->getAngle(); + } + else + SIMPLEFOC_DEBUG("MOT: No sensor."); + + if (exit_flag) + { + SIMPLEFOC_DEBUG("MOT: Ready."); + motor_status = FOCMotorStatus::motor_ready; + } + else + { + SIMPLEFOC_DEBUG("MOT: Init FOC failed."); + motor_status = FOCMotorStatus::motor_calib_failed; + disable(); + } + + return exit_flag; +} + +// Encoder alignment to electrical 0 angle +int HybridStepperMotor::alignSensor() +{ + int exit_flag = 1; // success + SIMPLEFOC_DEBUG("MOT: Align sensor."); + + // if unknown natural direction + if(sensor_direction==Direction::UNKNOWN) { + // check if sensor needs zero search + if (sensor->needsSearch()) + exit_flag = absoluteZeroSearch(); + // stop init if not found index + if (!exit_flag) + return exit_flag; + + // find natural direction + // move one electrical revolution forward + for (int i = 0; i <= 500; i++) + { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + // take and angle in the middle + sensor->update(); + float mid_angle = sensor->getAngle(); + // move one electrical revolution backwards + for (int i = 500; i >= 0; i--) + { + float angle = _3PI_2 + _2PI * i / 500.0f; + setPhaseVoltage(voltage_sensor_align, 0, angle); + sensor->update(); + _delay(2); + } + sensor->update(); + float end_angle = sensor->getAngle(); + setPhaseVoltage(0, 0, 0); + _delay(200); + // determine the direction the sensor moved + if (mid_angle == end_angle) + { + SIMPLEFOC_DEBUG("MOT: Failed to notice movement"); + return 0; // failed calibration + } + else if (mid_angle < end_angle) + { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CCW"); + sensor_direction = Direction::CCW; + } + else + { + SIMPLEFOC_DEBUG("MOT: sensor_direction==CW"); + sensor_direction = Direction::CW; + } + // check pole pair number + float moved = fabs(mid_angle - end_angle); + if (fabs(moved * pole_pairs - _2PI) > 0.5f) + { // 0.5f is arbitrary number it can be lower or higher! + SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI / moved); + } + else + SIMPLEFOC_DEBUG("MOT: PP check: OK!"); + } + else + SIMPLEFOC_DEBUG("MOT: Skip dir calib."); + + // zero electric angle not known + if (!_isset(zero_electric_angle)) + { + // align the electrical phases of the motor and sensor + // set angle -90(270 = 3PI/2) degrees + setPhaseVoltage(voltage_sensor_align, 0, _3PI_2); + _delay(700); + // read the sensor + sensor->update(); + // get the current zero electric angle + zero_electric_angle = 0; + zero_electric_angle = electricalAngle(); + _delay(20); + if (monitor_port) + { + SIMPLEFOC_DEBUG("MOT: Zero elec. angle: ", zero_electric_angle); + } + // stop everything + setPhaseVoltage(0, 0, 0); + _delay(200); + } + else + SIMPLEFOC_DEBUG("MOT: Skip offset calib."); + return exit_flag; +} + +// Encoder alignment the absolute zero angle +// - to the index +int HybridStepperMotor::absoluteZeroSearch() +{ + + SIMPLEFOC_DEBUG("MOT: Index search..."); + // search the absolute zero with small velocity + float limit_vel = velocity_limit; + float limit_volt = voltage_limit; + velocity_limit = velocity_index_search; + voltage_limit = voltage_sensor_align; + shaft_angle = 0; + while (sensor->needsSearch() && shaft_angle < _2PI) + { + angleOpenloop(1.5f * _2PI); + // call important for some sensors not to loose count + // not needed for the search + sensor->update(); + } + // disable motor + setPhaseVoltage(0, 0, 0); + // reinit the limits + velocity_limit = limit_vel; + voltage_limit = limit_volt; + // check if the zero found + if (monitor_port) + { + if (sensor->needsSearch()) + SIMPLEFOC_DEBUG("MOT: Error: Not found!"); + else + SIMPLEFOC_DEBUG("MOT: Success!"); + } + return !sensor->needsSearch(); +} + +// Iterative function looping FOC algorithm, setting Uq on the Motor +// The faster it can be run the better +void HybridStepperMotor::loopFOC() +{ + + // update sensor - do this even in open-loop mode, as user may be switching between modes and we could lose track + // of full rotations otherwise. + if (sensor) + sensor->update(); + + // if open-loop do nothing + if (controller == MotionControlType::angle_openloop || controller == MotionControlType::velocity_openloop) + return; + // shaft angle + shaft_angle = shaftAngle(); + + // if disabled do nothing + if (!enabled) + return; + + // Needs the update() to be called first + // This function will not have numerical issues because it uses Sensor::getMechanicalAngle() + // which is in range 0-2PI + electrical_angle = electricalAngle(); + + // set the phase voltage - FOC heart function :) + setPhaseVoltage(voltage.q, voltage.d, electrical_angle); +} + +// Iterative function running outer loop of the FOC algorithm +// Behavior of this function is determined by the motor.controller variable +// It runs either angle, velocity or voltage loop +// - needs to be called iteratively it is asynchronous function +// - if target is not set it uses motor.target value +void HybridStepperMotor::move(float new_target) +{ + + // downsampling (optional) + if (motion_cnt++ < motion_downsample) + return; + motion_cnt = 0; + + // shaft angle/velocity need the update() to be called first + // get shaft angle + // TODO sensor precision: the shaft_angle actually stores the complete position, including full rotations, as a float + // For this reason it is NOT precise when the angles become large. + // Additionally, the way LPF works on angle is a precision issue, and the angle-LPF is a problem + // when switching to a 2-component representation. + if (controller != MotionControlType::angle_openloop && controller != MotionControlType::velocity_openloop) + shaft_angle = shaftAngle(); // read value even if motor is disabled to keep the monitoring updated but not in openloop mode + // get angular velocity + shaft_velocity = shaftVelocity(); // read value even if motor is disabled to keep the monitoring updated + + // if disabled do nothing + if (!enabled) + return; + + // set internal target variable + if (_isset(new_target)) + target = new_target; + + // calculate the back-emf voltage if KV_rating available U_bemf = vel*(1/KV) + if (_isset(KV_rating)) + voltage_bemf = shaft_velocity / KV_rating / _RPM_TO_RADS; + // estimate the motor current if phase reistance available and current_sense not available + if (!current_sense && _isset(phase_resistance)) + current.q = (voltage.q - voltage_bemf) / phase_resistance; + + // choose control loop + switch (controller) + { + case MotionControlType::torque: + if (!_isset(phase_resistance)) + voltage.q = target; // if voltage torque control + else + voltage.q = target * phase_resistance + voltage_bemf; + voltage.q = _constrain(voltage.q, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-target * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::angle: + // angle set point + shaft_angle_sp = target; + // calculate velocity set point + shaft_velocity_sp = P_angle(shaft_angle_sp - shaft_angle); + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if voltage torque control + // if torque controlled through voltage + // use voltage if phase-resistance not provided + if (!_isset(phase_resistance)) + voltage.q = current_sp; + else + voltage.q = _constrain(current_sp * phase_resistance + voltage_bemf, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-current_sp * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity: + // velocity set point + shaft_velocity_sp = target; + // calculate the torque command + current_sp = PID_velocity(shaft_velocity_sp - shaft_velocity); // if current/foc_current torque control + // if torque controlled through voltage control + // use voltage if phase-resistance not provided + if (!_isset(phase_resistance)) + voltage.q = current_sp; + else + voltage.q = _constrain(current_sp * phase_resistance + voltage_bemf, -voltage_limit, voltage_limit); + // set d-component (lag compensation if known inductance) + if (!_isset(phase_inductance)) + voltage.d = 0; + else + voltage.d = _constrain(-current_sp * shaft_velocity * pole_pairs * phase_inductance, -voltage_limit, voltage_limit); + break; + case MotionControlType::velocity_openloop: + // velocity control in open loop + shaft_velocity_sp = target; + voltage.q = velocityOpenloop(shaft_velocity_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + case MotionControlType::angle_openloop: + // angle control in open loop + shaft_angle_sp = target; + voltage.q = angleOpenloop(shaft_angle_sp); // returns the voltage that is set to the motor + voltage.d = 0; // TODO d-component lag-compensation + break; + } +} + +void HybridStepperMotor::setPhaseVoltage(float Uq, float Ud, float angle_el) +{ + angle_el = _normalizeAngle(angle_el); + float _ca = _cos(angle_el); + float _sa = _sin(angle_el); + float center; + + switch (foc_modulation) { + case FOCModulationType::Trapezoid_120: + case FOCModulationType::Trapezoid_150: + // not handled + Ua = 0; + Ub = 0; + Uc = 0; + break; + case FOCModulationType::SinePWM: + // C phase is fixed at half-rail to provide bias point for A, B legs + Ua = (_ca * Ud) - (_sa * Uq); + Ub = (_sa * Ud) + (_ca * Uq); + + center = driver->voltage_limit / 2; + + Ua += center; + Ub += center; + Uc = center; + break; + + case FOCModulationType::SpaceVectorPWM: + // C phase moves in order to increase max bias on coils + uint8_t sector = floor(4.0 * angle_el / _PI) + 1; + Ua = (_ca * Ud) - (_sa * Uq); + Ub = (_sa * Ud) + (_ca * Uq); + + // Determine max/ min of [Ua, Ub, 0] based on phase angle. + switch (sector) + { + case 1: + case 8: + center = (driver->voltage_limit - Ua - Ub) / 2; + break; + + case 2: + center = (driver->voltage_limit - Ua - 0) / 2; + break; + + case 3: + center = (driver->voltage_limit - Ub - 0) / 2; + break; + + case 4: + case 5: + center = (driver->voltage_limit - Ub - Ua) / 2; + break; + + case 6: + center = (driver->voltage_limit - 0 - Ua) / 2; + break; + + case 7: + center = (driver->voltage_limit - 0 - Ub) / 2; + break; + + default: // this case does not occur, but compilers complain about uninitialized variables + center = (driver->voltage_limit - 0) / 2; + break; + } + + Ua += center; + Ub += center; + Uc = center; + break; + } + + driver->setPwm(Ua, Ub, Uc); +} + +// Function (iterative) generating open loop movement for target velocity +// - target_velocity - rad/s +// it uses voltage_limit variable +float HybridStepperMotor::velocityOpenloop(float target_velocity) +{ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if (Ts <= 0 || Ts > 0.5f) + Ts = 1e-3f; + + // calculate the necessary angle to achieve target velocity + shaft_angle = _normalizeAngle(shaft_angle + target_velocity * Ts); + // for display purposes + shaft_velocity = target_velocity; + + // use voltage limit or current limit + float Uq = voltage_limit; + if (_isset(phase_resistance)) + { + Uq = _constrain(current_limit * phase_resistance + fabs(voltage_bemf), -voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf)) / phase_resistance; + } + + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle(shaft_angle, pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} + +// Function (iterative) generating open loop movement towards the target angle +// - target_angle - rad +// it uses voltage_limit and velocity_limit variables +float HybridStepperMotor::angleOpenloop(float target_angle) +{ + // get current timestamp + unsigned long now_us = _micros(); + // calculate the sample time from last call + float Ts = (now_us - open_loop_timestamp) * 1e-6f; + // quick fix for strange cases (micros overflow + timestamp not defined) + if (Ts <= 0 || Ts > 0.5f) + Ts = 1e-3f; + + // calculate the necessary angle to move from current position towards target angle + // with maximal velocity (velocity_limit) + if (abs(target_angle - shaft_angle) > abs(velocity_limit * Ts)) + { + shaft_angle += _sign(target_angle - shaft_angle) * abs(velocity_limit) * Ts; + shaft_velocity = velocity_limit; + } + else + { + shaft_angle = target_angle; + shaft_velocity = 0; + } + + // use voltage limit or current limit + float Uq = voltage_limit; + if (_isset(phase_resistance)) + { + Uq = _constrain(current_limit * phase_resistance + fabs(voltage_bemf), -voltage_limit, voltage_limit); + // recalculate the current + current.q = (Uq - fabs(voltage_bemf)) / phase_resistance; + } + // set the maximal allowed voltage (voltage_limit) with the necessary angle + setPhaseVoltage(Uq, 0, _electricalAngle((shaft_angle), pole_pairs)); + + // save timestamp for next call + open_loop_timestamp = now_us; + + return Uq; +} diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h new file mode 100644 index 0000000..71e8792 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/HybridStepperMotor.h @@ -0,0 +1,112 @@ +/** + * @file HybridStepperMotor.h + * + */ + +#ifndef HybridStepperMotor_h +#define HybridStepperMotor_h + +#include "Arduino.h" +#include "common/base_classes/FOCMotor.h" +#include "common/base_classes/StepperDriver.h" +#include "common/base_classes/Sensor.h" +#include "common/foc_utils.h" +#include "common/time_utils.h" +#include "common/defaults.h" + +/** + Stepper Motor class +*/ +class HybridStepperMotor : public FOCMotor +{ +public: + /** + HybridStepperMotor class constructor + @param pp pole pair number + @param R motor phase resistance - [Ohm] + @param KV motor KV rating (1/K_bemf) - rpm/V + @param L motor phase inductance - [H] + */ + HybridStepperMotor(int pp, float R = NOT_SET, float KV = NOT_SET, float L = NOT_SET); + + /** + * Function linking a motor and a foc driver + * + * @param driver BLDCDriver handle for hardware peripheral control + */ + void linkDriver(BLDCDriver *driver); + + /** + * BLDCDriver link: + */ + BLDCDriver *driver; + + /** Motor hardware init function */ + void init() override; + /** Motor disable function */ + void disable() override; + /** Motor enable function */ + void enable() override; + + /** + * Function initializing FOC algorithm + * and aligning sensor's and motors' zero position + */ + int initFOC() override; + + /** + * Function running FOC algorithm in real-time + * it calculates the gets motor angle and sets the appropriate voltages + * to the phase pwm signals + * - the faster you can run it the better Arduino UNO ~1ms, Bluepill ~ 100us + */ + void loopFOC() override; + + /** + * Function executing the control loops set by the controller parameter of the HybridStepperMotor. + * + * @param target Either voltage, angle or velocity based on the motor.controller + * If it is not set the motor will use the target set in its variable motor.target + * + * This function doesn't need to be run upon each loop execution - depends of the use case + */ + void move(float target = NOT_SET) override; + + float Ua, Ub, Uc; //!< Phase voltages used for inverse Park and Clarke transform + + /** + * Method using FOC to set Uq to the motor at the optimal angle + * Heart of the FOC algorithm + * + * @param Uq Current voltage in q axis to set to the motor + * @param Ud Current voltage in d axis to set to the motor + * @param angle_el current electrical angle of the motor + */ + void setPhaseVoltage(float Uq, float Ud, float angle_el) override; + +private: + /** Sensor alignment to electrical 0 angle of the motor */ + int alignSensor(); + /** Motor and sensor alignment to the sensors absolute 0 angle */ + int absoluteZeroSearch(); + + // Open loop motion control + /** + * Function (iterative) generating open loop movement for target velocity + * it uses voltage_limit variable + * + * @param target_velocity - rad/s + */ + float velocityOpenloop(float target_velocity); + /** + * Function (iterative) generating open loop movement towards the target angle + * it uses voltage_limit and velocity_limit variables + * + * @param target_angle - rad + */ + float angleOpenloop(float target_angle); + // open loop variables + long open_loop_timestamp; +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md new file mode 100644 index 0000000..4176539 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/motors/HybridStepperMotor/README.md @@ -0,0 +1,45 @@ +# Three Phase / Hybrid Stepper Motor Class + +By tying two phases together, you can use a standard bipolar stepper motor with three-phase drivers! +Of course, it requires some different math for commutation. Another limitation is that with SinePWM the motor voltage is limited to 50% of the supply voltage. Using some tricks in SpaceVectorPWM, this max bias can be increased to about 70%. + +## Warning + +This code has not been tested much! +It has killed one B-G431B-ESC1 board before. +However, it has been succesfully used on a few different hardware platforms, so it does work. +BEMF may be the danger here, so take caution with fast decelerations. +Keep in mind that potentially twice the current can be sunk in the shorted phase as the two other legs. + +## Hardware setup + +Tie two of the phases of the motor together. This is phase "C". The other two phases are A & B. Phase C must go last in the driver constructor. + + +## Software setup + +Usage is quite simple: + +```c++ +#include "Arduino.h" +#include "SimpleFOC.h" +#include "SimpleFOCDrivers.h" +#include "motors/HybridStepperMotor/HybridStepperMotor.h" + +HybridStepperMotor motor = HybridStepperMotor(pole_pairs, phase_resistance, kv, winding_inductance); +BLDCDriver3PWM driver = BLDCDriver3PWM(phase_A, phase_B, phase_C, enable); + +void setup() { + driver.init(); + motor.linkDriver(&driver); + + motor.foc_modulation = FOCModulationType::SpaceVectorPWM; //or SinePWM + motor.init(); + motor.initFOC(); +} + +void loop(){ + motor.loopFOC(); + motor.move(); +} +``` \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/README.md new file mode 100644 index 0000000..9bd70fb --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/README.md @@ -0,0 +1,143 @@ +# SimpleFOC Settings Storage + +Code to persist calibration data and motor settings to and from storage. The SettingsStorage is abstract, and different implementations exist for different concete storage media. + +The SettingsStorage is based on the SimpleFOCRegisters abstraction, so this means you can load or store any of the registers (representing SimpleFOC settings) that is both readable and writeable (e.g. while the current shaft velocity is available as a register, you can't store it as a setting because its read-only). + +The SettingsStorage can save the settings for one or more motors, and you can customize which settings are saved. If you don't customize, only the motor +calibration will be stored (REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION). + +## Hardware requirements + +Obviously, to save settings you need a place to store them where they won't be lost when you power down. There can be many solutions to this question, and we attempt to provide some simple drivers for the common storage solutions people might want. + +But its also quite easy to extend the system to support your specific needs. + +Supported hardware: + +- [SAMD NVM](samd/) \ +Some SAMD chips have a built-in NVRAM flash, which can be written from user code. +- [CAT24 I2C Flash EEPROMs](i2c/) \ :warning: UNFINISHED +Simple programmable memories with I2C interface. Memory space is quite limited, but enough for storing settings. + +See Roadmap, below, for systems we may support in the future. + + +## Basic Usage + +Using SettingsStorage is simple. Just add a `settings.loadSettings()` after setting up your defaults, but before calling init() on the objects. And call `saveSettings()` after initialization of the motor has (successfully) completed. + +This example saves just the motor calibration, but you might want to add additional code to save other registers, or make your solution more user-friendly. See the other examples below. + +```c++ + +#include "settings/samd/SAMDNVMSettingsStorage.h" + +SAMDNVMSettingsStorage settings = SAMDNVMSettingsStorage(); +BLDCMotor motor = BLDCMotor(...); + +void setup() { + SimpleFOCDebug::enable(); // show debug messages from settings + + // initialize the settings early in the setup + settings.addMotor(&motor); + settings.init(); + + // first set your defaults + driver.voltage_power_supply = 12.0f; + driver.voltage_limit = 12.0f; // 12V driver limit, like PSU + motor.voltage_limit = 6.0f; // 6V motor limit + + // then try to load the settings + SettingsStatus loadStatus = settings.loadSettings(); + + // driver init code, etc... + ... + + // then init the motor + motor.init(); + motor.initFOC(); + + // and if the settings were not loaded earlier, then save them + if (motor.motor_status == FOCMotorStatus::motor_ready && loadStatus != SFOC_SETTINGS_SUCCESS) { + settings.saveSettings(); + } + + // any other initialization + ... +} + +``` + +### Choosing registers + +By default, only the calibration data (motor native direction, electrical zero position) are saved. To save additional registers, you can choose them before calling `SettingsStorage.init()`, as in the example below: + +```c++ + +SimpleFOCRegister settingsRegisters[] = { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION, REG_VEL_PID_P, REG_VEL_PID_I, REG_VEL_PID_D, REG_VEL_LPF_T, REG_VEL_LIMIT, REG_VEL_MAX_RAMP, REG_VOLTAGE_LIMIT, REG_MOTION_DOWNSAMPLE, REG_CONTROL_MODE, REG_TORQUE_MODE, REG_PHASE_RESISTANCE, REG_KV, REG_INDUCTANCE }; +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); +BLDCMotor motor = BLDCMotor(...); + +void setup() { + SimpleFOCDebug::enable(); // show debug messages from settings + + // initialize the settings early in the setup + settings.setRegisters(mySettingsRegisters, sizeof(mySettingsRegisters)); + settings.addMotor(&motor); + settings.init(); + ... +} +``` + + + +### Using settings with commander + +You can easily interact with the settings via the commander, for example to save your PID tuning values: + +```c++ + +SimpleFOCRegister settingsRegisters[] = { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION, REG_VEL_PID_P, REG_VEL_PID_I, REG_VEL_PID_D, REG_VEL_LPF_T }; + +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); + +Commander commander; + +void doSaveSettings(char *cmd) { settings.saveSettings(); } +void doLoadSettings(char *cmd) { settings.loadSettings(); } + + +void setup() { + ... + + settings.setRegisters(settingsRegisters, sizeof(settingsRegisters)); + settings.addMotor(&motor); + settings.init(); + + ... + + commander.addCommand('s', doSaveSettings, "Save settings"); + commander.addCommand('l', doLoadSettings, "Load settings"); + + ... + +} + + +void run() { + motor.move(); + motor.loopFOC(); + commander.run(); +} +``` + +## Roadmap + +The following ideas for storage backends are on the roadmap, with no definate schedule for implementation: + +- ESP32 SPIFFs Filesystem +- ESP32 Preferences +- Arduino EEPROM library +- In-Memory Buffer (for use with other libraries or abstractions) +- JSON String (for printing to console) diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.cpp new file mode 100644 index 0000000..5df2bcf --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.cpp @@ -0,0 +1,120 @@ + +#include "./SettingsStorage.h" +#include "communication/SimpleFOCDebug.h" + + +SettingsStorage::SettingsStorage() { + +}; + + +SettingsStorage::~SettingsStorage() { + +}; + + + +void SettingsStorage::addMotor(BLDCMotor* motor) { + if (numMotors < SIMPLEFOC_SETTINGS_MAX_MOTORS) { + motors[numMotors] = motor; + numMotors++; + } + else + SimpleFOCDebug::println("SS: too many motors"); +}; + + +void SettingsStorage::setRegisters(SimpleFOCRegister* registers, int numRegisters){ + this->registers = registers; + this->numRegisters = numRegisters; +}; + + +void SettingsStorage::init() { + // make sure we have motors and registers + if (numMotors < 1) { + SimpleFOCDebug::println("SS: no motors"); + return; + } + if (registers==NULL || numRegisters < 1) { + SimpleFOCDebug::println("SS: no registers"); + return; + } +}; + + + +SettingsStatus SettingsStorage::loadSettings() { + SimpleFOCDebug::println("Loading settings..."); + beforeLoad(); + uint8_t magic; readByte(&magic); + if (magic != SIMPLEFOC_SETTINGS_MAGIC_BYTE) { + SimpleFOCDebug::println("No settings found "); + return SFOC_SETTINGS_NONE; + } + uint8_t rversion; readByte(&rversion); + if (rversion != SIMPLEFOC_REGISTERS_VERSION) { + SimpleFOCDebug::println("Registers version mismatch"); + return SFOC_SETTINGS_OLD; + } + uint8_t version; readByte(&version); + if (version != settings_version) { + SimpleFOCDebug::println("Settings version mismatch"); + return SFOC_SETTINGS_OLD; + } + for (int m = 0; m < numMotors; m++) { + if (numMotors>1) + SimpleFOCDebug::println("Loading settings for motor ", m); + startLoadMotor(m); + for (int i = 0; i < numRegisters; i++) { + SimpleFOCRegister reg = registers[i]; + startLoadRegister(reg); + setRegister(reg, motors[m]); + endLoadRegister(); + } + endLoadMotor(); + } + afterLoad(); + SimpleFOCDebug::println("Settings loaded"); + return SFOC_SETTINGS_SUCCESS; +}; + + +SettingsStatus SettingsStorage::saveSettings() { + SimpleFOCDebug::println("Saving settings..."); + beforeSave(); + writeByte(SIMPLEFOC_SETTINGS_MAGIC_BYTE); + writeByte(SIMPLEFOC_REGISTERS_VERSION); + writeByte(settings_version); + for (int m = 0; m < numMotors; m++) { + if (numMotors>1) + SimpleFOCDebug::println("Saving settings for motor ", m); + startSaveMotor(m); + for (int i = 0; i < numRegisters; i++) { + SimpleFOCRegister reg = registers[i]; + startSaveRegister(reg); + sendRegister(reg, motors[m]); + endSaveRegister(); + } + endSaveMotor(); + } + afterSave(); + SimpleFOCDebug::println("Settings saved"); + return SFOC_SETTINGS_SUCCESS; +}; + +// empty implementation for these + +void SettingsStorage::startSaveMotor(uint8_t num) {}; +void SettingsStorage::endSaveMotor() {}; +void SettingsStorage::startSaveRegister(SimpleFOCRegister reg) {}; +void SettingsStorage::endSaveRegister() {}; +void SettingsStorage::startLoadMotor(uint8_t num) {}; +void SettingsStorage::endLoadMotor() {}; +void SettingsStorage::startLoadRegister(SimpleFOCRegister reg) {}; +void SettingsStorage::endLoadRegister() {}; + +void SettingsStorage::beforeLoad() {}; +void SettingsStorage::afterLoad() {}; +void SettingsStorage::beforeSave() {}; +void SettingsStorage::afterSave() {}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.h new file mode 100644 index 0000000..8506c3b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/SettingsStorage.h @@ -0,0 +1,70 @@ + +#pragma once + + +#include "../comms/SimpleFOCRegisters.h" +#include "../comms/RegisterReceiver.h" +#include "../comms/RegisterSender.h" +#include "BLDCMotor.h" + +#define SIMPLEFOC_SETTINGS_MAGIC_BYTE 0x42 +#define SIMPLEFOC_SETTINGS_VERSION 0x01 + +#define SIMPLEFOC_SETTINGS_REGISTERS_MINIMAL { REG_ZERO_ELECTRIC_ANGLE, REG_SENSOR_DIRECTION } + + +#if !defined(SIMPLEFOC_SETTINGS_MAX_MOTORS) +#define SIMPLEFOC_SETTINGS_MAX_MOTORS 4 +#endif + + +typedef enum : uint8_t { + SFOC_SETTINGS_SUCCESS = 0, // settings loaded/saved successfully + SFOC_SETTINGS_NONE = 1, // no settings found to load + SFOC_SETTINGS_OLD = 2, // settings found, but version is old + SFOC_SETTINGS_ERROR = 3 // other error +} SettingsStatus; + + + + +class SettingsStorage : public RegisterReceiver, public RegisterSender { +public: + SettingsStorage(); + ~SettingsStorage(); + + void addMotor(BLDCMotor* motor); + void setRegisters(SimpleFOCRegister* registers, int numRegisters); + + virtual void init(); + + SettingsStatus loadSettings(); + SettingsStatus saveSettings(); + + /* + Change this value if you make changes to the registers saved in code, and have saved settings on existing devices. + This will cause the device to reset to defaults on the next boot. + */ + uint8_t settings_version = SIMPLEFOC_SETTINGS_VERSION; + +protected: + virtual void startSaveMotor(uint8_t num); + virtual void endSaveMotor(); + virtual void startSaveRegister(SimpleFOCRegister reg); + virtual void endSaveRegister(); + virtual void startLoadMotor(uint8_t num); + virtual void endLoadMotor(); + virtual void startLoadRegister(SimpleFOCRegister reg); + virtual void endLoadRegister(); + + + virtual void beforeLoad(); + virtual void afterLoad(); + virtual void beforeSave(); + virtual void afterSave(); + + FOCMotor* motors[SIMPLEFOC_SETTINGS_MAX_MOTORS]; + int numMotors = 0; + SimpleFOCRegister* registers = NULL; + int numRegisters = 0; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp new file mode 100644 index 0000000..5d4b336 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.cpp @@ -0,0 +1,98 @@ + +#include "./CAT24I2CFlashSettingsStorage.h" + + +CAT24I2CFlashSettingsStorage::CAT24I2CFlashSettingsStorage(uint8_t address, uint16_t offset) { + _address = address; + _offset = offset; +}; + + + +CAT24I2CFlashSettingsStorage::~CAT24I2CFlashSettingsStorage() {}; + + + +void CAT24I2CFlashSettingsStorage::init(TwoWire* wire) { + SettingsStorage::init(); + _wire = wire; + reset(); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readByte(uint8_t* valueToSet) { + return readBytes(valueToSet, 1); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readFloat(float* valueToSet) { + return readBytes(valueToSet, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::readInt(uint32_t* valueToSet) { + return readBytes(valueToSet, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeByte(uint8_t value) { + return writeBytes(&value, 1); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeFloat(float value) { + return writeBytes(&value, 4); +}; + + + +uint8_t CAT24I2CFlashSettingsStorage::writeInt(uint32_t value) { + return writeBytes(&value, 4); +}; + + + +void CAT24I2CFlashSettingsStorage::beforeSave() { + reset(); +}; + + +void CAT24I2CFlashSettingsStorage::beforeLoad() { + reset(); + _wire->beginTransmission(_address); + _wire->write(_currptr >> 8); + _wire->write(_currptr & 0xFF); + _wire->endTransmission(false); +}; + + + +void CAT24I2CFlashSettingsStorage::reset() { + _currptr = _offset; +}; + + +int CAT24I2CFlashSettingsStorage::readBytes(void* valueToSet, int numBytes) { + int read = _wire->requestFrom((uint8_t)_address, (uint8_t)numBytes, (uint8_t)true); + _currptr += read; + if (read==numBytes) + _wire->readBytes((uint8_t*)valueToSet, numBytes); + return read; +}; + + +int CAT24I2CFlashSettingsStorage::writeBytes(void* value, int numBytes) { + _wire->beginTransmission(_address); + _wire->write(_currptr >> 8); + _wire->write(_currptr & 0xFF); + size_t written = _wire->write((uint8_t*)value, numBytes); + _wire->endTransmission(true); + _currptr += written; + delay(5); // write-cycle time + return written; +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h new file mode 100644 index 0000000..4f6c511 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/CAT24I2CFlashSettingsStorage.h @@ -0,0 +1,35 @@ + +#pragma once + +#include "../SettingsStorage.h" +#include "Wire.h" + +class CAT24I2CFlashSettingsStorage : public SettingsStorage { +public: + CAT24I2CFlashSettingsStorage(uint8_t address = 0xA0, uint16_t offset = 0x0); + ~CAT24I2CFlashSettingsStorage(); + + void init(TwoWire* wire = &Wire); + +protected: + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + void beforeSave() override; + void beforeLoad() override; + + void reset(); + int readBytes(void* valueToSet, int numBytes); + int writeBytes(void* value, int numBytes); + + TwoWire* _wire; + uint8_t _address; // i2c address + uint16_t _offset; // offset into NVRAM to store settings + uint16_t _currptr = 0; // pointer to current location in NVRAM + +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/README.md new file mode 100644 index 0000000..9cb33bd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/i2c/README.md @@ -0,0 +1,26 @@ + +# I2C memories settings drivers + +Store settings to I2C EEPROMs. + +## CAT24C64 I2C EEPROMs + +:warning: not yet finished or tested! + +Datasheet describes the CAT24C64 as: _The CAT24C64 is a 64 Kb CMOS Serial EEPROM device, internally organized as 8192 words of 8 bits each._ + +Provide I2C address in constructor: + +`CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0);` + +If you want to use a different I2C bus than the default, you can pass it to the constructor: + +`settings.init(&Wire2);` + +You can use multiple settings objects in the same EEPROM, by providing an offset to the constructor: + +```c++ +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0); +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0, 80); +CAT24I2CFlashSettingsStorage settings = CAT24I2CFlashSettingsStorage(0xA0, 160); +``` diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/README.md new file mode 100644 index 0000000..7382751 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/README.md @@ -0,0 +1,49 @@ +# SAMD21 settings driver + +Driver for storing settings on SAMD21 chips. + +There are two options: + +1. **RWW Flash** \ +Some SAMD21 chips have a seperate RWW flash area, of 2kB-8kB size. This area is seperate from the main flash array. \ +Note: this method is implemented and tested. + +2. **EEPROM emulation** \ +All SAMD21s support EEPROM emulation, where an area of the main flash is reserved for the application storage. \ +:warning: This method is not yet implemented fully, and untested. + +## Using RWW flash + +To use the RWW flash to store settings, you have to make sure the flash region is not locked (you can set the lock fuses using MCP Studio or other SAM development tools). + +Otherwise there is nothing special to do. When reprogramming the chip, be careful not to erase the RWW area. + +You can use multiple settings objects to store to different areas of the RWW. +To do so, specify an offset to the constructor: + +```c++ +SAMDNVMSettingsStorage settings0 = SAMDNVMSettingsStorage(); +SAMDNVMSettingsStorage settings1 = SAMDNVMSettingsStorage(NVMCTRL_ROW_SIZE); +SAMDNVMSettingsStorage settings2 = SAMDNVMSettingsStorage(NVMCTRL_ROW_SIZE*2); +``` + +Note: the offset must be a multiple of the flash memory row size, typically 256 bytes. + + +## Using EEPROM emulation + +TODO implement and test this method + +To use the EEPROM emulation, use a tool like MCP Studio to set the chip fuses to reserve an area of flash for EEPROM emulation: + +![](eeprom_emulation_screenshot.jpg) + +Normally 256 bytes (one flash page) should be plenty for SimpleFOC settings... Obviously, the area you reserve for EEPROM can't be used for your main program. + +Add a build-flag to your build to set the base address of your EEPROM memory: + +`-DSIMPLEFOC_SAMDNVMSETTINGS_BASE=0x003FF800` + +Then use the settings as normal. + +You'll have to adjust your board files to exclude the EEPROM area in the ldscript to prevent it being erased when you re-program, if you care to keep your settings when reflashing. diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp new file mode 100644 index 0000000..5569600 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/settings/samd/SAMDNVMSettingsStorage.cpp @@ -0,0 +1,164 @@ + + + +#include "./SAMDNVMSettingsStorage.h" + + +#if defined(_SAMD21_) + +#include "communication/SimpleFOCDebug.h" + + +SAMDNVMSettingsStorage::SAMDNVMSettingsStorage(uint32_t offset) { + _offset = offset; +}; + +SAMDNVMSettingsStorage::~SAMDNVMSettingsStorage(){}; + +void SAMDNVMSettingsStorage::init(){ + SettingsStorage::init(); + reset(); +}; + + + +void SAMDNVMSettingsStorage::reset(){ + _currptr = (uint8_t*) SIMPLEFOC_SAMDNVMSETTINGS_BASE + _offset; // add offset to NVM base address +}; + + + +void SAMDNVMSettingsStorage::beforeLoad(){ + reset(); +}; + + +void SAMDNVMSettingsStorage::beforeSave(){ + reset(); + _writeIndex = 0; + + // set manual write mode + _ctlB = NVMCTRL->CTRLB.reg; + NVMCTRL->CTRLB.reg |= NVMCTRL_CTRLB_MANW; + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + // unlock region - user can do it with fuses + // erase rows + // TODO handle case that it is more than one row + NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + NVMCTRL->STATUS.reg |= NVMCTRL_STATUS_MASK; + NVMCTRL->INTFLAG.bit.ERROR = 0; + NVMCTRL->CTRLA.reg = 0x1A | NVMCTRL_CTRLA_CMDEX_KEY; + delay(1); + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + if (NVMCTRL->INTFLAG.bit.ERROR == 1) + SimpleFOCDebug::println("NVM erase error ", NVMCTRL->STATUS.reg); + else + SimpleFOCDebug::println("NVM erased row @", (int)_currptr); +}; + + +void SAMDNVMSettingsStorage::afterSave() { + if (_writeIndex>0) + flushPage(); + // restore ctlb + delay(1); + NVMCTRL->CTRLB.reg =_ctlB; + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; +} + + +void SAMDNVMSettingsStorage::flushPage(){ + // if we have an odd number of bytes, pad with 0xFF + if (_writeIndex%2==1) { + _writeBuffer[_writeIndex++] = 0xFF; + } + // erase page buffer + // NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + // NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMD_PBC | NVMCTRL_CTRLA_CMDEX_KEY; + // while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + // copy writeBuffer to NVM, using 16-bit writes + uint16_t* src = (uint16_t*)_writeBuffer; + uint16_t* dst = (uint16_t*)_currptr; + for (int i=0;i<_writeIndex/2;i++) { + *dst++ = *src++; + } + // write page + NVMCTRL->ADDR.reg = ((uint32_t)_currptr - RWWEE_BASE) / 2; + NVMCTRL->STATUS.reg |= NVMCTRL_STATUS_MASK; + NVMCTRL->INTFLAG.bit.ERROR = 0; + NVMCTRL->CTRLA.reg = 0x1C | NVMCTRL_CTRLA_CMDEX_KEY; + delay(1); + while (NVMCTRL->INTFLAG.bit.READY == 0) {}; + if (NVMCTRL->INTFLAG.bit.ERROR == 1) + SimpleFOCDebug::println("NVM write error ", NVMCTRL->STATUS.reg); + else + SimpleFOCDebug::println("NVM wrote page @", (int)_currptr); + // reset writeBuffer pointer and advance currptr to next page + _writeIndex = 0; + _currptr+=NVMCTRL_PAGE_SIZE; +} + + +int SAMDNVMSettingsStorage::readBytes(void* valueToSet, int numBytes) { + uint8_t* bytes = (uint8_t*)valueToSet; + for (int i=0;i + +#if defined(_SAMD21_) + +#include "../SettingsStorage.h" + +#define RWWEE_BASE 0x00400000 + +#if !defined(SIMPLEFOC_SAMDNVMSETTINGS_BASE) +#define SIMPLEFOC_SAMDNVMSETTINGS_BASE RWWEE_BASE +#endif + + +class SAMDNVMSettingsStorage : public SettingsStorage { +public: + SAMDNVMSettingsStorage(uint32_t offset = 0x0); + ~SAMDNVMSettingsStorage(); + + void init() override; + +protected: + uint8_t readByte(uint8_t* valueToSet) override; + uint8_t readFloat(float* valueToSet) override; + uint8_t readInt(uint32_t* valueToSet) override; + + uint8_t writeByte(uint8_t value) override; + uint8_t writeFloat(float value) override; + uint8_t writeInt(uint32_t value) override; + + void beforeSave() override; + void afterSave() override; + void beforeLoad() override; + + + void reset(); + int readBytes(void* valueToSet, int numBytes); + int writeBytes(void* value, int numBytes); + void flushPage(); + + uint32_t _offset; // offset into NVRAM to store settings + + uint32_t _ctlB; + uint8_t* _currptr; // pointer to current location in NVM + int _writeIndex; // index into current page being written + uint8_t _writeBuffer[NVMCTRL_PAGE_SIZE]; // buffer for writing to NVM +}; + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp new file mode 100644 index 0000000..36271da --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.cpp @@ -0,0 +1,113 @@ +/* + * PreciseAngle.cpp + * + * Created on: 1 May 2021 + * Author: runger + */ + +#include +#include + +PreciseAngle::PreciseAngle() : angle(0), rotations(0) {} + +PreciseAngle::~PreciseAngle() {} + +PreciseAngle::PreciseAngle(const PreciseAngle &other) : angle(other.angle), rotations(other.rotations) {} + + +PreciseAngle::PreciseAngle(uint16_t count, int32_t rotations) : angle(count), rotations(rotations) {} + + +PreciseAngle::PreciseAngle(float radians) { + rotations = radians/_2PI; + angle = (radians - (rotations * _2PI)) * cpr / _2PI; +} + +PreciseAngle::PreciseAngle(double radians) { + rotations = radians/_2PI; + angle = (radians - (rotations * _2PI)) * cpr / _2PI; +} + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +float PreciseAngle::getShaftAngle() { + return angle * _2PI / cpr; +} + + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +int32_t PreciseAngle::getRotations() { + return rotations; +} + +// returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians +uint16_t PreciseAngle::getShaftTicks() { + return angle; +} + + +// returns the total angle, including rotations, in radians +float PreciseAngle::asFloat() { + return (angle * _2PI / (float)cpr) + ((float)rotations * _2PI); +} + + +// returns the total angle, including rotations, in radians, as double precision +double PreciseAngle::asDouble() { + return (double)angle * _2PI / cpr + (double)rotations * _2PI; +} + + +// returns the total angle, including rotations, in ticks, as 64 bit integer +int64_t PreciseAngle::asTicks() { + return (int64_t)angle + (int64_t)rotations * cpr; +} + + + + +PreciseAngle PreciseAngle::operator+(const PreciseAngle &other) { + PreciseAngle result; + result.rotations = rotations + other.rotations; + uint32_t temp = angle + other.angle; + if (temp>=cpr) { + result.angle = temp-cpr; + result.rotations+=1; + } + else + result.angle = temp; + return result; +} + + +PreciseAngle PreciseAngle::operator-(const PreciseAngle &other) { + PreciseAngle result; + result.rotations = rotations - other.rotations; + int32_t temp = angle - other.angle; + if (temp<0) { + result.angle = temp+cpr; + result.rotations-=1; + } + else + result.angle = temp; + return result; +} + + +// velocity in rad/s +float PreciseAngle::velocity(const PreciseAngle &previous, uint32_t microseconds) { + PreciseAngle diff = (*this - previous); + return diff.asFloat() * (1e6/(float)microseconds); +} + + + +void PreciseAngle::update(uint16_t current_angle){ + int32_t diff = current_angle - angle; + if (abs(diff)>cpr_overflow_check){ + rotations += diff>0?-1:1; + } + angle = current_angle; +} + + + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.h new file mode 100644 index 0000000..9b05d5e --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/PreciseAngle.h @@ -0,0 +1,57 @@ +/* + * PreciseAngle.h + * + * Created on: 1 May 2021 + * Author: runger + */ + +#ifndef LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ +#define LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ + +#include + +class PreciseAngle { +public: + PreciseAngle(); + virtual ~PreciseAngle(); + PreciseAngle(const PreciseAngle &other); + PreciseAngle(uint16_t count, int32_t rotations); + PreciseAngle(float radians); + PreciseAngle(double radians); + //PreciseAngle& operator=(const PreciseAngle &other); + + // returns the angle mod 2PI, i.e. the angle of the motor shaft, in radians + float getShaftAngle(); + // number of complete motor rotations + int32_t getRotations(); + uint16_t getShaftTicks(); + + // returns the total angle, including rotations, in radians + float asFloat(); + // returns the total angle, including rotations, in radians, as double precision + double asDouble(); + // returns the total angle, including rotations, in ticks, as 64 bit integer + int64_t asTicks(); + + + PreciseAngle operator+(const PreciseAngle &other); + PreciseAngle operator-(const PreciseAngle &other); + + // velocity in rad/s + float velocity(const PreciseAngle &previous, uint32_t microseconds); + + // update the angle with a new ticks value. this will increment the rotations + // if necessary, using a simple algorithm to detect overflows. + // if the motor turns too much between calls to update, the overflow could be missed + // and the rotations counted incorrectly. + void update(uint16_t current_angle); + + + const static uint16_t cpr = 16384; + const static uint16_t cpr_overflow_check = 13107; //cpr*0.8; +protected: + uint16_t angle; + int32_t rotations; +}; + +#endif /* LIBRARIES_ARDUNIO_FOC_DRIVERS_SRC_UTILITIES_PRECISEANGLE_H_ */ diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/README.md new file mode 100644 index 0000000..7eb1796 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/README.md @@ -0,0 +1,25 @@ +# Utilities + +Some classes and functions you may find useful when working with SimpleFOC. + +### PreciseAngle + +:warning: work in progress! only velocity mode has been tested. + +SimpleFOC generally uses floats for calculations. This preserves compatibility with older MCUs which only support 32bit FP, and +can't do double precision, and ensures the maximum compatibilty for the library. + +When older MCUs become irrelevant, this may change, but in the meantime there is a problem representing the angles as floats. +On the one hand they have to be precise enough to accurately capture the electric position of the motor's rotor with respect to +the stator, so meybe even less than 0.1deg. Without this precision you can't do FOC. On the other hand, we need to support many +rotations of the motor shaft - a motor turning at 1000RPM for 1h will be at 60000 rotations or approx. 378,991.118radians. +Expressing these large numbers while also maintaining the required precision is too much for the humble float. + +PreciseAngle works in conjunction with PreciseXXXSensor classes [like this one](../encoders/as5048a/PreciseMagneticSensorAS5048A.h). +It solves the float problem by representing the angle as seperate components: angle within rotation and number of rotations, both as +integers. It maintains the complete precision of the sensor over a rotation range of +/- 2^31, i.e. about 2 billion rotations, before +overflowing. + +Even at 50000RPM (which would be a lot for SimpleFOC!) that's enough for about 30 days of continuous rotation. At 1000RPM it's enough +for 4 years. + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/README.md b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/README.md new file mode 100644 index 0000000..20a641b --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/README.md @@ -0,0 +1,73 @@ + +# STM32 Utilities - PWM Input + +Using the `STM32PWMInput` class you can precisely read a PWM input signal on STM32 MCUs by using their timer's PWM-Input mode. This happens entirely in the timer hardware with zero MCU overhead, and is very precise. + +## Setup + +Connect the PWM input to either channel 1 or channel 2 of a general purpose or advanced control timer supporting PWM-Input mode. + +This should include the following timers: + +- Advanced control timers: TIM1, TIM8 +- General purpose timers (32 bit): TIM2, TIM5 +- General purpose timers (16 bit): TIM3, TIM4, TIM9, TIM12 + +The best to use are the 32 bit general purpose timers, although TIM1 may also be interesting because on some MCUs it can be clocked at a higher rate than the other timers. + +## Usage + +``` + STM32PWMInput pwmInput = STM32PWMInput(PA15); + pwmInput.init(); + + uint32_t periodTicks = pwmInput.getPeriodTicks(); + uint32_t dutyTicks = pwmInput.getDutyCycleTicks(); + float dutyPercent = pwmInput.getDutyCyclePercent(); +``` + +## Input signal + +The input signal should be a PWM signal (square wave) with a duty cycle that is >0% and <100%. The + +The behaviour if the input is not a square wave is not defined, although the MCU will continue to sample the input, and resumes correct measurement when the square wave input is restored. + +## Performance + +The range of PWM frequencies that can be measured and the resolution with which they can be sampled depends on the following: + +- timer clock speed - normally this is your MCU speed, but it can be lower, or in some cases even higher. +- timer prescaler - can divide the timer clock. + +The sample resolution for a PWM input signal of frequency _Fp_ is given by + +_R = Fc / Fp_ + +Where _R_ is the resolution in ticks, and _Fc_ is the timer tick frequency (timer clock with prescaler). + +This sample resolution is equal to the length of the PWM period in ticks. The timer needs to sample both the duty cycle and the full period, so the timer can't sample signals whose period would cause it to overflow. + +On a 16 bit timer this means the period (and sample resolution) must be less than 65536 ticks, while on a 32 bit timer it must be less than 4294967296 ticks. This places a lower limit on the PWM frequencies that can be sampled. + +On the other side, the minimum duty cycle that can be reliably captured is limited by the duration of one timer tick, e.g. the minimum on-time that can be measured reliably is equal to two timer ticks in duration. + +So as the frequency increases, the resolution decreases, and the minimum duty cycle increases. + +For example, on a 64MHz STM32 MCU, using TIM3 (16 bit) you could capture a PWM signal at 10kHz with a resolution of 6400 ticks. This fits in the 16 bit timer, so no problem. Assuming the signal is perfectly stable (it usually isn't) that would be 12 bits of accuracy on your input. + +If you lowered the PWM input frequency to 1kHz, you'd get a 64000 range - just fitting in the timer. But now the accuracy is greatly increased - almost 16 bits! + +If you increased the PWM frequency to 10Mhz, the resolution would be just 6 ticks, and the resulting accuracy very low - just 2 bits, with a minimum duty cycle of 33% and a maximum of 66%. + +Of course you could vary the 10MHz signal faster than the 1kHz one - so choosing the PWM input frequency is a tradeoff between accuracy and control bandwidth... + +Note: clock configuration is out of scope for this class. Set your clocks up in advance. + + + +## Roadmap + +- Support setting of the filtering function, this could help a lot against noise on the input +- Support setting of the timer prescaler (not the clock prescaler!) +- Support setting of the downsample function, this could help increase accuracy +- Support choosing of the alternate timers on pins with more than one diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp new file mode 100644 index 0000000..c530c04 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.cpp @@ -0,0 +1,108 @@ + +#include "./STM32PWMInput.h" + +#if defined(_STM32_DEF_) + + + +STM32PWMInput::STM32PWMInput(int pin){ + _pin = digitalPinToPinName(pin); +}; + + +STM32PWMInput::~STM32PWMInput(){}; + + + + + +int STM32PWMInput::initialize(){ + pinmap_pinout(_pin, PinMap_TIM); + uint32_t channel = STM_PIN_CHANNEL(pinmap_function(_pin, PinMap_TIM)); + timer.Instance = (TIM_TypeDef *)pinmap_peripheral(_pin, PinMap_TIM); + timer.Init.Prescaler = 0; + timer.Init.CounterMode = TIM_COUNTERMODE_UP; + timer.Init.Period = 4.294967295E9; // TODO max period, depends on which timer is used - 32 bits or 16 bits + timer.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + timer.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (channel!=1 && channel!=2) // only channels 1 & 2 supported + return -10; + useChannel2 = (channel==2);// remember the channel + if (HAL_TIM_Base_Init(&timer) != HAL_OK) { + return -1; + } + TIM_ClockConfigTypeDef clkCfg = { .ClockSource=TIM_CLOCKSOURCE_INTERNAL }; + if (HAL_TIM_ConfigClockSource(&timer, &clkCfg) != HAL_OK) { + return -2; + } + if (HAL_TIM_IC_Init(&timer) != HAL_OK) { + return -3; + } + + TIM_SlaveConfigTypeDef slCfg = { + .SlaveMode = TIM_SLAVEMODE_RESET, + .InputTrigger = (channel==1)?TIM_TS_TI1FP1:TIM_TS_TI2FP2, + .TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING, + .TriggerPrescaler = TIM_ICPSC_DIV1, + .TriggerFilter = 0 + }; + if (HAL_TIM_SlaveConfigSynchro(&timer, &slCfg) != HAL_OK) { + return -4; + } + TIM_IC_InitTypeDef icCfg = { + .ICPolarity = (channel==1)?TIM_INPUTCHANNELPOLARITY_RISING:TIM_INPUTCHANNELPOLARITY_FALLING, + .ICSelection = (channel==1)?TIM_ICSELECTION_DIRECTTI:TIM_ICSELECTION_INDIRECTTI, + .ICPrescaler = TIM_ICPSC_DIV1, + .ICFilter = 0 + }; + if (HAL_TIM_IC_ConfigChannel(&timer, &icCfg, TIM_CHANNEL_1) != HAL_OK) { + return -5; + } + icCfg.ICPolarity = (channel==1)?TIM_INPUTCHANNELPOLARITY_FALLING:TIM_INPUTCHANNELPOLARITY_RISING; + icCfg.ICSelection = (channel==1)?TIM_ICSELECTION_INDIRECTTI:TIM_ICSELECTION_DIRECTTI; + if (HAL_TIM_IC_ConfigChannel(&timer, &icCfg, TIM_CHANNEL_2) != HAL_OK) { + return -6; + } + TIM_MasterConfigTypeDef mCfg = { + .MasterOutputTrigger = TIM_TRGO_RESET, + .MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE + }; + if (HAL_TIMEx_MasterConfigSynchronization(&timer, &mCfg) != HAL_OK) { + return -7; + } + if (HAL_TIM_IC_Start(&timer, TIM_CHANNEL_1)!=HAL_OK) { + return -8; + } + if (HAL_TIM_IC_Start(&timer, TIM_CHANNEL_2)!=HAL_OK) { + return -9; + } + timer.Instance->CR1 |= TIM_CR1_CEN; + return 0; +}; + + +float STM32PWMInput::getDutyCyclePercent(){ + uint32_t period = getPeriodTicks(); + if (period<1) return 0.0f; + return getDutyCycleTicks() / (float)period * 100.0f; +}; + + +uint32_t STM32PWMInput::getDutyCycleTicks(){ + if (useChannel2) + return timer.Instance->CCR1; + else + return timer.Instance->CCR2; +}; + + +uint32_t STM32PWMInput::getPeriodTicks(){ + if (useChannel2) + return timer.Instance->CCR2; + else + return timer.Instance->CCR1; +}; + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h new file mode 100644 index 0000000..fcf3acd --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/utilities/stm32pwm/STM32PWMInput.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Arduino.h" + +#if defined(_STM32_DEF_) + + +class STM32PWMInput { + public: + STM32PWMInput(int pin); + ~STM32PWMInput(); + + int initialize(); + + float getDutyCyclePercent(); + uint32_t getDutyCycleTicks(); + uint32_t getPeriodTicks(); + + PinName _pin; + protected: + TIM_HandleTypeDef timer; + bool useChannel2 = false; +}; + + + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp new file mode 100644 index 0000000..714a0cb --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.cpp @@ -0,0 +1,44 @@ + +#include "GenericVoltageSense.h" +#include "Arduino.h" + + + +GenericVoltageSense::GenericVoltageSense(int pin, float gain, float offset, float Tf, float fullScaleVoltage) : VoltageSense(gain, offset, Tf), fullScaleVoltage(fullScaleVoltage), pin(pin) { +}; + +#ifndef INPUT_ANALOG +#define INPUT_ANALOG INPUT +#endif + +#ifndef ADC_RESOLUTION +#define ADC_RESOLUTION 10 +#endif + + +bool GenericVoltageSense::init(int resolution){ + pinMode(pin, INPUT_ANALOG); +#if !defined(ARDUINO_ARCH_AVR) && !defined(ARDUINO_ARCH_MEGAAVR) + if (resolution>0) { + analogReadResolution(resolution); + maxValue = _powtwo(resolution); + } + else { + maxValue = _powtwo(ADC_RESOLUTION); + } +#else + maxValue = _powtwo(ADC_RESOLUTION); +#endif + return true; +}; + + + + + +float GenericVoltageSense::readRawVoltage(){ + float val = analogRead(pin); + val = (val / (float)maxValue) * fullScaleVoltage; + return val; +}; + diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h new file mode 100644 index 0000000..673162a --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/GenericVoltageSense.h @@ -0,0 +1,23 @@ + +#ifndef __GENERICVOLTAGESENSE_H__ +#define __GENERICVOLTAGESENSE_H__ + +#include "./VoltageSense.h" +#include "common/foc_utils.h" + +class GenericVoltageSense : public VoltageSense { + public: + GenericVoltageSense(int pin, float gain = 1.0f, float offset = 0.0f, float Tf = 0.1f, float fullScaleVoltage = 3.3f); + bool init(int resolution = -1); + float fullScaleVoltage; + protected: + float readRawVoltage() override; + int maxValue; + int pin; +}; + + + + + +#endif diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.cpp b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.cpp new file mode 100644 index 0000000..725d16f --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.cpp @@ -0,0 +1,23 @@ + + +#include "./VoltageSense.h" + + + +VoltageSense::VoltageSense(float gain, float offset, float Tf) : filter(Tf), gain(gain), offset(offset) { + voltage = 0.0f; +}; + + + +float VoltageSense::getVoltage(){ + return voltage; +}; + + + +void VoltageSense::update(){ + float v = readRawVoltage(); + v = (v - offset) * gain; + voltage = filter(v); +}; diff --git a/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.h b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.h new file mode 100644 index 0000000..08ebfec --- /dev/null +++ b/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src/voltage/VoltageSense.h @@ -0,0 +1,24 @@ +#ifndef __VOLTAGESENSE_H__ +#define __VOLTAGESENSE_H__ + +#include "common/lowpass_filter.h" + +class VoltageSense { + public: + VoltageSense(float gain = 1.0f, float offset = 0.0f, float Tf = 0.1f); + float getVoltage(); + virtual void update(); + + LowPassFilter filter; + float gain; + float offset; + + protected: + virtual float readRawVoltage() = 0; + + float voltage; +}; + + + +#endif \ No newline at end of file diff --git a/.pio/libdeps/lemon-pepper/integrity.dat b/.pio/libdeps/lemon-pepper/integrity.dat new file mode 100644 index 0000000..24444a7 --- /dev/null +++ b/.pio/libdeps/lemon-pepper/integrity.dat @@ -0,0 +1,2 @@ +askuric/Simple FOC@^2.2.3 +simplefoc/SimpleFOCDrivers@^1.0.2 \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..1e3cbed --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,141 @@ +// +// !!! WARNING !!! AUTO-GENERATED FILE! +// PLEASE DO NOT MODIFY IT AND USE "platformio.ini": +// https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags +// +{ + "configurations": [ + { + "name": "PlatformIO", + "includePath": [ + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/include", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/EEPROM/src", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/dfu", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/can", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/Simple FOC/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SPI/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Wire/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/avr", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/LL", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/OpenAMP", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/hid", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/cdc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Inc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/STM32G4xx", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/open-amp/lib/include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/libmetal/lib/include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/virtual_driver", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/Core/Include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Source/Templates/gcc", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/Include", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/PrivateInclude", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/variants/STM32G4xx/G431C\(6-8-B\)U_G441CBU", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/CMSIS_DSP/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/IWatchdog/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Keyboard/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Mouse/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/RGB_LED_TLC59731/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Servo/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SoftwareSerial/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SrcWrapper/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SubGhz/src", + "" + ], + "browse": { + "limitSymbolsToIncludedHeaders": true, + "path": [ + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/include", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/EEPROM/src", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/dfu", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/firmware/lib/can", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/SimpleFOCDrivers/src", + "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/libdeps/lemon-pepper/Simple FOC/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SPI/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Wire/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/avr", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/LL", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/OpenAMP", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/hid", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino/stm32/usb/cdc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Inc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/STM32G4xx_HAL_Driver/Src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/STM32G4xx", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Inc", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/ST/STM32_USB_Device_Library/Core/Src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/open-amp/lib/include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/libmetal/lib/include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Middlewares/OpenAMP/virtual_driver", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/Core/Include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Include", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/system/Drivers/CMSIS/Device/ST/STM32G4xx/Source/Templates/gcc", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/Include", + "/Users/mateijordache/.platformio/packages/framework-cmsis/CMSIS/DSP/PrivateInclude", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/cores/arduino", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/variants/STM32G4xx/G431C\(6-8-B\)U_G441CBU", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/CMSIS_DSP/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/IWatchdog/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Keyboard/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Mouse/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/RGB_LED_TLC59731/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/Servo/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SoftwareSerial/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SrcWrapper/src", + "/Users/mateijordache/.platformio/packages/framework-arduinoststm32/libraries/SubGhz/src", + "" + ] + }, + "defines": [ + "PLATFORMIO=60111", + "STM32G4xx", + "STM32G431xx", + "USBCON", + "SIMPLEFOC_STM32_DEBUG", + "PIO_FRAMEWORK_ARDUINO_ENABLE_CDC", + "HAL_FDCAN_MODULE_ENABLED", + "FDCAN_ALT1", + "SN65HVD23x", + "ARDUINO_GENERIC_G431CBUX", + "STM32G4xx", + "ARDUINO=10808", + "ARDUINO_ARCH_STM32", + "NDEBUG", + "ARDUINO_GENERIC_G431CBUX", + "BOARD_NAME=\"GENERIC_G431CBUX\"", + "HAL_UART_MODULE_ENABLED", + "USE_FULL_LL_DRIVER", + "VARIANT_H=\"variant_generic.h\"", + "USBD_USE_CDC", + "USBCON", + "USB_VID=0", + "USB_PID=0", + "HAL_PCD_MODULE_ENABLED", + "" + ], + "cStandard": "gnu11", + "cppStandard": "gnu++14", + "compilerPath": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-gcc", + "compilerArgs": [ + "-mcpu=cortex-m4", + "-mthumb", + "-mfpu=fpv4-sp-d16", + "-mfloat-abi=hard", + "" + ] + } + ], + "version": 4 +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..de0a4e3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,47 @@ +// AUTOMATICALLY GENERATED FILE. PLEASE DO NOT MODIFY IT MANUALLY +// +// PIO Unified Debugger +// +// Documentation: https://docs.platformio.org/page/plus/debugging.html +// Configuration: https://docs.platformio.org/page/projectconf/section_env_debug.html + +{ + "version": "0.2.0", + "configurations": [ + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug", + "executable": "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/build/lemon-pepper/firmware.elf", + "projectEnvName": "lemon-pepper", + "toolchainBinDir": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin", + "internalConsoleOptions": "openOnSessionStart", + "svdPath": "/Users/mateijordache/.platformio/platforms/ststm32/misc/svd/STM32G431xx.svd", + "preLaunchTask": { + "type": "PlatformIO", + "task": "Pre-Debug" + } + }, + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug (skip Pre-Debug)", + "executable": "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/build/lemon-pepper/firmware.elf", + "projectEnvName": "lemon-pepper", + "toolchainBinDir": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin", + "internalConsoleOptions": "openOnSessionStart", + "svdPath": "/Users/mateijordache/.platformio/platforms/ststm32/misc/svd/STM32G431xx.svd" + }, + { + "type": "platformio-debug", + "request": "launch", + "name": "PIO Debug (without uploading)", + "executable": "/Users/mateijordache/Documents/electronics_projects/lemon-pepper-stepper/.pio/build/lemon-pepper/firmware.elf", + "projectEnvName": "lemon-pepper", + "toolchainBinDir": "/Users/mateijordache/.platformio/packages/toolchain-gccarmnoneeabi/bin", + "internalConsoleOptions": "openOnSessionStart", + "svdPath": "/Users/mateijordache/.platformio/platforms/ststm32/misc/svd/STM32G431xx.svd", + "loadMode": "manual" + } + ] +} diff --git a/firmware/boards/genericSTM32G431CB.json b/firmware/boards/genericSTM32G431CB.json new file mode 100644 index 0000000..f1ad769 --- /dev/null +++ b/firmware/boards/genericSTM32G431CB.json @@ -0,0 +1,44 @@ +{ + "build": { + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32G4xx -DSTM32G431xx", + "f_cpu": "170000000L", + "mcu": "stm32g431cbu6", + "product_line": "STM32G431xx", + "variant": "STM32G4xx/G431C(6-8-B)U_G441CBU" + }, + "connectivity": [ + "can" + ], + "debug": { + "default_tools": [ + "stlink" + ], + "jlink_device": "STM32G431CB", + "openocd_target": "stm32g4x", + "svd_path": "STM32G431xx.svd" + }, + "frameworks": [ + "arduino", + "cmsis", + "libopencm3", + "stm32cube", + "zephyr" + ], + "name": "STM32G431CB", + "upload": { + "maximum_ram_size": 32768, + "maximum_size": 131072, + "protocol": "stlink", + "protocols": [ + "stlink", + "jlink", + "cmsis-dap", + "blackmagic", + "mbed" + ] + }, + "url": "https://www.st.com/en/microcontrollers-microprocessors/stm32g431cb.html", + "vendor": "ST" +} diff --git a/firmware/include/README b/firmware/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/firmware/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/firmware/include/lemon-pepper.h b/firmware/include/lemon-pepper.h new file mode 100644 index 0000000..5319898 --- /dev/null +++ b/firmware/include/lemon-pepper.h @@ -0,0 +1,38 @@ +#pragma once + +// MOTOR DRIVER L6226Q +#define U_PWM PA10 +#define V_PWM PA9 +#define W_PWM PA1 + +#define A1 PA0 +#define A2 PA1 +#define B1 PA9 +#define B2 PA10 + +#define MOT_EN PB12 + +// ENCODER MT6835 +#define ENC_A PB4 +#define ENC_B PB5 +#define ENC_Z PB3 + +#define ENC_COPI PA7 +#define ENC_CIPO PA6 +#define ENC_SCK PA5 +#define ENC_CS PC4 +#define ENC_CAL PA4 + +// CURRENT SENSE +#define ISENSE_U PA3 +#define ISENSE_V PB13 +#define ISENSE_W PB0 + +// COMMUNICATION +#define CAN_TX PB8 +#define CAN_RX PB9 + +// AUX +#define LED_GOOD PB10 +#define LED_FAULT PB11 + diff --git a/firmware/ldscript.ld b/firmware/ldscript.ld new file mode 100644 index 0000000..e99f274 --- /dev/null +++ b/firmware/ldscript.ld @@ -0,0 +1,185 @@ +/* +****************************************************************************** +** +** @file : LinkerScript.ld +** +** @author : Auto-generated by STM32CubeIDE +** +** @brief : Linker script for STM32G431CBUx Device from STM32G4 series +** 128Kbytes FLASH +** 32Kbytes RAM +** +** Set heap size, stack size and stack location according +** to application requirements. +** +** Set memory bank area and size if external memory is used +** +** Target : STMicroelectronics STM32 +** +** Distribution: The file is distributed as is, without any warranty +** of any kind. +** +****************************************************************************** +** @attention +** +** Copyright (c) 2023 STMicroelectronics. +** All rights reserved. +** +** This software is licensed under terms that can be found in the LICENSE file +** in the root directory of this software component. +** If no LICENSE file comes with this software, it is provided AS-IS. +** +****************************************************************************** +*/ + +/* Entry Point */ +ENTRY(Reset_Handler) + +/* Highest address of the user mode stack */ +_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */ + +_Min_Heap_Size = 0x200; /* required amount of heap */ +_Min_Stack_Size = 0x400; /* required amount of stack */ + +/* Memories definition */ +MEMORY +{ + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K + FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K +} + +/* Sections */ +SECTIONS +{ + /* The startup code into "FLASH" Rom type memory */ + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) /* Startup code */ + . = ALIGN(4); + } >FLASH + + /* The program code and other data into "FLASH" Rom type memory */ + .text : + { + . = ALIGN(4); + *(.text) /* .text sections (code) */ + *(.text*) /* .text* sections (code) */ + *(.glue_7) /* glue arm to thumb code */ + *(.glue_7t) /* glue thumb to arm code */ + *(.eh_frame) + + KEEP (*(.init)) + KEEP (*(.fini)) + + . = ALIGN(4); + _etext = .; /* define a global symbols at end of code */ + } >FLASH + + /* Constant data into "FLASH" Rom type memory */ + .rodata : + { + . = ALIGN(4); + *(.rodata) /* .rodata sections (constants, strings, etc.) */ + *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ + . = ALIGN(4); + } >FLASH + + .ARM.extab : { + . = ALIGN(4); + *(.ARM.extab* .gnu.linkonce.armextab.*) + . = ALIGN(4); + } >FLASH + + .ARM : { + . = ALIGN(4); + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + . = ALIGN(4); + } >FLASH + + .preinit_array : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP (*(.preinit_array*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + . = ALIGN(4); + } >FLASH + + .init_array : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array*)) + PROVIDE_HIDDEN (__init_array_end = .); + . = ALIGN(4); + } >FLASH + + .fini_array : + { + . = ALIGN(4); + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array*)) + PROVIDE_HIDDEN (__fini_array_end = .); + . = ALIGN(4); + } >FLASH + + /* Used by the startup to initialize data */ + _sidata = LOADADDR(.data); + + /* Initialized data sections into "RAM" Ram type memory */ + .data : + { + . = ALIGN(4); + _sdata = .; /* create a global symbol at data start */ + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + *(.RamFunc) /* .RamFunc sections */ + *(.RamFunc*) /* .RamFunc* sections */ + + . = ALIGN(4); + _edata = .; /* define a global symbol at data end */ + + } >RAM AT> FLASH + + /* Uninitialized data section into "RAM" Ram type memory */ + . = ALIGN(4); + .bss : + { + /* This is used by the startup in order to initialize the .bss section */ + _sbss = .; /* define a global symbol at bss start */ + __bss_start__ = _sbss; + *(.bss) + *(.bss*) + *(COMMON) + + . = ALIGN(4); + _ebss = .; /* define a global symbol at bss end */ + __bss_end__ = _ebss; + } >RAM + + /* User_heap_stack section, used to check that there is enough "RAM" Ram type memory left */ + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE ( end = . ); + PROVIDE ( _end = . ); + . = . + _Min_Heap_Size; + . = . + _Min_Stack_Size; + . = ALIGN(8); + } >RAM + + /* Remove information from the compiler libraries */ + /DISCARD/ : + { + libc.a ( * ) + libm.a ( * ) + libgcc.a ( * ) + } + + .ARM.attributes 0 : { *(.ARM.attributes) } +} diff --git a/firmware/lib/README b/firmware/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/firmware/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/firmware/lib/can/can.cpp b/firmware/lib/can/can.cpp new file mode 100644 index 0000000..99509c4 --- /dev/null +++ b/firmware/lib/can/can.cpp @@ -0,0 +1,263 @@ +#include "can.h" + +enum canAction +{ + WaitForCmd, + TxDeviceInfo, + SearchForID +}; + +FDCAN_HandleTypeDef hfdcan1; +FDCAN_FilterTypeDef sFilterConfig; +FDCAN_RxHeaderTypeDef RxHeader; +FDCAN_TxHeaderTypeDef TxHeader; + +canAction FDCAN_State = WaitForCmd; +const uint16_t FDCAN_GlobalID = 0x7CC; +uint16_t FDCAN_TempID; +uint8_t foundExtDevice = 0; +uint8_t JunkBuf[8]; +uint8_t TxData[8]; +uint8_t RxData[8]; + +extern "C" void FDCAN1_IT0_IRQHandler(void); + +void FDCAN_Error_Handler(void) +{ + HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); +} + +void FDCAN_Init(void) +{ + hfdcan1.Instance = FDCAN1; + hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; + hfdcan1.Init.FrameFormat = FDCAN_FRAME_FD_NO_BRS; + hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; + hfdcan1.Init.AutoRetransmission = ENABLE; + hfdcan1.Init.TransmitPause = DISABLE; + hfdcan1.Init.ProtocolException = DISABLE; + + // transceiver and peripheral clock specific values + hfdcan1.Init.NominalPrescaler = 1; + hfdcan1.Init.NominalSyncJumpWidth = 2; + hfdcan1.Init.NominalTimeSeg1 = 21; + hfdcan1.Init.NominalTimeSeg2 = 2; + + hfdcan1.Init.DataPrescaler = 1; + hfdcan1.Init.DataSyncJumpWidth = 5; + hfdcan1.Init.DataTimeSeg1 = 6; + hfdcan1.Init.DataTimeSeg2 = 5; + // + + hfdcan1.Init.StdFiltersNbr = 1; + hfdcan1.Init.ExtFiltersNbr = 0; + hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; + + if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) + { + FDCAN_Error_Handler(); + } +} + +void FDCAN_ConfigFilter(uint16_t canID) +{ + sFilterConfig.IdType = FDCAN_STANDARD_ID; + sFilterConfig.FilterIndex = 0; + sFilterConfig.FilterType = FDCAN_FILTER_DUAL; + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; + sFilterConfig.FilterID1 = canID; + sFilterConfig.FilterID2 = FDCAN_GlobalID; + if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK) + { + /* Filter configuration Error */ + FDCAN_Error_Handler(); + } +} + +void FDCAN_ConfigTxHeader(uint16_t canID) +{ + TxHeader.Identifier = canID; + TxHeader.IdType = FDCAN_STANDARD_ID; + TxHeader.TxFrameType = FDCAN_DATA_FRAME; + TxHeader.DataLength = FDCAN_DLC_BYTES_8; + TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE; + TxHeader.BitRateSwitch = FDCAN_BRS_OFF; + TxHeader.FDFormat = FDCAN_FD_CAN; + TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; + TxHeader.MessageMarker = 0; +} + +void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef *hfdcan) +{ + + GPIO_InitTypeDef GPIO_InitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; + if (hfdcan->Instance == FDCAN1) + { + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; + PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_HSE; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) + { + Error_Handler(); + } + + __HAL_RCC_FDCAN_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + + /**FDCAN1 GPIO Configuration + PB8-BOOT0 ------> FDCAN1_RX + PB9 ------> FDCAN1_TX + */ + GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN1; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + HAL_NVIC_SetPriority(FDCAN1_IT0_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(FDCAN1_IT0_IRQn); + } +} + +void HAL_FDCAN_MspDeInit(FDCAN_HandleTypeDef *hfdcan) +{ + + if (hfdcan->Instance == FDCAN1) + { + __HAL_RCC_FDCAN_CLK_DISABLE(); + + /**FDCAN1 GPIO Configuration + PB8-BOOT0 ------> FDCAN1_RX + PB9 ------> FDCAN1_TX + */ + HAL_GPIO_DeInit(GPIOB, GPIO_PIN_8 | GPIO_PIN_9); + + HAL_NVIC_DisableIRQ(FDCAN1_IT0_IRQn); + } +} + +extern "C" void FDCAN1_IT0_IRQHandler(void) +{ + HAL_FDCAN_IRQHandler(&hfdcan1); +} + +void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) +{ + if((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != RESET) + { + HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData); + } + + if (RxHeader.RxFrameType == FDCAN_REMOTE_FRAME) + { + // Reply globally but put the replying ID in the data packet. + TxHeader.Identifier = FDCAN_GlobalID; + memset(TxData, 0x00, 8 * sizeof(uint8_t)); + memcpy(&TxData, (sFilterConfig.FilterID1), sizeof(uint16_t)); + FDCAN_SendMessage(); + } + else + { + switch (FDCAN_State) + { + case WaitForCmd: + // Set some flag to indicate that sfoc has a new command. + if (RxHeader.Identifier = FDCAN_GlobalID) + { + + } + else + { + + } + break; + + case SearchForID: + foundExtDevice = 1; + break; + + default: + break; + } + } +} + +void FDCAN_SendMessage(void) +{ + switch (FDCAN_State) + { + case TxDeviceInfo: + break; + + case SearchForID: + TxHeader.Identifier = FDCAN_TempID; + TxHeader.TxFrameType = FDCAN_REMOTE_FRAME; + TxHeader.DataLength = FDCAN_DLC_BYTES_0; + break; + + default: + break; + } + + if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData) != HAL_OK) + { + FDCAN_Error_Handler(); + } + + // Set the tx parameters back to normal. + TxHeader.Identifier = sFilterConfig.FilterID1; + TxHeader.TxFrameType = FDCAN_DATA_FRAME; + TxHeader.DataLength = FDCAN_DLC_BYTES_8; +} + +uint16_t FDCAN_FindUniqueID(void) +{ + FDCAN_TempID = 0x000; + FDCAN_State = SearchForID; + + while (1) + { + foundExtDevice = 0; + FDCAN_SendMessage(); + delay(100); + if (foundExtDevice) + { + // Try the next address. + FDCAN_TempID++; + } + else + { + // We found a unique device ID! + FDCAN_State = WaitForCmd; + FDCAN_ChangeID(FDCAN_TempID); + digitalWrite(PA7, HIGH); + return FDCAN_TempID; + } + } +} + +void FDCAN_ChangeID(uint16_t newID) +{ + // HAL_FDCAN_Stop(); + FDCAN_ConfigFilter(newID); + FDCAN_ConfigTxHeader(newID); + // HAL_FDCAN_Start(); +} + +void FDCAN_Start(uint16_t canID) +{ + FDCAN_Init(); + FDCAN_ConfigFilter(canID); + FDCAN_ConfigTxHeader(canID); + + if (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK) + { + FDCAN_Error_Handler(); + } + + if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK) + { + FDCAN_Error_Handler(); + } +} \ No newline at end of file diff --git a/firmware/lib/can/can.h b/firmware/lib/can/can.h new file mode 100644 index 0000000..b3eac4a --- /dev/null +++ b/firmware/lib/can/can.h @@ -0,0 +1,17 @@ +/** + * CAN header +*/ + +#ifndef AIOLI_CAN_H +#define AIOLI_CAN_H + +#include +#include "stm32g4xx_hal.h" +#include "stm32g4xx_hal_fdcan.h" + +void FDCAN_Start(uint16_t canID); +void FDCAN_SendMessage(); +void FDCAN_ChangeID(uint16_t newID); +uint16_t FDCAN_FindUniqueID(void); + +#endif \ No newline at end of file diff --git a/firmware/lib/dfu/dfu.cpp b/firmware/lib/dfu/dfu.cpp new file mode 100644 index 0000000..f7beaf0 --- /dev/null +++ b/firmware/lib/dfu/dfu.cpp @@ -0,0 +1,26 @@ +#include "stm32g4xx_hal.h" +#include "stm32g4xx_hal_conf.h" +#include "stm32g4xx_hal_rcc.h" + +void perform_system_reset(void){ + __disable_irq(); + NVIC_SystemReset(); +} + +// https://stm32f4-discovery.net/2017/04/tutorial-jump-system-memory-software-stm32/ +void jump_to_bootloader(void){ + __enable_irq(); + HAL_RCC_DeInit(); + HAL_DeInit(); + SysTick->CTRL = SysTick->LOAD = SysTick->VAL = 0; + __HAL_SYSCFG_REMAPMEMORY_SYSTEMFLASH(); + + const uint32_t p = (*((uint32_t *) 0x1FFF0000)); + __set_MSP( p ); + + void (*SysMemBootJump)(void); + SysMemBootJump = (void (*)(void)) (*((uint32_t *) 0x1FFF0004)); + SysMemBootJump(); + + while( 1 ) {} +} diff --git a/firmware/lib/dfu/dfu.h b/firmware/lib/dfu/dfu.h new file mode 100644 index 0000000..7aa7898 --- /dev/null +++ b/firmware/lib/dfu/dfu.h @@ -0,0 +1,7 @@ +#ifndef BOOTLOADER_H +#define BOOTLOADER_H + +void perform_system_reset(void); +void jump_to_bootloader(void); + +#endif diff --git a/firmware/src/clock.c b/firmware/src/clock.c new file mode 100644 index 0000000..0416025 --- /dev/null +++ b/firmware/src/clock.c @@ -0,0 +1,50 @@ +#include "pins_arduino.h" + +/** + * @brief System Clock Configuration + * @retval None + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + /** Configure the main internal regulator output voltage + */ + HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST); + + /** Initializes the RCC Oscillators according to the specified parameters + * in the RCC_OscInitTypeDef structure. + */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSI48 + |RCC_OSCILLATORTYPE_HSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.HSIState = RCC_HSI_ON; + RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; + RCC_OscInitStruct.HSI48State = RCC_HSI48_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV1; + RCC_OscInitStruct.PLL.PLLN = 28; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV8; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) + { + Error_Handler(); + } + + /** Initializes the CPU, AHB and APB buses clocks + */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) + { + Error_Handler(); + } +} \ No newline at end of file diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp new file mode 100644 index 0000000..1cb830d --- /dev/null +++ b/firmware/src/main.cpp @@ -0,0 +1,163 @@ +#include +#include +#include + +#include +#include +#include "encoders/MT6835/MagneticSensorMT6835.h" + +#include "stm32g4xx_hal_conf.h" +#include "stm32g4xx_hal_fdcan.h" + +#include "can.h" +#include "dfu.h" +#include "lemon-pepper.h" + +#define USBD_MANUFACTURER_STRING "matei repair lab" +#define USBD_PRODUCT_STRING_FS "lemon-pepper-stepper" + +// board specific data +typedef struct +{ + uint16_t signature; + int8_t electricalDir; + float electricalZero; + uint8_t canID; +}userData; + +userData boardData; +const uint16_t magicWord = 0xAF0C; + +// canbus things +extern uint8_t TxData[8]; +extern uint8_t RxData[8]; + +// simpleFOC things +#define POLEPAIRS 7 +#define RPHASE 1.4 +#define MOTORKV 1000 + +SPISettings myMT6835SPISettings(1000000, MT6835_BITORDER, SPI_MODE3); +MagneticSensorMT6835 sensor = MagneticSensorMT6835(ENC_CS, myMT6835SPISettings); + +BLDCDriver3PWM driver = BLDCDriver3PWM(U_PWM, V_PWM, W_PWM, U_EN, V_EN, W_EN); +BLDCMotor motor = BLDCMotor(POLEPAIRS, RPHASE, MOTORKV); +MagneticSensorMT6701SSI enc = MagneticSensorMT6701SSI(ENC_CS); +Commander commander = Commander(SerialUSB); + +// Prototypes +void configureFOC(void); +void configureCAN(void); +void userButton_IT(void); + +void setup() +{ + // SCB->VTOR == 0x08000000; + pinMode(USER_LED, OUTPUT); + attachInterrupt(USER_BUTTON, userButton_IT, HIGH); + + SerialUSB.begin(115200); + + EEPROM.get(0, boardData); + + configureCAN(); + configureFOC(); + + if(boardData.canID == 0x000) + { + // If the can ID is not initialized, then we'll look for a free ID. + boardData.canID = FDCAN_FindUniqueID(); + SerialUSB.println(boardData.canID); + } + + if(boardData.signature != magicWord) + { + // If the EEPROM has not been initalized yet, save all the known data. + EEPROM.put(0, boardData); + } +} + +void loop() +{ + motor.loopFOC(); + motor.move(); + commander.run(); + + #ifdef HAS_MONITOR + motor.monitor(); + #endif +} + +void doMotor(char *cmd) +{ + commander.motor(&motor, cmd); +} + +void configureFOC(void){ + commander.add('M', doMotor, "motor"); + commander.verbose = VerboseMode::machine_readable; + + #ifdef SIMPLEFOC_STM32_DEBUG + SimpleFOCDebug::enable(&SerialUSB); + #endif + + // Encoder initialization. + // Encoder on SPI1 + enc.init(); + + // Driver initialization. + driver.pwm_frequency = 32000; + driver.voltage_power_supply = 5; + driver.voltage_limit = 2.5; + driver.init(); + + // Motor PID parameters. + motor.PID_velocity.P = 0.2; + motor.PID_velocity.I = 3; + motor.PID_velocity.D = 0.002; + motor.PID_velocity.output_ramp = 100; + motor.LPF_velocity.Tf = 0.5; + motor.LPF_angle.Tf = 0; // try to avoid + + // Motor initialization. + motor.voltage_sensor_align = 2; + motor.current_limit = 0.5; + motor.velocity_limit = 50; + motor.controller = MotionControlType::velocity; + motor.foc_modulation = FOCModulationType::SinePWM; + + // Monitor initialization + #ifdef HAS_MONITOR + motor.useMonitoring(SerialUSB); + motor.monitor_start_char = 'M'; + motor.monitor_end_char = 'M'; + motor.monitor_downsample = 250; + #endif + + motor.linkSensor(&enc); + motor.linkDriver(&driver); + + motor.target = 0; + + if(boardData.signature != magicWord){ + // If we have not initialized the EEPROM before. + motor.init(); + motor.initFOC(); + + boardData.signature = magicWord; + boardData.electricalZero = motor.zero_electric_angle; + boardData.electricalDir = motor.sensor_direction; + } + else{ + motor.zero_electric_angle = boardData.electricalZero; + motor.sensor_direction = boardData.electricalDir; + motor.init(); + motor.initFOC(); + } +} + +void configureCAN(void){ + FDCAN_Start(0x000); +} + + diff --git a/firmware/test/README b/firmware/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/firmware/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/hardware/currentsense.kicad_sch b/hardware/currentsense.kicad_sch index aba1840..9a67f19 100644 --- a/hardware/currentsense.kicad_sch +++ b/hardware/currentsense.kicad_sch @@ -644,12 +644,12 @@ (property "LCSC Part" "C146412" (at 125.73 87.63 0) (effects (font (size 1.27 1.27)) hide) ) - (pin "1" (uuid d60ea7da-e95d-4c22-ad65-def1505307d4)) - (pin "1" (uuid d60ea7da-e95d-4c22-ad65-def1505307d4)) + (pin "1" (uuid d60ea7da-e95d-4c22-ad65-def1505307d6)) + (pin "1" (uuid d60ea7da-e95d-4c22-ad65-def1505307d6)) (pin "11" (uuid e8d3a338-a773-49b8-a716-d1ab18131472)) (pin "12" (uuid 8c659ecd-ac7a-4f4a-9995-9dcc08bc6ebc)) - (pin "2" (uuid e042e7b2-9853-43dd-9d84-0a27309e3475)) - (pin "2" (uuid e042e7b2-9853-43dd-9d84-0a27309e3475)) + (pin "2" (uuid e042e7b2-9853-43dd-9d84-0a27309e3477)) + (pin "2" (uuid e042e7b2-9853-43dd-9d84-0a27309e3477)) (pin "5" (uuid d5cf600e-4c97-43db-818b-ea3248045403)) (pin "6" (uuid 2c89c0c4-ec2f-42de-81b6-e62c8d188968)) (pin "10" (uuid 95506ff9-4ec7-44f1-b5f7-87c53697753f)) @@ -708,12 +708,12 @@ (property "LCSC Part" "C146412" (at 125.73 158.75 0) (effects (font (size 1.27 1.27)) hide) ) - (pin "1" (uuid f3a4724f-c83e-4357-93f5-de2b64412a05)) - (pin "1" (uuid f3a4724f-c83e-4357-93f5-de2b64412a05)) + (pin "1" (uuid f3a4724f-c83e-4357-93f5-de2b64412a07)) + (pin "1" (uuid f3a4724f-c83e-4357-93f5-de2b64412a07)) (pin "11" (uuid 381951b5-9e1d-46d9-9fdd-5023089bf34f)) (pin "12" (uuid 4a0f3bdc-1b3f-4b5a-9fdd-ad98ac9347e8)) - (pin "2" (uuid e061c3c4-4715-4c93-a856-a0881bf59ba6)) - (pin "2" (uuid e061c3c4-4715-4c93-a856-a0881bf59ba6)) + (pin "2" (uuid e061c3c4-4715-4c93-a856-a0881bf59ba8)) + (pin "2" (uuid e061c3c4-4715-4c93-a856-a0881bf59ba8)) (pin "5" (uuid 040e7810-ff19-4cb2-8089-e98a109dca74)) (pin "6" (uuid c4b7cfc6-3923-4e8e-beab-8edfebf00db1)) (pin "10" (uuid 2f7d9d6f-7117-437c-a6f9-ab265c02086b)) @@ -872,12 +872,12 @@ (property "LCSC Part" "C146412" (at 125.73 123.19 0) (effects (font (size 1.27 1.27)) hide) ) - (pin "1" (uuid ac305c0b-c868-41dc-8758-5dddab10c0ad)) - (pin "1" (uuid ac305c0b-c868-41dc-8758-5dddab10c0ad)) + (pin "1" (uuid ac305c0b-c868-41dc-8758-5dddab10c0af)) + (pin "1" (uuid ac305c0b-c868-41dc-8758-5dddab10c0af)) (pin "11" (uuid 5f952846-310c-40d4-879c-8b224fb60661)) (pin "12" (uuid 9720c386-328c-42a4-97c9-f29be92781fd)) - (pin "2" (uuid 81d43ea9-5248-42eb-8446-126bd3fa7bbe)) - (pin "2" (uuid 81d43ea9-5248-42eb-8446-126bd3fa7bbe)) + (pin "2" (uuid 81d43ea9-5248-42eb-8446-126bd3fa7bc0)) + (pin "2" (uuid 81d43ea9-5248-42eb-8446-126bd3fa7bc0)) (pin "5" (uuid 9f25a38a-708f-427f-b757-bac5c276de82)) (pin "6" (uuid 15056b44-bd76-49d3-97b1-8f239c766f7b)) (pin "10" (uuid 43fa4b5b-1368-473b-a20c-160035bf6d2e)) diff --git a/hardware/lemon-pepper.kicad_pcb b/hardware/lemon-pepper.kicad_pcb index 66b8046..6ecfcc2 100644 --- a/hardware/lemon-pepper.kicad_pcb +++ b/hardware/lemon-pepper.kicad_pcb @@ -155,8 +155,8 @@ (net 58 "unconnected-(U301-PC13-Pad2)") (net 59 "/encoder/CAL_EN") (net 60 "unconnected-(U301-PC14-Pad3)") - (net 61 "/mcu/MOT_EN_B") - (net 62 "/mcu/MOT_EN_A") + (net 61 "unconnected-(U301-PC6-Pad29)") + (net 62 "unconnected-(U301-PA8-Pad30)") (net 63 "/mcu/CAN_TX") (net 64 "/mcu/CAN_RX") (net 65 "unconnected-(U302-Vref-Pad5)") @@ -3496,9 +3496,9 @@ (pad "28" smd roundrect (at 3.4375 1.25 315) (size 0.875 0.25) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25) (net 85 "/mcu/DIR") (pinfunction "PB15") (pintype "bidirectional") (tstamp af73c5e8-62cf-4ece-982e-35fd523353a3)) (pad "29" smd roundrect (at 3.4375 0.75 315) (size 0.875 0.25) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25) - (net 61 "/mcu/MOT_EN_B") (pinfunction "PC6") (pintype "bidirectional") (tstamp 34610372-85cb-4a2e-9e19-917dc98c9f0d)) + (net 61 "unconnected-(U301-PC6-Pad29)") (pinfunction "PC6") (pintype "bidirectional+no_connect") (tstamp 34610372-85cb-4a2e-9e19-917dc98c9f0d)) (pad "30" smd roundrect (at 3.4375 0.25 315) (size 0.875 0.25) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25) - (net 62 "/mcu/MOT_EN_A") (pinfunction "PA8") (pintype "bidirectional") (tstamp b4179fad-faab-4912-9e38-b00b0a022d6f)) + (net 62 "unconnected-(U301-PA8-Pad30)") (pinfunction "PA8") (pintype "bidirectional+no_connect") (tstamp b4179fad-faab-4912-9e38-b00b0a022d6f)) (pad "31" smd roundrect (at 3.4375 -0.25 315) (size 0.875 0.25) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25) (net 75 "/half bridges/IN1A") (pinfunction "PA9") (pintype "bidirectional") (tstamp 220fb8b1-a2ff-4276-a1f6-6cb5d18c2b93)) (pad "32" smd roundrect (at 3.4375 -0.75 315) (size 0.875 0.25) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25) diff --git a/hardware/mcu.kicad_sch b/hardware/mcu.kicad_sch index af06a0b..cdf49e3 100644 --- a/hardware/mcu.kicad_sch +++ b/hardware/mcu.kicad_sch @@ -1195,6 +1195,8 @@ (no_connect (at 83.82 110.49) (uuid 593faef5-a6f9-49c8-a028-8d906acecb05)) (no_connect (at 83.82 125.73) (uuid 6f78a344-296e-4bc0-9b4d-f0d8818bd611)) (no_connect (at 187.96 72.39) (uuid 723bc003-4440-4fde-8075-7cc16a53689e)) + (no_connect (at 116.84 90.17) (uuid 8d51f5ea-dd4b-44c8-9378-9028df98e3a6)) + (no_connect (at 83.82 90.17) (uuid 94e0402c-0184-4456-884e-f2f4030381ff)) (no_connect (at 96.52 64.77) (uuid 978c225f-ee58-48c6-9f3f-273d7f0dff63)) (no_connect (at 83.82 123.19) (uuid 97f585dd-d2ce-4ec6-9945-899b6cf15c81)) (no_connect (at 83.82 97.79) (uuid be6c71ad-b273-4b5a-a960-d02ff9e3835b)) @@ -1225,14 +1227,6 @@ (stroke (width 0) (type default)) (uuid 07d463d1-e48d-47cf-aace-6ce945412e99) ) - (wire (pts (xy 73.66 90.17) (xy 83.82 90.17)) - (stroke (width 0) (type default)) - (uuid 13d564f0-b9c9-49c8-af6b-7993abcb686d) - ) - (wire (pts (xy 116.84 90.17) (xy 128.27 90.17)) - (stroke (width 0) (type default)) - (uuid 13f8ed3c-202d-4bae-afa9-668fae09a196) - ) (wire (pts (xy 101.6 50.8) (xy 101.6 64.77)) (stroke (width 0) (type default)) (uuid 16277490-d7d7-465a-85f5-ae26c2ce3037) @@ -1723,10 +1717,6 @@ (effects (font (size 1.27 1.27)) (justify left)) (uuid 8796c5b6-5c37-408a-b24b-6b4972032774) ) - (hierarchical_label "MOT_EN_B" (shape output) (at 73.66 90.17 180) (fields_autoplaced) - (effects (font (size 1.27 1.27)) (justify right)) - (uuid 894d13f1-3c40-4736-b73e-02a2c6d763f5) - ) (hierarchical_label "TIM2_CH2" (shape output) (at 128.27 72.39 0) (fields_autoplaced) (effects (font (size 1.27 1.27)) (justify left)) (uuid 8c9ad854-f4a8-44f5-815e-cf78df03cc66) @@ -1755,10 +1745,6 @@ (effects (font (size 1.27 1.27)) (justify right)) (uuid b431fb32-a08c-4eb8-89f2-23b466b1f8bd) ) - (hierarchical_label "MOT_EN_A" (shape output) (at 128.27 90.17 0) (fields_autoplaced) - (effects (font (size 1.27 1.27)) (justify left)) - (uuid cabf158c-1393-4821-973b-380760327da4) - ) (hierarchical_label "TIM3_CH2" (shape input) (at 73.66 118.11 180) (fields_autoplaced) (effects (font (size 1.27 1.27)) (justify right)) (uuid d358859b-ec8e-4c3a-bf01-3644c8c83381) diff --git a/lemon-pepper.pdf b/lemon-pepper.pdf index 58c5ec0d6b5b97d6ebda9bb5a6496895546f5407..17ea681aeed7d6215d4e1d0f4afe726c5296651f 100644 GIT binary patch delta 175102 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 z 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⋙Oy@PF)7hTC_CTZrbaopQz_o6QN2|Q_>vL$ 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

s{kJ&{nId?aoKa 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�a|D)u>iQ%4TWO7~}{GOUJ*+ogSJ+~rlOtDzsP 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

z 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%0Darn0sY! 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

  • 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±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

    ~^yAG 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≶xqIM5P-okBQrXA4iL^rS*03D7%yGdzZFT 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

    tSCOTlPo)ABVa*2)xI*6YI`Uhi*?Nl#7 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 ZX6G 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 Gz5XAye?Nl& diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..0ccaa55 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,41 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = lemon-pepper +boards_dir = firmware/boards +src_dir = firmware/src +lib_dir = firmware/lib +include_dir = firmware/include +test_dir = firmware/test + +[env:lemon-pepper] +platform = ststm32 +board = genericSTM32G431CB +board_build.f_cpu = 168000000 +board_build.ldscript = ./firmware/ldscript.ld +framework = arduino +upload_protocol = stlink +debug_tool = stlink +; monitor_port = /dev/cu.usbmodem207C345A56501 +monitor_speed = 115200 +monitor_eol = LF +build_flags = + -D USBCON + -D SIMPLEFOC_STM32_DEBUG + -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC + -D HAL_FDCAN_MODULE_ENABLED + -D FDCAN_ALT1 + -D SN65HVD23x + -D ARDUINO_GENERIC_G431CBUX +lib_deps = + askuric/Simple FOC@^2.2.3 + simplefoc/SimpleFOCDrivers@^1.0.2 +lib_archive = false

    +dI}t8L2PG1@--LOnhW31CVXh~ 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 THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lemon-pepper/platformio.ini b/lemon-pepper/platformio.ini new file mode 100644 index 0000000..1542056 --- /dev/null +++ b/lemon-pepper/platformio.ini @@ -0,0 +1,14 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:genericSTM32G431CB] +platform = ststm32 +board = genericSTM32G431CB +framework = arduino diff --git a/lemon-pepper/test/README b/lemon-pepper/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/lemon-pepper/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/pinout.png b/pinout.png index 9d575779faea4b46b1f7603fcdaebdd102721281..c3cb44ed937bab59b86332ef78fa8d1a4b1132ed 100644 GIT binary patch literal 342911 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 literal 353540 zcmeFZby!tf_$>@u5D`VBQAr7D0qGP0X^=)zx{=zXghdL{9nwlS(jX0j!w>GRIQSggXXG+1$O*2NHMPn&&LVZi#SVfKV}*-O+QZw-{JmV`a%wy&XBlkiHcDg^|in=Y}3}u z*x*YYdk5+#LQF4IJMev*C6{jO!8zQeWAJlCCg@}X{oZQe&EA3~b(0xHgwbRAM~|mn zoe&`GCi&qr6557&?ac_vEX50XvC7v9GDK)algP6@q`623 zH4%-)u$ZtBoM{GYEUK@r)lu^&eg5H<*kQ`}i*2Z>yHd=L9w(mY8ZH%u+Pe`-R(>wo z7aZje!wJ}o?ngCC&T;$lyl1^}5WQxIzA~Fa{O*;x&D0N%q7Sh!_*>XkS8otj=n{tI zlD*N+x3BuqN1`TYAF+jPJ&t~8yPSE898VOPdc5$+6|uWf7R=ofufLcS=dY z=UY=9?ypfW{2pJOx#Et_@rdf#@cp+44aI90Z~boVzHdA3ayw2z+12yaJRTxT?{dR3 zx?kLg=jZU4xZURvcl5!}!V0u)V)oZwE=vua?{*d&7&a~s&u~r?Ux?CbH%h)2=pTB| zYp%6NYAF(;Juy*L2?czO{U7)0EfonrcVkS@4xf zMPbR%z*2GtA1zTi^$GIB_p6E|h{k;%!A;otLp#0YQG#M7@rnN2A71?z<2O8A?p)_D zzE46ELU9_J=SB9}B4?E^m+Jd|$Z=>}99M$v@XOxb3r%68WQ_5`Wac9A5zqA-a2UTY zX=5A;sbx!C5%zjE{;h)iw&Eeqd1eVh1+Kx{XWyoOgh!gN_ZCeRA$9_5Sd)cyPJ*xI z(x;LbJeDvKO2n+c=E0}c?nEr^Z0~4)RpjJVXnoz3SXbR$KlwIin?`aJmkMlbY_f}< zcjd4`1gXvClWAVdt7nc6uDm?JQKw@0=ur7f)Y}%5vvS~<_w9#QhF=QFH{5hV6~?(H z`k319ij+QXkPu$J16#~h6QAQ0vvy-3F6F(skIW2p$hi-g{sMR*9DH!~b&s zmp4%gdCisN+h~-ODsSH}Q?~k{8~E~2-e|_a6l)44rpA;KXUCzWdTc_8^^raO-e*xR zN~d<+c^rEfudgFz#YYTD*4;bDZw<1qe|dK&HEP)`!iT-@;UnxsKa&*YKG9NQodC7e zvSsSA$;7XUO2D29`&UdZA73 zty4cdyF2o28$YjOH_q9y2oWT|VQtLW-f+1;6|C1tK68G=4U5zlDZKB3JMwnt?Tw~K zqL`mv7e>DP`11N@23Zq7RcQ8;QmU$NwJ~DdR6W!zl==5JY02o|x5;l~NK%K9bp+4L z1yI{b9;f5>Kkt`UVq9VW?ECAnlxBxUhZMaey{1&QO1jFY(kCl1{l*zt(#ajGMp(sk zykXtrKtyujPVTa51uk&MJ&;) zGi*H*$~hA`P7BBk$h$5>BQBm=tYRGBX&0%*V4JP-g=Q!uPWF2iOUCds1*I)z;p~1D zQkBs!0z+TF41M|VC0bSQOXv{OV2nlh;PoMf!Gjzfl{IC$9IY=UgS@K_gE>RDK2Ix> z|41