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),