Motor Controller for a Robot Arm

Created 2 project docs

Firmware Starter — STM32G431CBT6

View Document

MCU: STM32G431CBT6 Framework: STM32 HAL

Board Bring-Up Plan — Closed-Loop DC Motor Controller

View Document

Equipment: current-limited bench supply, DMM, oscilloscope, ST-LINK/SWD debugger, USB-UART adapter, encoder simulator or known encoder, dumm...
Implemented and verified the schematic for the closed-loop brushed DC motor controller.
What was completed:
  • Added a robust 12 V nominal motor-controller architecture: MC33926 H-bridge, STM32G431 MCU, INA240 current sense, LMR33630 3.3 V buck, LM74700 reverse protection, encoder conditioning, connectors, LEDs, and protection.
  • Wired power, motor drive, current sensing, encoder A/B/Z, SWD, UART control, reset/BOOT0, and fault/status paths.
  • Ran schematic/ERC checks; there are no open ERROR/WARNING/UNCERTAIN checks in the reviewed schematic categories.
Project files created/updated:
Open items:
  • Layout is not started yet; high-current motor paths, shunt Kelvin routing, buck layout, and thermal copper will be critical.
  • Firmware starter is a HAL bring-up baseline; final PID/current-limit tuning still needs motor characterization.
Suggested next steps:
  1. Set PCB constraints: board size, stackup, current-aware trace widths, and clearance rules.
  2. Place components with power path and thermal priorities.
  3. Route the PCB, then run DRC/PI checks.
  4. Add test points for 3V3, VM_PROT, HBRIDGE_EN, current sense, and encoder signals.
Set constraints and layout
Set PCB constraints
Place components
Route the PCB
Add test points
Review BOM risks

Refine this doc
Ask about this doc
Firmware Starter — STM32G431CBT6
Platform & Toolchain
  • MCU: STM32G431CBT6
  • Framework: STM32 HAL
  • Build system: STM32CubeIDE or PlatformIO STM32Cube
  • Primary functions: PWM H-bridge drive, TIM3 quadrature encoder, ADC current monitoring, UART telemetry/control, safe fault handling.
Pin Mapping

Table


FunctionMCU PinNetConnected ToDirectionNotes
H-bridge IN1 PWMPA8HBRIDGE_IN1_PWMU2 IN1Output AF TIM1_CH1PWM / direction control
H-bridge IN2 PWMPA9HBRIDGE_IN2_PWMU2 IN2Output AF TIM1_CH2PWM / direction control
H-bridge D1PA10HBRIDGE_D1U2 D1GPIO outputHigh disables; default pull-down R10
H-bridge D2_NPA11HBRIDGE_D2_NU2 D2GPIO outputLow disables; default pull-up R11
H-bridge enablePB5HBRIDGE_ENU2 ENGPIO outputKeep low until firmware armed; pull-down R12
H-bridge faultPB6HBRIDGE_FAULT_NU2 SFGPIO input pull-upActive-low fault; pull-up R7
Coarse driver FB ADCPA4HBRIDGE_FB_ADCU2 FB/R6/C25ADC inputMC33926 current mirror auxiliary feedback
Precision current ADCPA1MOTOR_CURRENT_ADCU4 INA240 output filterADC inputZero-current midpoint about 1.65 V
Encoder APA6ENC_AU6 1YAF TIM3_CH1Quadrature encoder input
Encoder BPA7ENC_BU6 2YAF TIM3_CH2Quadrature encoder input
Encoder Z/indexPB7ENC_ZU6 3YGPIO input EXTIOptional index pulse
Buck power-goodPB12PWR_GOODU3 PG/R3GPIO inputHigh when 3.3 V buck OK
UART TXPA2UART_TXJ5 pin 3AF USART2_TXControl/telemetry
UART RXPA3UART_RXJ5 pin 4AF USART2_RXControl/telemetry
Fault/status LEDPB0LED_FAULTLED2 cathodeGPIO outputActive-low LED sink
Heartbeat LEDPB1LED_HEARTBEATLED3 cathodeGPIO outputActive-low LED sink
SWDIOPA13SWDIOJ4 pin 2AF SWDDebug/programming
SWCLKPA14SWCLKJ4 pin 4AF SWDDebug/programming
SWOPB3SWOJ4 pin 6AF traceOptional trace output
ResetPG10-NRSTNRSTJ4 pin 5 / SW1ResetManual reset and SWD reset
Boot modePB8-BOOT0BOOT0R4 pull-downBoot strapNormal flash boot when low
Dependencies & Project Setup
PlatformIO example

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
For production, create a CubeMX project for STM32G431CBT6 LQFP-48 and assign these peripherals:
  • TIM1 CH1 = PA8 PWM, TIM1 CH2 = PA9 PWM.
  • TIM3 encoder mode TI1/TI2 = PA6/PA7.
  • ADC input on PA1 and PA4.
  • USART2 TX/RX = PA2/PA3 at 115200 baud.
  • GPIO outputs: PA10, PA11, PB5, PB0, PB1.
  • GPIO inputs: PB6, PB7, PB12.
Complete Firmware Source
This is a HAL 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);
    }
}
Build & Flash Instructions
  • Generate a CubeMX project for STM32G431CBT6 LQFP-48 with the pin assignments above.
  • Build in STM32CubeIDE, or run pio run if using PlatformIO with a suitable custom board definition.
  • Connect SWD to J4: 3V3 reference, SWDIO, SWCLK, NRST, GND.
  • Flash with STM32CubeProgrammer or your ST-LINK tool.
  • Monitor UART on J5 at 115200 baud, 3.3 V logic.
Bring-Up Firmware Safety Notes
  • Keep HBRIDGE_EN low until ADC current measurement, PWR_GOOD, and HBRIDGE_FAULT_N are initialized.
  • Do not connect the real motor until current ADC zero-current offset and dummy-load current scaling are verified.
  • Add the actual PID loop only after open-loop PWM and current limiting have been validated.
  • Platform & Toolchain

  • Pin Mapping

  • Dependencies & Project Setup

  • PlatformIO example

  • Complete Firmware Source

  • Build & Flash Instructions

  • Bring-Up Firmware Safety Notes

Closed-Loop DC Motor Controller

Closed-Loop DC Motor Controller thumbnail
Robotics-ready closed-loop brushed DC motor controller with bidirectional 2 A H-bridge drive, quadrature encoder feedback, current sensing, protection, MCU control, debug access, and status indicators.

Properties

Properties describe core aspects of the project.

Pricing & Availability

Distributor

Qty 1

Arrow

$13.69–$22.87

Digi-Key

$18.16

HQonline

$3.75

LCSC

$17.95–$18.00

Mouser

$28.86

TME

$8.93

Verical

$11.17–$25.75

Controls