Motor Controller for a Robot Arm
Board Bring-Up Plan — Closed-Loop DC Motor Controller
Table
| Function | MCU Pin | Net | Connected To | Direction | Notes |
|---|---|---|---|---|---|
| H-bridge IN1 PWM | PA8 | HBRIDGE_IN1_PWM | U2 IN1 | Output AF TIM1_CH1 | PWM / direction control |
| H-bridge IN2 PWM | PA9 | HBRIDGE_IN2_PWM | U2 IN2 | Output AF TIM1_CH2 | PWM / direction control |
| H-bridge D1 | PA10 | HBRIDGE_D1 | U2 D1 | GPIO output | High disables; default pull-down R10 |
| H-bridge D2_N | PA11 | HBRIDGE_D2_N | U2 | GPIO output | Low disables; default pull-up R11 |
| H-bridge enable | PB5 | HBRIDGE_EN | U2 EN | GPIO output | Keep low until firmware armed; pull-down R12 |
| H-bridge fault | PB6 | HBRIDGE_FAULT_N | U2 | GPIO input pull-up | Active-low fault; pull-up R7 |
| Coarse driver FB ADC | PA4 | HBRIDGE_FB_ADC | U2 FB/R6/C25 | ADC input | MC33926 current mirror auxiliary feedback |
| Precision current ADC | PA1 | MOTOR_CURRENT_ADC | U4 INA240 output filter | ADC input | Zero-current midpoint about 1.65 V |
| Encoder A | PA6 | ENC_A | U6 1Y | AF TIM3_CH1 | Quadrature encoder input |
| Encoder B | PA7 | ENC_B | U6 2Y | AF TIM3_CH2 | Quadrature encoder input |
| Encoder Z/index | PB7 | ENC_Z | U6 3Y | GPIO input EXTI | Optional index pulse |
| Buck power-good | PB12 | PWR_GOOD | U3 PG/R3 | GPIO input | High when 3.3 V buck OK |
| UART TX | PA2 | UART_TX | J5 pin 3 | AF USART2_TX | Control/telemetry |
| UART RX | PA3 | UART_RX | J5 pin 4 | AF USART2_RX | Control/telemetry |
| Fault/status LED | PB0 | LED_FAULT | LED2 cathode | GPIO output | Active-low LED sink |
| Heartbeat LED | PB1 | LED_HEARTBEAT | LED3 cathode | GPIO output | Active-low LED sink |
| SWDIO | PA13 | SWDIO | J4 pin 2 | AF SWD | Debug/programming |
| SWCLK | PA14 | SWCLK | J4 pin 4 | AF SWD | Debug/programming |
| SWO | PB3 | SWO | J4 pin 6 | AF trace | Optional trace output |
| Reset | PG10-NRST | NRST | J4 pin 5 / SW1 | Reset | Manual reset and SWD reset |
| Boot mode | PB8-BOOT0 | BOOT0 | R4 pull-down | Boot strap | Normal flash boot when low |
Ini
[env:stm32g431cb] platform = ststm32 board = nucleo_g431kb ; use as a close STM32G4 HAL target, or create a custom board file for STM32G431CBT6 framework = stm32cube monitor_speed = 115200 build_flags = -DSTM32G431xx
main.c starter. It assumes CubeMX generated the HAL startup files and clock tree for STM32G431CBT6.C
#include "main.h" #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <string.h> TIM_HandleTypeDef htim1; // PWM to MC33926 IN1/IN2 TIM_HandleTypeDef htim3; // Quadrature encoder PA6/PA7 ADC_HandleTypeDef hadc1; // PA1 precision current ADC ADC_HandleTypeDef hadc2; // PA4 driver FB ADC UART_HandleTypeDef huart2; // PA2/PA3 control/telemetry #define HBRIDGE_D1_GPIO_Port GPIOA #define HBRIDGE_D1_Pin GPIO_PIN_10 #define HBRIDGE_D2N_GPIO_Port GPIOA #define HBRIDGE_D2N_Pin GPIO_PIN_11 #define HBRIDGE_EN_GPIO_Port GPIOB #define HBRIDGE_EN_Pin GPIO_PIN_5 #define HBRIDGE_FAULTN_GPIO_Port GPIOB #define HBRIDGE_FAULTN_Pin GPIO_PIN_6 #define PWR_GOOD_GPIO_Port GPIOB #define PWR_GOOD_Pin GPIO_PIN_12 #define ENC_Z_GPIO_Port GPIOB #define ENC_Z_Pin GPIO_PIN_7 #define LED_FAULT_GPIO_Port GPIOB #define LED_FAULT_Pin GPIO_PIN_0 #define LED_HEART_GPIO_Port GPIOB #define LED_HEART_Pin GPIO_PIN_1 #define PWM_PERIOD_TICKS 3399U // 170 MHz / (3399+1) ~= 50 kHz if timer clock is 170 MHz #define CURRENT_ZERO_MV 1650.0f #define INA240_GAIN 20.0f #define SHUNT_OHMS 0.010f #define CURRENT_LIMIT_A_START 1.0f static volatile bool motor_armed = false; static uint32_t last_telemetry_ms = 0; static void Error_Handler_Local(void); static void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART2_UART_Init(void); static void MX_TIM1_Init(void); static void MX_TIM3_Init(void); static void MX_ADC1_Init(void); static void MX_ADC2_Init(void); static void uart_print(const char *s) { HAL_UART_Transmit(&huart2, (uint8_t *)s, (uint16_t)strlen(s), 100); } static uint16_t adc_read_once(ADC_HandleTypeDef *hadc) { if (HAL_ADC_Start(hadc) != HAL_OK) return 0; if (HAL_ADC_PollForConversion(hadc, 10) != HAL_OK) return 0; uint16_t value = (uint16_t)HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return value; } static float adc_counts_to_mv(uint16_t counts) { return (3300.0f * (float)counts) / 4095.0f; } static float read_motor_current_a(void) { uint16_t counts = adc_read_once(&hadc1); float mv = adc_counts_to_mv(counts); float sense_v = (mv - CURRENT_ZERO_MV) / 1000.0f; return sense_v / (INA240_GAIN * SHUNT_OHMS); } static uint32_t read_encoder_count(void) { return __HAL_TIM_GET_COUNTER(&htim3); } static void hbridge_force_safe(void) { motor_armed = false; HAL_GPIO_WritePin(HBRIDGE_EN_GPIO_Port, HBRIDGE_EN_Pin, GPIO_PIN_RESET); // EN low = sleep HAL_GPIO_WritePin(HBRIDGE_D1_GPIO_Port, HBRIDGE_D1_Pin, GPIO_PIN_RESET); // D1 low = not disabled by D1 HAL_GPIO_WritePin(HBRIDGE_D2N_GPIO_Port, HBRIDGE_D2N_Pin, GPIO_PIN_SET); // D2_N high = not disabled by D2 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 0); } static bool safety_inputs_ok(void) { if (HAL_GPIO_ReadPin(PWR_GOOD_GPIO_Port, PWR_GOOD_Pin) == GPIO_PIN_RESET) return false; if (HAL_GPIO_ReadPin(HBRIDGE_FAULTN_GPIO_Port, HBRIDGE_FAULTN_Pin) == GPIO_PIN_RESET) return false; return true; } static void hbridge_arm(void) { hbridge_force_safe(); if (!safety_inputs_ok()) return; HAL_GPIO_WritePin(HBRIDGE_EN_GPIO_Port, HBRIDGE_EN_Pin, GPIO_PIN_SET); motor_armed = true; } static void motor_set_signed_pwm(float command) { if (!motor_armed || !safety_inputs_ok()) { hbridge_force_safe(); return; } if (command > 1.0f) command = 1.0f; if (command < -1.0f) command = -1.0f; uint32_t duty = (uint32_t)((command >= 0 ? command : -command) * PWM_PERIOD_TICKS); if (command >= 0.0f) { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 0); } else { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, duty); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); MX_TIM1_Init(); MX_TIM3_Init(); MX_ADC1_Init(); MX_ADC2_Init(); hbridge_force_safe(); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); uart_print("Closed-loop DC motor controller boot\r\n"); hbridge_arm(); while (1) { float current_a = read_motor_current_a(); if (current_a > CURRENT_LIMIT_A_START || current_a < -CURRENT_LIMIT_A_START || !safety_inputs_ok()) { hbridge_force_safe(); HAL_GPIO_WritePin(LED_FAULT_GPIO_Port, LED_FAULT_Pin, GPIO_PIN_RESET); // LED on, active-low sink } else { HAL_GPIO_WritePin(LED_FAULT_GPIO_Port, LED_FAULT_Pin, GPIO_PIN_SET); // LED off } // Safe starter behavior: keep motor stopped until command parser is added. motor_set_signed_pwm(0.0f); uint32_t now = HAL_GetTick(); if (now - last_telemetry_ms >= 500) { last_telemetry_ms = now; HAL_GPIO_TogglePin(LED_HEART_GPIO_Port, LED_HEART_Pin); char line[128]; int n = snprintf(line, sizeof(line), "i=%.3fA enc=%lu fault_n=%u pgood=%u z=%u\r\n", current_a, (unsigned long)read_encoder_count(), HAL_GPIO_ReadPin(HBRIDGE_FAULTN_GPIO_Port, HBRIDGE_FAULTN_Pin), HAL_GPIO_ReadPin(PWR_GOOD_GPIO_Port, PWR_GOOD_Pin), HAL_GPIO_ReadPin(ENC_Z_GPIO_Port, ENC_Z_Pin)); HAL_UART_Transmit(&huart2, (uint8_t *)line, (uint16_t)n, 100); } HAL_Delay(5); } } static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOA, HBRIDGE_D1_Pin | HBRIDGE_D2N_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(HBRIDGE_D2N_GPIO_Port, HBRIDGE_D2N_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, HBRIDGE_EN_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, LED_FAULT_Pin | LED_HEART_Pin, GPIO_PIN_SET); // active-low LEDs off GPIO_InitStruct.Pin = HBRIDGE_D1_Pin | HBRIDGE_D2N_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = HBRIDGE_EN_Pin | LED_FAULT_Pin | LED_HEART_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.Pin = HBRIDGE_FAULTN_Pin | PWR_GOOD_Pin | ENC_Z_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } static void MX_USART2_UART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; huart2.Init.ClockPrescaler = UART_PRESCALER_DIV1; if (HAL_UART_Init(&huart2) != HAL_OK) Error_Handler_Local(); } static void MX_TIM1_Init(void) { TIM_OC_InitTypeDef sConfigOC = {0}; htim1.Instance = TIM1; htim1.Init.Prescaler = 0; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = PWM_PERIOD_TICKS; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_PWM_Init(&htim1) != HAL_OK) Error_Handler_Local(); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) Error_Handler_Local(); if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) Error_Handler_Local(); } static void MX_TIM3_Init(void) { TIM_Encoder_InitTypeDef sConfig = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0xFFFF; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 4; sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler = TIM_ICPSC_DIV1; sConfig.IC2Filter = 4; if (HAL_TIM_Encoder_Init(&htim3, &sConfig) != HAL_OK) Error_Handler_Local(); } static void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; if (HAL_ADC_Init(&hadc1) != HAL_OK) Error_Handler_Local(); sConfig.Channel = ADC_CHANNEL_2; // PA1 = ADC12_IN2 sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) Error_Handler_Local(); } static void MX_ADC2_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc2.Instance = ADC2; hadc2.Init.Resolution = ADC_RESOLUTION_12B; hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc2.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc2.Init.ContinuousConvMode = DISABLE; hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START; if (HAL_ADC_Init(&hadc2) != HAL_OK) Error_Handler_Local(); sConfig.Channel = ADC_CHANNEL_17; // PA4 = ADC2_IN17 on STM32G431 sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK) Error_Handler_Local(); } static void SystemClock_Config(void) { // Prefer generating this in CubeMX for the exact oscillator/PLL settings. // This fallback uses HAL defaults; replace with CubeMX 170 MHz configuration before performance testing. } static void Error_Handler_Local(void) { hbridge_force_safe(); while (1) { HAL_GPIO_TogglePin(LED_FAULT_GPIO_Port, LED_FAULT_Pin); HAL_Delay(100); } }
pio run if using PlatformIO with a suitable custom board definition.HBRIDGE_EN low until ADC current measurement, PWR_GOOD, and HBRIDGE_FAULT_N are initialized.Platform & Toolchain
Pin Mapping
Dependencies & Project Setup
PlatformIO example
Complete Firmware Source
Build & Flash Instructions
Bring-Up Firmware Safety Notes