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

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