Reading encoder values of GA12-N20 motor and PI velocity control through STM32


Read encoder values

We use a GA12-N20 motor whose PPR (pulse per revolution) is $7$ (ref). The velocity of a motor is derived by applying the following equation (ref)

\mathrm{velocity} [\mathrm{rpm}] = 60 \times \frac{\mathrm{encoder \ value}}{\mathrm{interval}} \times \frac{1}{\mathrm{PPR} \times \mathrm{gear \ ratio} \times \mathrm{encoder \ number}}


  • PPR: $7$
  • gear ratio: $100$
  • encoder number: $2$ (This motor has two encoders).

Here, the measurement timer frequency and the interval is determined as follows (ref).

\mathrm{frequency} &= \frac{\mathrm{frequency \ of \ the \ clock}}{\mathrm{prescaler} \times \mathrm{counter \ period}} \\
\mathrm{interval} &= \frac{1}{\mathrm{frequency}}


  • frequency of the clock: $8,000,000 \ \mathrm{Hz}$
  • prescaler: $10$
  • counter period: $8,000$

The frequency is $100 \ \mathrm{Hz}$ and the interval is $10 \ \mathrm{ms}$.

Hardware setup

These are configurations for the experimental equipment.

host PCDELL XPS 13
host OSubuntu 22.04
IDESTM32CubeIDE 1.13.1
microcontrollerNUCLEO STM32 F030R8T6

The circuit configuration is as follows.

Nch MOSFETK27962 pieces
Pch MOSFET2SJ6812 pcs
gate resistor$220 \, \mathrm{\Omega}$2 pcs
gate to source resistor$0.477 \, \mathrm{k \Omega}$4 pcs
capacitor$8.5 \, \mathrm{\mu F}$1 piece
tactile switch2 pcs
diodeS44 pcs
motorGB12-N20B (chosen in this article)1 pc
AA battery (単3電池)Panasonic EVOLTA4 pcs

We use 6 PINs and each PIN has the following role.

PA0A0input for tactile switch blue
PA1A1input for tactile switch red
PA8D7Timer 1 channel 1: PWM for motor
PA9D8Timer 1 channel 2: PWM for motor
PA6D12Timer 3 channel 1: read encoder value from hall signal B
PA7D11Timer 3 channel 2: read encoder value from hall signal A

From the website, the wiring pattern is shown below.

STM32CubeIDE and Kicad view should help your understanding.

Software configuration

Overall source codes can be found here.

Timer usage

Each timer has the following role.

  • Timer 1: PWM signal generation
  • Timer 3: Acquire encoder value
  • Timer 6: A basic timer for control and measurement

Timer 1

The following configuration is adopted. According to the counter period (ARR: Auto reload register value), the maximum value of the CCR is 255.

Timer 3

We use the encoder mode for timer 3 and measure the encoder value.

The following video explains the usage of encoder mode for STM32 timers. It explains that using this mode is very useful in terms of avoiding the jitter of the encoder.


Adopt the default prescaler and counter period and we check only TI1 trigger (not TI2).

Timer 6

In order to achieve $100 \ \mathrm{Hz}$, the prescaler and the counter period are set as follows.

Timer 6 is a basic timer that periodically measures the rotational speed of the motor and calculates the PWM command. We dig into read_encoder_count(), calculate_rpm(), and control_motor_when_button_pressed() function more in the next section.

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
	if ( htim == &htim6 )
		int enc_count = read_encoder_count();
		int target_rpm = 60;
		int measured_rpm = calculate_rpm(enc_count);
		printf("%d\n\r", measured_rpm);

		control_motor_when_button_pressed(target_rpm, measured_rpm);

Function explanation

int read_encoder_count (void)

int read_encoder_count(void)
  int enc_buff = (int16_t) TIM3->CNT;
  TIM3->CNT = 0;
  return enc_buff;

This function gets the encoder value and sets the value as zero. By doing this and divide by the interval, we can get the rotational velocity of the wheel.

TIM3->CNT is originally an unsigned 32 bit integer. Converting this into a signed integer results in a negative value when the motor rotates in a counter-clockwise direction.

int calculate_rpm(int timer_count)

int calculate_rpm(int timer_count)
	// motor specification
	const int PPR = 7;
	const int encoder_num = 2;
	const int gear_ratio = 100;

	// timer specification
	const int prescaler = htim6.Init.Prescaler + 1;
	const int period = htim6.Init.Period + 1;
	const int clock_frequency = HAL_RCC_GetPCLK1Freq();
	const double frequency = (double) clock_frequency / ((double) prescaler * period);
	const double interval = 1.0 / frequency;

	// rpm calculation
	const double timer_count_in_1s = (double) timer_count / interval;
	const int measured_rpm = (60 * timer_count_in_1s) / (PPR * gear_ratio * encoder_num);
	return measured_rpm;

Utilizing these equations gives us the measured rotational velocity [rpm].

\mathrm{velocity} [\mathrm{rpm}] &= 60 \times \frac{\mathrm{encoder \ value}}{\mathrm{interval}} \times \frac{1}{\mathrm{PPR} \times \mathrm{gear \ ratio} \times \mathrm{encoder \ number}} \\
\mathrm{frequency} &= \frac{\mathrm{frequency \ of \ the \ clock}}{\mathrm{prescaler} \times \mathrm{counter \ period}} \\
\mathrm{interval} &= \frac{1}{\mathrm{frequency}}

Timer 6 uses APB1 clock as shown below and the frequency is acquired by the API HAL_RCC_GetPCLK1Freq() (ref).

int calculate_pid_cmd(int target_rpm, int measured_rpm)

PI control for the motor is implemented here. The target velocity [rpm] and measured velocity [rpm] are compared. A feed-forward term is added for stable motion. We adopt saturation for the integral term and the command.

int calculate_pid_cmd(int target_rpm, int measured_rpm)
	// pid gain and parameter
  	const int kp = 1;
  	const int ki = 1;
  	const int ff = 100;
  	const int max_ei  = 300;
  	const int max_cmd = 255;

  	// deviation calculation
  	const int e = target_rpm - abs(measured_rpm);
  	ei_ += e;
  	ei_ = max(min(ei_, max_ei), -max_ei);

  	// command calculation
  	int cmd = kp * e + ki * ei_ + ff;
  	cmd = max(min(max_cmd, cmd), 0);

  	return cmd;

void control_motor_when_button_pressed(int target_rpm, const int measured_rpm)

Depending on which button is pressed, the motor can change the rotational direction.

void control_motor_when_button_pressed(int target_rpm, const int measured_rpm)
	// check button states
	const uint8_t button_state0 = HAL_GPIO_ReadPin(SW0_GPIO_Port, SW0_Pin);
  	const uint8_t button_state1 = HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin);

  	// calculate pid command
  	target_rpm = button_state0 != button_state1 ? target_rpm : 0;
  	const int cmd = calculate_pid_cmd(target_rpm, measured_rpm);

  	// blue button: clockwise
  	if (button_state0 == 1) __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, cmd);
  	else __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);

  	// red button: counter-clockwise
  	if (button_state1 == 1) __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, cmd);
  	else __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 0);

Control performance

The rotational velocity graphs of a motor are as follows.

Target velocity is 20 [rpm] case

Target velocity is 60 [rpm] case

Target velocity is 100 [rpm] case

In either case, the graph plots show that the result was pretty good though there is a bit overshoot.

Tips for plotting data

SWV (Serial Wire Viewer)

Unfortunately, I could not use this method since our module is using cortex m0.

SWV options is not available and not work with Cortex-M0.

How do I enable SWV in my STM32Cube ide?

printf to COM port + Python data plot

I used Python for plotting data. For data collection, I used the following command referring to this site.

stdbuf -o0 cat /dev/ttyACM1 > rpm_data.log

Used the following very simple Python code for data plot (ref).

import pandas as pd
import matplotlib.pyplot as plt

data = pd.read_csv('rpm_data.log',sep='\s+',header=None)
data = pd.DataFrame(data)
y = data[0][400:]
x = range(len(y))
t = []
for i in x:
    i *= 0.01    # data acquired in 100Hz

plt.plot(t, y, 'r--')
plt.title("motor control; target: 60 [rpm]")
plt.xlabel("time [s]")
plt.ylabel("rotational velocity [rpm]")
plt.ylim(-120, 120)