From 4f30d9f99675fdffcf027c1f31eb0359306c8cc5 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 18:42:42 -0700 Subject: [PATCH 1/4] enable DMA for ADC1 in CubeMX --- Firmware/Board/v3/Odrive.ioc | 14 +++++++++++++- Firmware/Board/v3/Src/adc.c | 24 ++++++++++++++++++++++++ Firmware/Board/v3/Src/dma.c | 4 ++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Firmware/Board/v3/Odrive.ioc b/Firmware/Board/v3/Odrive.ioc index ce8e1e2f..9aca69e1 100644 --- a/Firmware/Board/v3/Odrive.ioc +++ b/Firmware/Board/v3/Odrive.ioc @@ -78,9 +78,20 @@ ADC3.ScanConvMode=DISABLE CAN1.CalculateTimeBit=1142 CAN1.CalculateTimeQuantum=380.95238095238096 CAN1.IPParameters=CalculateTimeQuantum,CalculateTimeBit +Dma.ADC1.2.Direction=DMA_PERIPH_TO_MEMORY +Dma.ADC1.2.FIFOMode=DMA_FIFOMODE_DISABLE +Dma.ADC1.2.Instance=DMA2_Stream0 +Dma.ADC1.2.MemDataAlignment=DMA_MDATAALIGN_HALFWORD +Dma.ADC1.2.MemInc=DMA_MINC_ENABLE +Dma.ADC1.2.Mode=DMA_CIRCULAR +Dma.ADC1.2.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD +Dma.ADC1.2.PeriphInc=DMA_PINC_DISABLE +Dma.ADC1.2.Priority=DMA_PRIORITY_LOW +Dma.ADC1.2.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode Dma.Request0=UART4_RX Dma.Request1=UART4_TX -Dma.RequestsNb=2 +Dma.Request2=ADC1 +Dma.RequestsNb=3 Dma.UART4_RX.0.Direction=DMA_PERIPH_TO_MEMORY Dma.UART4_RX.0.FIFOMode=DMA_FIFOMODE_DISABLE Dma.UART4_RX.0.Instance=DMA1_Stream2 @@ -198,6 +209,7 @@ NVIC.ADC_IRQn=true\:5\:0\:false\:false\:true\:true\:true NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:true NVIC.DMA1_Stream2_IRQn=true\:5\:0\:false\:false\:true\:true\:true NVIC.DMA1_Stream4_IRQn=true\:5\:0\:false\:false\:true\:true\:false +NVIC.DMA2_Stream0_IRQn=true\:5\:0\:false\:false\:false\:true\:false NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:true NVIC.EXTI2_IRQn=true\:0\:0\:false\:false\:false\:false\:true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:true diff --git a/Firmware/Board/v3/Src/adc.c b/Firmware/Board/v3/Src/adc.c index 6d0db380..0af9cbba 100644 --- a/Firmware/Board/v3/Src/adc.c +++ b/Firmware/Board/v3/Src/adc.c @@ -51,6 +51,7 @@ #include "adc.h" #include "gpio.h" +#include "dma.h" /* USER CODE BEGIN 0 */ @@ -63,6 +64,7 @@ ADC_HandleTypeDef hadc1; ADC_HandleTypeDef hadc2; ADC_HandleTypeDef hadc3; +DMA_HandleTypeDef hdma_adc1; /* ADC1 init function */ void MX_ADC1_Init(void) @@ -255,6 +257,25 @@ void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle) GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + /* ADC1 DMA Init */ + /* ADC1 Init */ + hdma_adc1.Instance = DMA2_Stream0; + hdma_adc1.Init.Channel = DMA_CHANNEL_0; + hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; + hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; + hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; + hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + hdma_adc1.Init.Mode = DMA_CIRCULAR; + hdma_adc1.Init.Priority = DMA_PRIORITY_LOW; + hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) + { + _Error_Handler(__FILE__, __LINE__); + } + + __HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1); + /* ADC1 interrupt Init */ HAL_NVIC_SetPriority(ADC_IRQn, 5, 0); HAL_NVIC_EnableIRQ(ADC_IRQn); @@ -354,6 +375,9 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) HAL_GPIO_DeInit(GPIOA, M1_TEMP_Pin|AUX_I_Pin|VBUS_S_Pin); + /* ADC1 DMA DeInit */ + HAL_DMA_DeInit(adcHandle->DMA_Handle); + /* ADC1 interrupt Deinit */ /* USER CODE BEGIN ADC1:ADC_IRQn disable */ /** diff --git a/Firmware/Board/v3/Src/dma.c b/Firmware/Board/v3/Src/dma.c index a725a585..55d5e034 100644 --- a/Firmware/Board/v3/Src/dma.c +++ b/Firmware/Board/v3/Src/dma.c @@ -68,6 +68,7 @@ void MX_DMA_Init(void) { /* DMA controller clock enable */ __HAL_RCC_DMA1_CLK_ENABLE(); + __HAL_RCC_DMA2_CLK_ENABLE(); /* DMA interrupt init */ /* DMA1_Stream2_IRQn interrupt configuration */ @@ -76,6 +77,9 @@ void MX_DMA_Init(void) /* DMA1_Stream4_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 5, 0); HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn); + /* DMA2_Stream0_IRQn interrupt configuration */ + HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0); + HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); } From b9268e961720c7dccf7a87248415dbfd68fac4e4 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 19:02:45 -0700 Subject: [PATCH 2/4] disable IRQ for DMA2_Stream0 This DMA stream is used to read values from ADC1 while ADC1 cycles through it's sequence of input channels. No interrupts are required to make this work. --- Firmware/Board/v3/Src/dma.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Firmware/Board/v3/Src/dma.c b/Firmware/Board/v3/Src/dma.c index 55d5e034..3de873ad 100644 --- a/Firmware/Board/v3/Src/dma.c +++ b/Firmware/Board/v3/Src/dma.c @@ -78,8 +78,10 @@ void MX_DMA_Init(void) HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 5, 0); HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn); /* DMA2_Stream0_IRQn interrupt configuration */ - HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0); - HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); + // Dear STM, no we _don't_ want to fire an interrupt for this DMA + // (it's not possible to deselect this in CubeMX) + //HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0); + //HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); } From 59187ef1f29f88f2d53d5fa1a4d1946490f155a2 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Mon, 26 Mar 2018 19:05:29 -0700 Subject: [PATCH 3/4] add patch file for DMA IRQ --- .../0003-disable-IRQ-for-DMA2_Stream0.patch | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Firmware/Board/v3/0003-disable-IRQ-for-DMA2_Stream0.patch diff --git a/Firmware/Board/v3/0003-disable-IRQ-for-DMA2_Stream0.patch b/Firmware/Board/v3/0003-disable-IRQ-for-DMA2_Stream0.patch new file mode 100644 index 00000000..282317d1 --- /dev/null +++ b/Firmware/Board/v3/0003-disable-IRQ-for-DMA2_Stream0.patch @@ -0,0 +1,33 @@ +From ab5ca860b3729d76a9c43c485776147ab69d2342 Mon Sep 17 00:00:00 2001 +From: Samuel Sadok +Date: Mon, 26 Mar 2018 19:02:45 -0700 +Subject: [PATCH] disable IRQ for DMA2_Stream0 + +This DMA stream is used to read values from ADC1 +while ADC1 cycles through it's sequence of input +channels. No interrupts are required to make +this work. +--- + Firmware/Board/v3/Src/dma.c | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/Firmware/Board/v3/Src/dma.c b/Firmware/Board/v3/Src/dma.c +index 55d5e03..3de873a 100644 +--- a/Firmware/Board/v3/Src/dma.c ++++ b/Firmware/Board/v3/Src/dma.c +@@ -78,8 +78,10 @@ void MX_DMA_Init(void) + HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 5, 0); + HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn); + /* DMA2_Stream0_IRQn interrupt configuration */ +- HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0); +- HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); ++ // Dear STM, no we _don't_ want to fire an interrupt for this DMA ++ // (it's not possible to deselect this in CubeMX) ++ //HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 5, 0); ++ //HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); + + } + +-- +2.16.2 + From b064800116ea03aacaa4e94b9caf29177cc434b4 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Fri, 27 Apr 2018 17:46:11 -0700 Subject: [PATCH 4/4] Add general purpose ADC API ADC1 is configured to sample channels 0 to 15 continuously at about 30kHz. Users can now set their GPIO of choice to analog mode (as long as it's wired to one of the analog channels) and then read the voltage at any time. The values can be read like this: GPIO_set_to_analog(GPIO_3_GPIO_Port, GPIO_3_Pin); my_voltage = get_adc_voltage(GPIO_3_GPIO_Port, GPIO_3_Pin); --- Firmware/Board/v3/Inc/gpio.h | 2 +- Firmware/Board/v3/Src/gpio.c | 11 +++ Firmware/CHANGELOG.md | 1 + Firmware/MotorControl/low_level.cpp | 106 +++++++++++++++++++++++ Firmware/MotorControl/low_level.h | 3 + Firmware/MotorControl/main.cpp | 3 + Firmware/MotorControl/odrive_main.h | 2 + Firmware/communication/communication.cpp | 4 + 8 files changed, 131 insertions(+), 1 deletion(-) diff --git a/Firmware/Board/v3/Inc/gpio.h b/Firmware/Board/v3/Inc/gpio.h index f8ffe61b..6ec71035 100644 --- a/Firmware/Board/v3/Inc/gpio.h +++ b/Firmware/Board/v3/Inc/gpio.h @@ -71,11 +71,11 @@ void MX_GPIO_Init(void); /* USER CODE BEGIN Prototypes */ void SetGPIO12toUART(); -void SetupENCIndexGPIO(); bool GPIO_subscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin, uint32_t pull_up_down, void (*callback)(void*), void* ctx); void GPIO_unsubscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); +void GPIO_set_to_analog(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); /* USER CODE END Prototypes */ diff --git a/Firmware/Board/v3/Src/gpio.c b/Firmware/Board/v3/Src/gpio.c index 9585749f..b3941ff3 100644 --- a/Firmware/Board/v3/Src/gpio.c +++ b/Firmware/Board/v3/Src/gpio.c @@ -260,6 +260,17 @@ void GPIO_unsubscribe(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { HAL_NVIC_DisableIRQ(get_irq_number(GPIO_pin)); } +// @brief Configures the specified GPIO as an analog input. +// This disables any subscriptions that were active for this pin. +void GPIO_set_to_analog(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { + GPIO_InitTypeDef GPIO_InitStruct; + GPIO_unsubscribe(GPIO_port, GPIO_pin); + GPIO_InitStruct.Pin = GPIO_pin; + GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; + GPIO_InitStruct.Pull = GPIO_NOPULL; + HAL_GPIO_Init(GPIO_port, &GPIO_InitStruct); +} + //Dispatch processing of external interrupts based on source void HAL_GPIO_EXTI_Callback(uint16_t GPIO_pin) { for (size_t i = 0; i < n_subscriptions; ++i) { diff --git a/Firmware/CHANGELOG.md b/Firmware/CHANGELOG.md index b675c703..59188818 100644 --- a/Firmware/CHANGELOG.md +++ b/Firmware/CHANGELOG.md @@ -35,6 +35,7 @@ Please add a note of your changes below this heading if you make a Pull Request. * **USB Bootloader** * `make erase_config` to erase the configuration with an STLink (the configuration can also be erased from within explore_odrive.py, using `odrv0.erase_configuration()`) * Travis-CI builds firmware for all board versions and deploys the binaries when a tag is pushed to master +* General purpose ADC API. See function get_adc_voltage() in low_level.cpp for more detais. ### Changed * Most of the code from `lowlevel.c` moved to `axis.cpp`, `encoder.cpp`, `controller.cpp`, `sensorless_estimator.cpp`, `motor.cpp` and the corresponding header files diff --git a/Firmware/MotorControl/low_level.cpp b/Firmware/MotorControl/low_level.cpp index 41d58990..19b0500f 100644 --- a/Firmware/MotorControl/low_level.cpp +++ b/Firmware/MotorControl/low_level.cpp @@ -312,6 +312,112 @@ void low_level_fault(Motor::Error_t error) { safety_critical_disarm_brake_resistor(); } +// @brief ADC1 measurements are written to this buffer by DMA +uint16_t adc_measurements_[ADC_CHANNEL_COUNT] = { 0 }; + +// @brief Starts the general purpose ADC on the ADC1 peripheral. +// The measured ADC voltages can be read with get_adc_voltage(). +// +// ADC1 is set up to continuously sample all channels 0 to 15 in a +// round-robin fashion. +// DMA is used to copy the measured 12-bit values to adc_measurements_. +// +// The injected (high priority) channel of ADC1 is used to sample vbus_voltage. +// This conversion is triggered by TIM1 at the frequency of the motor control loop. +void start_general_purpose_adc() { + ADC_ChannelConfTypeDef sConfig; + + // Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) + hadc1.Instance = ADC1; + hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + hadc1.Init.Resolution = ADC_RESOLUTION_12B; + hadc1.Init.ScanConvMode = ENABLE; + hadc1.Init.ContinuousConvMode = ENABLE; + hadc1.Init.DiscontinuousConvMode = DISABLE; + hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; + hadc1.Init.NbrOfConversion = ADC_CHANNEL_COUNT; + hadc1.Init.DMAContinuousRequests = ENABLE; + hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + if (HAL_ADC_Init(&hadc1) != HAL_OK) + { + _Error_Handler((char*)__FILE__, __LINE__); + } + + // Set up sampling sequence (channel 0 ... channel 15) + sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; + for (uint32_t channel = 0; channel < ADC_CHANNEL_COUNT; ++channel) { + sConfig.Channel = channel << ADC_CR1_AWDCH_Pos; + sConfig.Rank = channel + 1; // rank numbering starts at 1 + if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) + _Error_Handler((char*)__FILE__, __LINE__); + } + + HAL_ADC_Start_DMA(&hadc1, reinterpret_cast(adc_measurements_), ADC_CHANNEL_COUNT); +} + +// @brief Returns the ADC voltage associated with the specified pin. +// GPIO_set_to_analog() must be called first to put the Pin into +// analog mode. +// Returns NaN if the pin has no associated ADC1 channel. +// +// On ODrive 3.3 and 3.4 the following pins can be used with this function: +// GPIO_1, GPIO_2, GPIO_3, GPIO_4 and some pins that are connected to +// on-board sensors (M0_TEMP, M1_TEMP, AUX_TEMP) +// +// The ADC values are sampled in background at ~30kHz without +// any CPU involvement. +// +// Details: each of the 16 conversion takes (15+26) ADC clock +// cycles and the ADC, so the update rate of the entire sequence is: +// 21000kHz / (15+26) / 16 = 32kHz +// The true frequency is slightly lower because of the injected vbus +// measurements +float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin) { + uint32_t channel = UINT32_MAX; + if (GPIO_port == GPIOA) { + if (GPIO_pin == GPIO_PIN_0) + channel = 0; + else if (GPIO_pin == GPIO_PIN_1) + channel = 1; + else if (GPIO_pin == GPIO_PIN_2) + channel = 2; + else if (GPIO_pin == GPIO_PIN_3) + channel = 3; + else if (GPIO_pin == GPIO_PIN_4) + channel = 4; + else if (GPIO_pin == GPIO_PIN_5) + channel = 5; + else if (GPIO_pin == GPIO_PIN_6) + channel = 6; + else if (GPIO_pin == GPIO_PIN_7) + channel = 7; + } else if (GPIO_port == GPIOB) { + if (GPIO_pin == GPIO_PIN_0) + channel = 8; + else if (GPIO_pin == GPIO_PIN_1) + channel = 9; + } else if (GPIO_port == GPIOC) { + if (GPIO_pin == GPIO_PIN_0) + channel = 10; + else if (GPIO_pin == GPIO_PIN_1) + channel = 11; + else if (GPIO_pin == GPIO_PIN_2) + channel = 12; + else if (GPIO_pin == GPIO_PIN_3) + channel = 13; + else if (GPIO_pin == GPIO_PIN_4) + channel = 14; + else if (GPIO_pin == GPIO_PIN_5) + channel = 15; + } + if (channel < ADC_CHANNEL_COUNT) + return ((float)adc_measurements_[channel]) * (3.3f / (float)(1 << 12)); + else + return 0.0f / 0.0f; // NaN +} + //-------------------------------- // IRQ Callbacks //-------------------------------- diff --git a/Firmware/MotorControl/low_level.h b/Firmware/MotorControl/low_level.h index e3784788..d99c9916 100644 --- a/Firmware/MotorControl/low_level.h +++ b/Firmware/MotorControl/low_level.h @@ -39,6 +39,9 @@ void start_adc_pwm(); void start_pwm(TIM_HandleTypeDef* htim); void sync_timers(TIM_HandleTypeDef* htim_a, TIM_HandleTypeDef* htim_b, uint16_t TIM_CLOCKSOURCE_ITRx, uint16_t count_offset); +void start_general_purpose_adc(); + +float get_adc_voltage(GPIO_TypeDef* GPIO_port, uint16_t GPIO_pin); void update_brake_current(); diff --git a/Firmware/MotorControl/main.cpp b/Firmware/MotorControl/main.cpp index b7ec03d5..217e9c2b 100644 --- a/Firmware/MotorControl/main.cpp +++ b/Firmware/MotorControl/main.cpp @@ -81,6 +81,9 @@ int odrive_main(void) { *encoder, *sensorless_estimator, *controller, *motor); } + // Start ADC for temperature measurements and user measurements + start_general_purpose_adc(); + // TODO: make dynamically reconfigurable #if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR >= 3 if (board_config.enable_uart) { diff --git a/Firmware/MotorControl/odrive_main.h b/Firmware/MotorControl/odrive_main.h index a66fcb74..6e72270f 100644 --- a/Firmware/MotorControl/odrive_main.h +++ b/Firmware/MotorControl/odrive_main.h @@ -34,6 +34,8 @@ extern bool user_config_loaded; extern uint64_t serial_number; extern char serial_number_str[13]; +#define ADC_CHANNEL_COUNT 16 +extern uint16_t adc_measurements_[ADC_CHANNEL_COUNT]; #ifdef __cplusplus } diff --git a/Firmware/communication/communication.cpp b/Firmware/communication/communication.cpp index 27e08bfb..322404ed 100644 --- a/Firmware/communication/communication.cpp +++ b/Firmware/communication/communication.cpp @@ -113,6 +113,10 @@ static inline auto make_obj_tree() { ), make_protocol_object("axis0", axes[0]->make_protocol_definitions()), make_protocol_object("axis1", axes[1]->make_protocol_definitions()), +#if HW_VERSION_MAJOR == 3 && HW_VERSION_MINOR == 4 + make_protocol_property("adc_gpio1", &adc_measurements_[0]), + make_protocol_property("adc_gpio2", &adc_measurements_[1]), +#endif make_protocol_function("save_configuration", static_functions, &StaticFunctions::save_configuration_helper), make_protocol_function("erase_configuration", static_functions, &StaticFunctions::erase_configuration_helper), make_protocol_function("reboot", static_functions, &StaticFunctions::NVIC_SystemReset_helper),