2. STM32 Version

2.1 STM32 Installation & Wiring

2.1.1 STM32 Controller Installation & Wiring

2.1.2 Battery Installation & Wiring

2.2 Development Tutorial (STM32 Version)

Once the program is downloaded, the car chassis executes a sequence of actions in the following order:

2.2.1 Execution Sequence

(1) Move forward for 4 seconds

(2) Reverse for 4 seconds

(3) Turn left for 4 seconds

(4) Return to the initial position

(5) Move forward to the right for 4 seconds

(6) Return to the original position

(7) A 1-second interval separates each action.

2.2.2 Hardware Introduction

(1) STM32 Main Control Module Introduction

The controller utilizes the STM32F407VET6 and the motor drive chip employed is the YX4055 (over-current protection). The voltage range is from 3V to 12V, depending on the actual voltage applied to the motor. The interface layout is as follow:

The interface instruction is as follow:

Interface type Mark Instruction
Encoder motor interface GND The negative terminal of the hall power supply
A A phase pulse signal output terminal
B B phase pulse signal output terminal
VCC The positive terminal of the hall power supply
M+ The positive terminal of the motor power supply
M- The negative terminal of the motor power supply
Power port + The positive terminal of the power source
- The negative terminal of the power source

(2) Steering Servo

The steering servo in this chassis utilizes the LD-1501MG PWM servo model.

The LD-1501MG operates on a PWM servo mechanism. To control it, a PWM signal with a 20ms period is sent to the signal end. The servo angle is adjusted by manipulating the pulse width within the range of 500 to 2500μs. This corresponds to an angle range of 0 to 180°, and the recommended operating voltage is between 6V and 8.4V.

The PWM waveform signal is employed to regulate the servo position. When the PWM signal is fed into the motor drive circuit, the motor rotation is controlled based on the signal level. A high PWM duty cycle results in a greater driving force, enabling a larger rotation angle. Conversely, a low duty cycle yields a smaller driving force, resulting in a reduced motor rotation angle.

By continuously fine-tuning the PWM duty cycle, the microcontroller can precisely control the motor’s rotation angle. This, in turn, drives the mechanical structure to rotate, achieving accurate position control of the steering gear output shaft.

The above picture is the PWM servo wiring interface diagram, and the following table is the pin distribution table:

PIN Function
White wire Signal wire
Red wire Power positive electrode
Black wire Ground wire

(3) Encoder Geared Motor

The motor model employed by the suspension Ackermann chassis is JGB37-520R30-12, where J represents a DC motor, GB represents an electronic output shaft, 520 represents the motor type, R30 represents a reduction ratio of 1:30, 12 represents the a rated voltage of 12V. Its interface description is shown in the figure below:

The Hall encoder disk is aligned with the motor along the same shaft. As the motor rotates, the Hall device detects the motion and generates a series of pulse signals. To ascertain the steering direction, it typically produces two sets of square wave signals with a specific phase difference.

2.2.3 Wiring

The Ackerman car’s wiring setup is as follows: Connect the front steering servo to the J1 interface of the STM32 main control board using the specified wiring interface. Additionally, link the left motor to the M1 interface and the right motor to the M2 interface on the STM32 main control board. The detailed wiring method is illustrated below.

2.2.4 Program Download

Source Code

After the project is compiled completely, download the generated hex file to the STM32 control board. Please prepare the following hardware materials:

(1) Software & Hardware Preparation for USB Download

① Software: FlyMcu (it is located in 2. Software Tools/ 01 STM32 Installation Pack/ USB Serial Port Download/ FlyMcu)

② Hardware: Type-C cable, STN32 main control board

(2) USB Download Operation Steps

The specific operation steps are as follow:

① Hardware connection

Use Type-C cable to connect the Type-C port (UART) of the STM32 control board and the USB port of your computer:

② Basic setting

Open FlyMcu. Click “Enum Port” at top menu bar, then set the baud rate (bps) as 115200:

Click “STMISP” option and configure as shown in the following figure.

Select “DTR Low(<-3V),ISP @RTS High”:

③ Software burning

In the FlyMcu software interface, click to select the hex file to be burned. The program below is just for your reference, please rely on the actual program. Select the hex file in 03 Program/RosRobotControllerM4_MINACKER/MDK-ARM/ RosRobotControllerM4/RosRobotControllerM4.hex.

Go back to the previous interface and click the “Start ISP” to burn the generated hex file onto the STM32 main control board:

The burning is in progress.:

The burning is complete, you will receive the following prompt.

In addition to use USB method for program download, you can also use either st-link or j-link method for direct compilation and burning.

::{Note} To avoid the presence of any exception during the burning process, please strictly follow the above operation steps to operate!

2.2.5 Outcome

Following the program download, the car chassis executes a sequence of actions in the specified order:

(1) Move forward for 4 seconds.

(2) Reverse for 4 seconds.

(3) Turn left for 4 seconds.

(4) Return to the initial position.

(5) Move forward to the right for 4 seconds.

(6) Return to the original position.

(7) Each action is separated by a 1-second interval.

2.2.6 Source Code Analysis

Source Code

(1) Robot Motion Program Analysis

① Robot Motion Settings

When the Ackerman chassis invokes the chassis_init() function to initialize the chassis type, the functions within various chassis structure variables are directed to their respective control functions, as illustrated in the figure below:

132
133
134
135
136
    //Initialize the motion parameters of the chassis motor
    chassis_init();     

    //Select the chassis type
    set_chassis_type(Chassis_run_type);
27
28
//Chassis type (default is the Large Ackerman chassis)
uint32_t Chassis_run_type = CHASSIS_TYPE_JETACKER;

In the program located at ..\Hiwonder\System\app.c, the minacker_control() function is employed to manage the movement of the small Ackerman chassis. Here’s a concise description of its functionality:

Within this function, the following actions are executed:

  • Set the speed of the small Ackerman chassis in the X-axis direction to 300 millimeters per second.

  • Move the driving wheel forward with the steering wheel in a neutral position, resulting in the car moving straight ahead.

  • After a 2-second delay, bring the car to a stop for 1 second.

  • Move the driving wheel backward with the steering wheel in a neutral position, causing the car to move straight backward.

  • After another 2-second delay, pause for 1 second.

  • Move the driving wheel forward once more, this time turning the steering wheel left for a left turn.

  • After a 1-second delay, return the steering wheel to the right direction, concluding the movement.

This describes the controlled movement of the small Ackerman chassis in the specified program.

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
void minacker_control(void)
{
    //Define the motor's movement speed
    //The suggested range is [50, 300]
    static float speed = 150.0f;
	
	//Move at a linear velocity of speed along the X-axis (i.e., move forward)
	chassis->set_velocity(chassis, speed, 0, 0);
	osDelay(2000); //Delay 2 seconds
	
	chassis->stop(chassis); //stop
	osDelay(1000); //Delay for 1 second
	
	chassis->set_velocity(chassis, -speed, 0, 0); //astern motion
	osDelay(2000); //Delay 2 seconds
	
	chassis->stop(chassis); //stop
	osDelay(1000); //Delay for 1 second
	
	//Perform a circular motion around a circle with a radius of 500mm to its left at the linear speed of the X-axis speed
	chassis->set_velocity_radius(chassis, speed, 500, true);
	osDelay(2000); //Delay 2 seconds
	
	chassis->stop(chassis); //stop
	osDelay(1000); //Delay for 1 second
	
	//Perform a circular motion around a circle with a radius of 500mm to its left at the linear speed of the X-axis speed
	chassis->set_velocity_radius(chassis, -speed, 500, true);
	osDelay(2000); //Delay 2 seconds
	
	chassis->stop(chassis); //stop
	osDelay(1000); //Delay for 1 second

② Set Straight Line

Within the file path ..\Hiwonder\Chassis\ackermann_chassis.c, the movement of the small Ackermann chassis is governed by the kinematic algorithm. The straight-line trajectory of the chassis is specifically regulated using the set_velocity() function.

186
187
	//Move at a linear velocity of speed along the X-axis (i.e., move forward)
	chassis->set_velocity(chassis, speed, 0, 0);
117
118
119
120
121
122
123
124
125
126
127
128
void ackermann_chassis_object_init(AckermannChassisTypeDef *self){
	if(self->base.chassis_type == CHASSIS_TYPE_JETACKER)
	{
		self->base.stop = jetacker_stop;
		self->base.set_velocity = jetacker_set_velocity;
		self->base.set_velocity_radius = jetacker_set_velocity_radius;
	}else{
		self->base.stop = minacker_stop;
		self->base.set_velocity = minacker_set_velocity;
		self->base.set_velocity_radius = minacker_set_velocity_radius;
	}
}
107
108
109
110
static void minacker_set_velocity(void *self, float vx, float vy, float r)
{
    minacker_chassis_move(self, vx, r);
}

The kinematic solution for the Ackermann chassis is depicted in the accompanying figure. To achieve regular car movement, the calculation involves determining the speed of each Ackermann motor. When the car moves straight, with a turning radius of 0 and a turning angle of 0 degrees, the speeds of the left and right wheels (vl and vr) are synchronized with the speed of vx. The set_motors() function is then employed to establish these motor speeds, with the servo angle set to 1500 to maintain a centered position. This comprehensive process ensures the Ackermann chassis exhibits consistent and predictable movements, particularly during straight-line motion.

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//minacker
void minacker_chassis_move(AckermannChassisTypeDef *self, float vx, float r )
{
	float vr = 0 , vl = 0;
	float angle = 0;
	if(r != 0)
	{
		angle = atan(self->shaft_length/r);
		vl = vx/r * (r - self->wheelbase/2);
		vr = vx/r * (r + self->wheelbase/2);
	}else{ //r == 0 go straight
		angle = 0;
		vr = vx;
		vl = vx;
	}
	vr = linear_speed_to_rps(self , vr); //Find the linear velocity of the right wheel
	vl = linear_speed_to_rps(self , vl); //Find the linear velocity of the left wheel
	if(angle > PI/4.5f)
	{
		angle = PI/4.5f;
	}else if(angle < -PI/4.5f)
	{
		angle = -PI/4.5f;
	}
	angle = -2000/PI * angle + 1500; //Find the steering Angle
	self->set_motors(self , vl , vr , angle);
}

③ Set Robot’s Turning

Control the chassis turning using set_velocity_radius() function.

112
113
114
115
static void minacker_set_velocity_radius(void* self, float linear, float r,bool swerve)
{
		minacker_chassis_move(self, linear, r);
}

During turns, the rear motors operate similarly to when the robot moves straight ahead. The turning angle of the servo is computed based on the turning radius (r) and the length of the vehicle body shaft (shaft_length, i.e., the distance between the front and rear wheels). To determine the speeds (vl and vr) of the left and right driving wheels, respectively, the axis length (wheelbase) of the two wheels and the linear speed (vx) of the movement are considered. The function linear_speed_to_rps() converts the linear wheel speed into rotational speed, and the turning angle is restricted to prevent servo damage, capped at ±40 degrees. The expression angle = -2000/PI * angle + 1500; converts the servo angle value into a control value for servo rotation, with 1500 as the initial centering value for the servo.

Subsequently, the set_motors(self, vl, vr, angle); function assigns the motor and servo values to the motor motion control function, ultimately regulating the motion of both the motor and servo.

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//minacker
void minacker_chassis_move(AckermannChassisTypeDef *self, float vx, float r )
{
	float vr = 0 , vl = 0;
	float angle = 0;
	if(r != 0)
	{
		angle = atan(self->shaft_length/r);
		vl = vx/r * (r - self->wheelbase/2);
		vr = vx/r * (r + self->wheelbase/2);
	}else{ //r == 0 go straight
		angle = 0;
		vr = vx;
		vl = vx;
	}
	vr = linear_speed_to_rps(self , vr); //Find the linear velocity of the right wheel
	vl = linear_speed_to_rps(self , vl); //Find the linear velocity of the left wheel
	if(angle > PI/4.5f)
	{
		angle = PI/4.5f;
	}else if(angle < -PI/4.5f)
	{
		angle = -PI/4.5f;
	}
	angle = -2000/PI * angle + 1500; //Find the steering Angle
	self->set_motors(self , vl , vr , angle);
}

The figure below illustrates the application of Ackermann’s kinematics formula in the program, providing a visual representation of the kinematic principles employed.

Steering servo angle formula:

Left wheel speed formula:

Right wheel speed formula:

④ Control Robot to Stop

The stop() function is designed to bring the robot to a halt by setting the motor speed to 0, effectively ceasing its movement.

246
	chassis->stop(chassis); //stop
102
103
104
105
static void minacker_stop(void *self)
{
    ((AckermannChassisTypeDef*)self)->set_motors(self, 0, 0,1500);
}

(2) AB Quadrature Encoder Motor Analysis

The preceding analysis establishes the fundamental movements of the car, followed by a detailed examination of motor control specifics.

① Initialize Timer

128
MX_TIM1_Init();
130
MX_TIM5_Init();
139
MX_TIM7_Init();

In the main function within the main.c file, several timers are initialized. We will focus on elucidating the motor control process for Motor 1, with the understanding that the control mechanisms for the other motors remain identical. Motor 1 involves the utilization of three timers: Timer 1 (for PWM control of motor speed), Timer 5 (for obtaining motor speed through encoder), and Timer 7 (for timed interrupts to update motor speed measurements and PID control frequency). Additionally, interrupt initialization is performed. The figure below illustrates the parameter configuration in STM32CubeMX, and for a more intuitive view, it is recommended to refer to the tim.c file or use the STM32CubeMX software interface for configuration settings.

Timer 1 (PWM Control Motor Speed)

Timer 5 (Encoder measures and captures the speed of the motor)

Timer 7(update motor speed measurements and regulate the frequency of PID control)

② Motor Initialization

Before controlling the motor, it is necessary to initialize the relevant motor parameters located in the app.c function under the folder ..\Hiwonder\System\app.c:

108
109
    /* hardware initialization*/
    motors_init();      //Motor initialization

The below code segment shows the initialization of the motor, located in ..\Hiwonder\motor_porting.c. It initializes the motor structure. In the next section, “Chassis Motor Motion Parameter Initialization”, the parameters of this motor will be overridden. Pay attention to selecting the appropriate chassis type.

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void motors_init(void)
{
    for(int i = 0; i < 4; ++i) {
        motors[i] = LWMEM_CCM_MALLOC(sizeof( EncoderMotorObjectTypeDef));
        encoder_motor_object_init(motors[i]);
		motors[i]->ticks_overflow = 60000;
        motors[i]->ticks_per_circle = MOTOR_DEFAULT_TICKS_PER_CIRCLE;
        motors[i]->rps_limit = MOTOR_DEFAULT_RPS_LIMIT;
        motors[i]->pid_controller.set_point = 0.0f;
        motors[i]->pid_controller.kp = MOTOR_DEFAULT_PID_KP;
        motors[i]->pid_controller.ki = MOTOR_DEFAULT_PID_KI;
        motors[i]->pid_controller.kd = MOTOR_DEFAULT_PID_KD;
    }

    /* Madagascar 1*/
    motors[0]->set_pulse = motor1_set_pulse;
    __HAL_TIM_SET_COUNTER(&htim1, 0);
    __HAL_TIM_ENABLE(&htim1);
    __HAL_TIM_MOE_ENABLE(&htim1);

LWMEM_CCM_MALLOC() dynamically creates a group of objects, allocating memory for objects of type EncoderMotorObjectTypeDef, and stores the memory address in the corresponding position of the motors array. The following is the structure parameters for motor1, located in …\Hiwonder\Peripherals\encoder_motor.h. For specific parameter contents, please refer to the annotations shown in the figure.

46
        motors[i] = LWMEM_CCM_MALLOC(sizeof( EncoderMotorObjectTypeDef));
20
21
22
23
24
25
26
27
28
29
30
/**
 * @brief Encoder motor object structure
*/
struct EncoderMotorObject{
    int64_t counter;        /**< @brief The total value, 64-bit, is considered not to overflow */
    int64_t overflow_num;   /**< @brief Overflow count */
    int32_t ticks_overflow; /**< @brief Count overflow value */
    float tps;              /**< @brief ticks per second Counter frequency */
    float rps;              /**< @brief revolutions per second The output shaft rotates at a speed of revolutions per second */
    int current_pulse;      /**< @brief The current output PWM value has a sign corresponding to the forward and reverse rotation */
    PID_ControllerTypeDef pid_controller; /**< @brief PID controller */

Initialize the defined structure (encoder_motor_object_init), located in …\Hiwonder\Portings\motor_porting.c.

47
        encoder_motor_object_init(motors[i]);

The following program is located in ..\Hiwonder\Peripherals\encoder_motor.c:

72
73
74
75
76
77
78
79
80
81
82
void encoder_motor_object_init(EncoderMotorObjectTypeDef *self)
{
    self->counter = 0;
    self->overflow_num = 0;
    self->tps = 0; 
    self->rps = 0;
	self->current_pulse = 0;
	self->ticks_overflow = 0; 
    self->ticks_per_circle = 9999; /* The number of counts generated by one rotation of the motor output shaft should be filled in according to the actual situation of the motor */
	pid_controller_init(&self->pid_controller, 0, 0, 0);
}

Use Timer 1 to generate PWM signal, as shown in the below figure:

57
58
59
60
61
    /* Madagascar 1 */
    motors[0]->set_pulse = motor1_set_pulse;
    __HAL_TIM_SET_COUNTER(&htim1, 0);
    __HAL_TIM_ENABLE(&htim1);
    __HAL_TIM_MOE_ENABLE(&htim1);

Use TIM5 to control the acquisition and start the encoder of the motor, as shown in the following figure:

63
64
65
66
67
68
    /* encoder */
    __HAL_TIM_SET_COUNTER(&htim5, 0);
    __HAL_TIM_CLEAR_IT(&htim5, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE_IT(&htim5, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE(&htim5);
    HAL_TIM_Encoder_Start(&htim5, TIM_CHANNEL_ALL);

Use TIM7 to calculate the encoder value once to obtain the speed, as shown in the following figure:

114
115
116
117
118
    // Speed test update timer
    __HAL_TIM_SET_COUNTER(&htim7, 0);
    __HAL_TIM_CLEAR_IT(&htim7, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE_IT(&htim7, TIM_IT_UPDATE);
    __HAL_TIM_ENABLE(&htim7);

③ Chassis Motor Motion Parameter Initialization

In the program located in ..\Hiwonder\System\app.c, initialize the motion parameters of the chassis motors. For the Ackermann chassis, use CHASSIS_TYPE_MINACKER.

27
28
//Chassis type (default is the Large Ackerman chassis)
uint32_t Chassis_run_type = CHASSIS_TYPE_JETACKER;
132
133
134
135
    //Initialize the motion parameters of the chassis motor
    chassis_init();     
    //Select the chassis type
    set_chassis_type(Chassis_run_type);

In the program located in ..\Hiwonder\Portings\chassis_porting.c, the chassis_init() function initializes the motion parameters for the car chassis motors.

73
74
void chassis_init(void)
{
113
114
115
116
117
118
119
120
121
	//Initialization of the small Ackermann chassis structure
    minacker.base.chassis_type = CHASSIS_TYPE_MINACKER;
    minacker.correction_factor = MINACKER_CORRECITION_FACTOR;
    minacker.wheel_diameter = MINACKER_WHEEL_DIAMETER;
    minacker.shaft_length = MINACKER_SHAFT_LENGTH;
    minacker.wheelbase = MINACKER_WHEELBASE;
    minacker.set_motors = minacker_set_motors;
	ackermann_chassis_object_init(&minacker); //The initialization of the structure is placed later
}

The macro definitions for the parameters are located in ..\Hiwonder\Chassis\chassis.h. Among them, WHEEL_DIAMETER represents the diameter of the wheels. SHAFT_LENGTH represents the shaft length, i.e., the distance between the left and right wheels. CORRECTION_FACTOR is the conversion ratio coefficient, which is currently not in use.

55
56
57
58
#define MINACKER_WHEEL_DIAMETER 60.0 /* mm */
#define MINACKER_CORRECITION_FACTOR 1.0 /* mm */
#define MINACKER_SHAFT_LENGTH 170.0 /* mm */
#define MINACKER_WHEELBASE  180.0 /* mm */

Following initializes the motor of the ackerman chassis car.

96
97
void set_chassis_type(uint8_tchassis_type)
{
162
163
164
165
166
167
168
		case CHASSIS_TYPE_MINACKER:
			chassis = (ChassisTypeDef*)&minacker;
            set_motor_type(motors[0], MOTOR_TYPE_JGB520);
            set_motor_type(motors[1], MOTOR_TYPE_JGB520);
            set_motor_type(motors[2], MOTOR_TYPE_JGB520);
            set_motor_type(motors[3], MOTOR_TYPE_JGB520);
            break;

After jumping, you can obtain the corresponding parameters of the motor.

112
113
114
115
116
117
118
119
/* Each revolution of the motor shaft generates 11 pulses. The counter counts evenly at the rising and falling edges of phases A and B, meaning the count value is four times the number of pulses. The reduction ratio is 90.0.
 * That is, for each revolution of the motor output shaft, the counter value changes by 11.0 * 4.0 * 90.0 = 3960
 */
#define MOTOR_JGB520_TICKS_PER_CIRCLE 3960.0f
#define MOTOR_JGB520_PID_KP  63.0f
#define MOTOR_JGB520_PID_KI  2.6f
#define MOTOR_JGB520_PID_KD  2.4f
#define MOTOR_JGB520_RPS_LIMIT 1.5f

④ PWM Speed Control

Based on our previous explanation, after setting the velocity along the x-axis and the angular velocity, perform kinematic analysis to obtain the speeds of the left and right motors, denoted as rps_l and rps_r respectively. Motor 1 corresponds to the left wheel, and Motor 2 corresponds to the right wheel. Finally, use the function self->set_motors(self, rps_l, rps_r) to set the motor speeds.

14
15
16
17
18
19
20
21
void diff_chassis_move(DifferentialChassisTypeDef *self,float vxfloat angule_rate)
{
	float v_l = VX - angule_rate *((DifferentialChassisTypeDef*)self)->shaft_length / 2.0f;
	float V_r =VX_+ angule_rate *((DifferentialChassisTypeDef*)self)->shaft_length / 2.0f;
	float rps_1=linear_speed_to_rps(self,v1);
	float rps_r =linear_speed_to_rps(self,v_r);
	self->set_motors(self,rps_1,rps_r);
}

We have selected the MINACKER Ackermann chassis vehicle. The function tankblack_set_motors sets the speeds of the two motors.

113
114
115
116
117
118
119
120
121
	//Initialization of the small Ackermann chassis structure
    minacker.base.chassis_type = CHASSIS_TYPE_MINACKER;
    minacker.correction_factor = MINACKER_CORRECITION_FACTOR;
    minacker.wheel_diameter = MINACKER_WHEEL_DIAMETER;
    minacker.shaft_length = MINACKER_SHAFT_LENGTH;
    minacker.wheelbase = MINACKER_WHEELBASE;
    minacker.set_motors = minacker_set_motors;
	ackermann_chassis_object_init(&minacker); //The initialization of the structure is placed later
}

Navigate to the minacker_set_motors function, depicted in the figure below, to configure the motor speeds for both Motor 1 and Motor 2.

59
60
61
62
63
64
65
66
67
68
69
static void minacker_set_motors(void* self, float rps_lh, float rps_lt,int position)
{
	static int position_last = 0;
	if( position_last != position )
	{
		pwm_servo_set_position(pwm_servos[0] , position , 100);
		position_last = position;
	}
    encoder_motor_set_speed(motors[1], -rps_lh);
    encoder_motor_set_speed(motors[0], rps_lt);
}

In the program located in ..\Hiwonder\Peripherals\encoder_motor.c, implement speed limitation and set the target speed for the PID controller.

60
61
62
63
64
void encoder_motor_set_speed(EncoderMotorObjectTypeDef *self, float rps)
{
	rps = rps > self->rps_limit ? self->rps_limit : (rps < -self->rps_limit ? -self->rps_limit : rps); /* Limit the speed */
    self->pid_controller.set_point = rps; /* Set the PID controller target */
}

The encoder_motor_control() function is responsible for updating the speed control of the encoder motor. It adjusts the PWM value to stabilize the motor at the previously set target speed. First, it updates the PID parameters, then corrects the output PWM, and finally sets the new PWM signal using self->set_pulse(). The specific implementation of the program is as follows (in ..\Hiwonder\Peripherals\encoder_motor.c):

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
 * @brief Encoder motor speed control task
 * @detials The encoder motor speed PID control task needs to be specified at regular intervals to complete the PID control update
 * @param self Encoder motor object pointer
 * @param period The time interval between the current update and the last update (update cycle), measured in sec
 * @retval None.
*/
void encoder_motor_control(EncoderMotorObjectTypeDef *self, float period) 
{
	float pulse = 0;
    pid_controller_update(&self->pid_controller, self->rps, period);   /* Update the PID controller */
    pulse = self->current_pulse + self->pid_controller.output; /* Calculate the new PWM value */
	
	/* Limit the output PWM value. The limit is determined by the timer setting. In this example, the duty cycle set by the timer from 0 to 100 corresponds to 0 to 1000 */
    pulse = pulse > 1000 ?  1000 : pulse; 
    pulse = pulse < -1000 ? -1000 : pulse;
	
    self->set_pulse(self, pulse > -250 && pulse < 250 ? 0 : pulse); /* Set a new PWM value and limit the minimum value of PWM. If the PWM is too small, the motor will only buzz but not move */
    self->current_pulse = pulse; /* Record the new PWM value */
}

self->set_pulse passes the PWM value to the motor1_set_pulse() function. When the speed is positive, it starts TIM_CHANNEL_4 (forward). When the speed is negative, it starts TIM_CHANNEL_3 (reverse). When the speed is zero, both channels stop, and the motor does not rotate. As shown in the figure below, located in ..\Hiwonder\Portings\motor_porting.c.

49
	self->set_pulse(self, pulse > -250 && pulse < 250 ? 0 : pulse);
57
58
    /* Madagascar 1 */
    motors[0]->set_pulse = motor1_set_pulse;
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
static void motor1_set_pulse(EncoderMotorObjectTypeDef *self, int speed)
{
    if(speed > 0) {
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 0);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, speed);
    } else if(speed < 0) {
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, 0);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, -speed);
    } else {
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 0);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, 0);
    }
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
}

Combined with the STM32CubeMX configuration, as shown in the figure below, the TIM1 clock frequency is 84MHz, the prescaler value is 839, and the counting period is 999. In each PWM cycle, the timer counter counts from 0 to 999. The “pulse” represents the PWM duty cycle. When the counter reaches the duty cycle value, the high and low voltage levels are inverted, and the counter resets when it reaches 999. “Pulse” is used to set the timer’s compare-match register, thereby controlling the PWM signal’s duty cycle.

According to the following formula, the interrupt period of Timer 1 becomes 0.01 seconds, which means an update interrupt is generated every 10 milliseconds: Formula: Timer overflow time = (prescaler value + 1) × (counting period + 1) / Timer clock frequency

The calculation process is as follows: Timer overflow time = (999+1) × (839+1) / 84MHz = 10 ms

Adjusting the PWM duty cycle enables the motor to adjust between maximum and minimum speeds. In .\Hiwonder\Portings\motors_param.h, the speed limit for the motor is set to 1.5 revolutions per second.

19
#define MOTOR_JGB520_RPS_LIMIT 1.5f

According to the interrupt function of Timer 7, it updates every 10 milliseconds, and the PWM value is used to control the motor speed.

504
505
506
507
508
509
510
511
512
513
514
515
516
517
void TIM7_IRQHandler(void)
{
  /* USER CODE BEGIN TIM7_IRQn 0 */
    extern EncoderMotorObjectTypeDef *motors[4];
    if(__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE) != RESET) {
        __HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE);
		encoder_update(motors[0], 0.01, __HAL_TIM_GET_COUNTER(&htim5));
		encoder_update(motors[1], 0.01, __HAL_TIM_GET_COUNTER(&htim2));
		encoder_update(motors[2], 0.01, __HAL_TIM_GET_COUNTER(&htim4));
		encoder_update(motors[3], 0.01, __HAL_TIM_GET_COUNTER(&htim3));
		for(int i = 0; i < 4; ++i) {
			encoder_motor_control(motors[i], 0.01);
		}
    }

Combined with the STM32CubeMX configuration, as shown in the figure below, TIM7 is connected to APB1, with a clock frequency of 84MHz, which means a 84 prescaler gives a frequency of 1MHz.

According to the following formula, the interrupt period of Timer 7 becomes 0.01 seconds, which means an update interrupt is generated every 10 milliseconds: Timer overflow time = (83+1)×(9999+1)/84MHz = 10 ms

⑤ Update Encoder Motor to Obtain Speed

Timer 7 generates an update interrupt every 10 ms, which calculates the motor speed and updates PID parameters. Its update interrupt function is as follows (in ..\Core\Src\stm32f4xx_it.c), where the encoder motor speed measurement of motor 1 obtained by Timer 5 is updated.

504
505
506
507
508
509
510
511
512
513
514
515
516
517
void TIM7_IRQHandler(void)
{
  /* USER CODE BEGIN TIM7_IRQn 0 */
    extern EncoderMotorObjectTypeDef *motors[4];
    if(__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE) != RESET) {
        __HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE);
		encoder_update(motors[0], 0.01, __HAL_TIM_GET_COUNTER(&htim5));
		encoder_update(motors[1], 0.01, __HAL_TIM_GET_COUNTER(&htim2));
		encoder_update(motors[2], 0.01, __HAL_TIM_GET_COUNTER(&htim4));
		encoder_update(motors[3], 0.01, __HAL_TIM_GET_COUNTER(&htim3));
		for(int i = 0; i < 4; ++i) {
			encoder_motor_control(motors[i], 0.01);
		}
    }

The encoder_update() function calculates and updates the motor speed based on the count obtained from the encoder. It calculates the motor speed based on the count of the encoder. overflow_num represents the number of overflows of Timer 5, which will be further detailed in the following content. delta_count represents the change in the encoder count within the timer period.

The TPS (Ticks Per Second) is calculated to determine the pulse frequency (i.e., the number of pulses per second). It is achieved by dividing the change in count by the time interval. Here, a filter is applied (with coefficients of 0.9 and 0.1) to smooth the result.

Finally, RPS (Revolutions Per Second) represents the motor’s rotational speed (in revolutions per second), and it is calculated as the TPS divided by the number of pulses generated by the motor in one revolution. The number of pulses generated by the motor in one revolution is described in point three below. The encoder_update program is located in ..\Hiwonder\Peripherals\encoder_motor.c.

Motor speed (rps) = pulses per second (tps) / number of pulses generated by the motor in one revolution (1320)

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * @brief Update of encoder motor speed measurement
 * @detials
 * @param self Encoder motor object pointer
 * @param period The time interval between the current update and the last update (update cycle), measured in sec
 * @param counter The current count value of the encoder
 * @retval None.
*/
void encoder_update(EncoderMotorObjectTypeDef *self, float period, int64_t counter)
{
    counter = counter + self->overflow_num * self->ticks_overflow; /* The total count value is 60,000 based on the actual timer overflow value set*/
    int delta_count = counter - self->counter;
    self->counter = counter; /* Store the new count value */
    self->tps = (float)delta_count / period * 0.9f + self->tps * 0.1f; /* Calculate the pulse frequency */
    self->rps = self->tps / self->ticks_per_circle; /* The unit for calculating rotational speed is rps, which is revolutions per second */
}

According to the motor manual, this motor generates 11 pulses per revolution of its shaft, and the gear ratio of the motor is 45:1. The timer is set to use a quadrature counting mode with a 4x frequency, meaning that for every rotation of the motor output shaft, the counter value changes by 11.0 * 4.0 * 30.0 = 1320. Therefore, whenever the motor’s output shaft completes one revolution, the encoder interface records an increase of 3960 in the counter value. This information can be found in ..\Hiwonder\Portings\motors_param.h

15
16
17
18
19
20
21
22
/* Each revolution of the motor shaft generates 11 pulses. The counter counts evenly at the rising and falling edges of phases A and B, meaning the count value is four times the number of pulses. The reduction ratio is 90.0.
 * That is, for each revolution of the motor output shaft, the counter value changes by 11.0 * 4.0 * 90.0 = 3960
 */
#define MOTOR_JGB520_TICKS_PER_CIRCLE 3960.0f
#define MOTOR_JGB520_PID_KP  63.0f
#define MOTOR_JGB520_PID_KI  2.6f
#define MOTOR_JGB520_PID_KD  2.4f
#define MOTOR_JGB520_RPS_LIMIT 1.5f

From the following diagram, we can see that the 4x frequency counting mode counts each pulse four times.

When the encoder count overflows, it enters the interrupt of Timer 5, where the overflow count changes. The number of underflow occurrences decreases, and the number of overflow occurrences increases. The program is located in ..\Core\Src\stm32f4xx_it.c

469
470
471
472
473
474
475
476
477
478
479
void TIM5_IRQHandler(void)
{
  /* USER CODE BEGIN TIM5_IRQn 0 */
    extern EncoderMotorObjectTypeDef *motors[4];
    if(__HAL_TIM_GET_FLAG(&htim5, TIM_FLAG_UPDATE) != RESET) {
        __HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);
        if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim5)) {
            --motors[0]->overflow_num;
        } else {
            ++motors[0]->overflow_num;
        }

(3) PID Stabilizing Target Speed Analysis

① Positional PID Algorithm

The PID controller update function, pid_controller_update(), implements positional PID control. The specific code implementation is shown in the following figure, located at ..\Hiwonder\Misc\pid.c.

15
16
17
18
19
20
21
22
23
24
25
26
void pid_controller_update(PID_ControllerTypeDef *self, float actual, float time_delta) {
	float err = self->set_point - actual;
	float proportion = err - self->previous_0_err;
	
	float integral = err * time_delta;
	float derivative = (err - 2 * self->previous_1_err + self->previous_0_err) / time_delta;
	
	self->output = (self->kp * err) + (self->ki * integral) + (self->kd * derivative);
	self->previous_1_err = self->previous_0_err;
	self->previous_0_err = err;

}

The "err" variable represents the error between the current target value and the actual value: it calculates the difference between the setpoint (self->set_point, desired speed) and the actual measured value (actual, current speed).

  • Error = Setpoint - Actual Value

The proportional (P) term, represented by "proportion," is the difference between the current error and the previous error. It is used to calculate the derivative.

  • Proportional = Error - Previous Error

The integral (I) term, represented by "integral," accumulates the error over time. It is used in conjunction with the derivative term to calculate the overall control signal. This term calculates the integral of the error over time, helping to eliminate long-term steady-state errors.

  • Integral = Error x Time Interval

The derivative (D) term, represented by "derivative," calculates the rate of change of the error. It predicts the future behavior of the system and quickly responds to changes in the error.

  • Derivative = (Error - 2 x Previous Previous Error + Previous Error) / Time Interval

Converting PID output to PWM value: Finally, the function stores the output in the PID_ControllerTypeDef as "output." After calling the pid_controller_update function, the output of the PID controller (self->output) can be used to adjust the PWM signal.