Have you ever tried to hold a DC motor at a constant speed using only a fixed PWM signal? It works fine at first, until you touch the shaft, change the load, or let the motor heat up. Suddenly the speed drifts, and there is nothing in the system to bring it back.
That is exactly the problem closed-loop control solves. And here, we will show you how to build a compact and practical motor speed controller from scratch.
The system combines three main building blocks:
- The XENSIV™ TLE5012B Angle Sensor 2GO kit measures the absolute shaft angle using a small diametrically magnetized magnet mounted on the motor shaft. From the continuously updated angle, the on-board MCU computes the actual motor speed in RPM and streams it to the controller.
- The XMC4700 Relax Kit acts as the main controller. It receives the speed measurement over UART, runs a PID algorithm, and outputs the motor command.
- The DC-SHIELD_BTN9970LV sits directly on the XMC4700 Arduino headers and drives the 12 V brushed DC motor.
The result is a speed controller that actively reacts to disturbances and holds the motor at the desired RPM, even when an external load is applied to the shaft.
To demonstrate the controller in action, we will apply a mechanical brake to the shaft while the system is running and observe how the PID controller responds and recovers.
Let's get started!
Closed‑Loop Speed Control & PID (Quick Background)Before wiring anything up, let’s quickly look at what we are controlling.
Closed‑loop speed control in one sentence
We continuously measure the motor speed, compare it to a target value, and adjust the motor drive signal until both match.
In our case the loop looks like this:
- Measure the shaft angle with the TLE5012B
- Compute RPM from angle change over time
- Run the controller and update the motor command
- Repeat at a fixed rate
On the XMC4700 we run the control loop at a fixed rate of 50 Hz (every 20 ms). That makes the controller behavior predictable and repeatable.
The control error
The controller always works with the error:
- If the motor is too slow, the error is positive → we need more drive.
- If the motor is too fast, the error is negative → we need less drive.
What P, I and D do (practical view)
P = Proportional
- Reacts immediately to an error
- Bigger error → stronger correction
- If Kp is too small, the motor reacts slowly
- If Kp is too large, the motor can overshoot and start to oscillate around the setpoint
I = Integral
- Removes the last remaining offset (for example when friction or load causes the motor to stay slightly below the setpoint)
- If Ki is too small, you often end up with a steady error (e.g. always 1–2 RPM off)
- If Ki is too large, the controller can “push too far” and cause slow back-and-forth movement around the setpoint
D = Derivative
- Adds damping: it helps prevent overshoot and makes the controller “calmer”
- If Kd is too large, small measurement noise can lead to visible fluctuations in the output
For DC motor speed control, PI already works well in many cases. We still include D here because it is a great tuning knob when you want a crisp response without overshoot, as long as the RPM signal is reasonably clean.
Feedforward
In addition to PID, we use a simple feedforward term:
This is basically a “baseline drive value” that roughly matches how much PWM the motor needs for a given target RPM.The PID controller then only has to correct the difference.
Why it helps:
- Faster reaction to setpoint changes
- Less work for the integral term
- Often less overshoot
To make the setup easy to reproduce (and to keep the magnet alignment consistent), we built a small test fixture: a simple holder where the motor and the TLE5012B sensor board sit directly opposite to each other. The motor shaft faces the sensor, and a small wheel on the shaft holds the magnet.
The wheel has two jobs:
- It keeps the diametrically magnetized magnet fixed and centered.
- It works as a simple hand brake so we can apply load and watch the controller react.
With the mechanical alignment solved, the electrical wiring becomes straightforward.
Mechanical assembly (motor, wheel, magnet, sensor)
- Insert the motor into the fixture so the shaft points toward the sensor position. (There are holes to fix it with screws, but that's not necessary)
- Insert the diametric magnet into the wheel.
- Mount the wheel onto the motor shaft.
- Place the TLE5012B Angle Sensor 2GO in the fixture so the sensor sits centered behind the magnet.
- Rotate the shaft by hand and confirm that the wheel runs freely and does not touch the sensor board.
If you later notice unstable RPM readings, the most common reason is still mechanical: magnet not centered, wrong distance, or wrong magnet type (must be diametrically magnetized).
Motor + power wiring (DC-SHIELD_BTN9970LV)
Stack the motor control shield onto the XMC4700 Relax Kit (Arduino headers).
Now connect the motor to the output terminals of the shield:
- Motor wire A → OUT1
- Motor wire B → OUT2
If the motor spins in the wrong direction later, simply swap the two motor wires. For speed control the direction itself does not matter, as long as your sign convention in software matches.
Power supply:
- VBAT → +12 V
- GND → 0 V (GND)
Make sure your 12 V supply can deliver enough current for startup and load changes.
Sensor-to-controller wiring (UART)
The Angle Sensor 2GO board reads the TLE5012B internally and streams the speed data to the XMC4700 via UART.
Connect:
TLE5012B Angle Sensor 2GO → XMC4700
- 2GO UART:TX (Pin 6 / P2.0) → XMC4700 RX (Pin 0 / P2.15)
- 2GO GND → XMC4700 GND
That’s all that’s required for the data link.
Powering during development
For development we recommend powering both boards via USB:
- XMC4700 via USB (flash + Serial Monitor)
- Angle Sensor 2GO via USB (flash + debugging)
This avoids confusion during bring-up. Once everything works, the sensor board can also be powered from the XMC4700 if you want to reduce cables.
Quick hardware check (before PID)
Before moving on, it helps to do two quick checks:
- Motor driver check (open loop): Upload a minimal test that sets a fixed speed (e.g. motor.set_speed(100)) and confirm the motor spins.
#include "btn99x0_motor_control.hpp"
using namespace btn99x0;
DCShield shield;
MotorControl motor(shield);
void setup() {
Serial.begin(115200);
motor.begin();
motor.set_speed(100);
Serial.println("Motor running at fixed speed 100");
}
void loop() {
}- Sensor data check: Run the UART receiver on the XMC4700 and confirm you see rpm_raw=... changing when the motor speed changes or when you apply brake load.
If both checks work, the full PID demo will work as well.
Software & CodeIn this project we use two microcontrollers that each run their own sketch:
- The Angle Sensor 2GO (XMC1100) acts as a sensor frontend. It reads the shaft angle from the TLE5012B, computes the motor speed in RPM, and streams the result to the XMC4700 via UART.
- The XMC4700 Relax Kit receives the RPM data, runs the PID controller, and drives the motor through the DC-SHIELD_BTN9970LV.
This split keeps the setup clean and easy to reproduce: the sensor board handles everything related to angle measurement, while the XMC4700 focuses entirely on control.
Setting Up the Arduino IDE
We developed both sketches using the Arduino IDE.
Step 1: Install board support
- Open Arduino IDE → Boards Manager
- Search for Infineon XMC Boards and install it
- For each sketch, select the correct board:
- KIT_XMC47_RELAX for the PID controller
- KIT_XMC_2GO_XMC1100_V1 for the sensor frontend
Step 2: Install required libraries
Open Sketch → Include Library → Manage Libraries and install:
- XENSIV Angle Sensor TLx5012B
- motix-btn99x0
Sender Sketch : Sensor Frontend (Angle Sensor 2GO / XMC1100)
This sketch runs on the Angle Sensor 2GO board. It reads the absolute shaft angle from the TLE5012B, computes motor speed from the angle difference over time, applies a small low-pass filter, and sends the result to the XMC4700 over UART at about 100 Hz.
Important: In the Arduino IDE, set Tools → Serial Output Selection → On Board, before uploading. This routes Serial to the UART pins (P2.0 / TX) instead of the USB debugger.
How it works
The sketch reads a fresh angle value from the TLE5012B on every loop iteration. The RPM is computed from the change in angle over time, with 0°/360° wraparound handled correctly. The result is filtered with a small low-pass filter to reduce noise, then packed into a compact binary frame and sent over UART.
UART frame format
To make the data transfer reliable, we use a simple frame with two fixed start bytes and a CRC8 checksum at the end:
[0xAA][0xBB][angle_hi][angle_lo][rpm_hi][rpm_lo][status][crc8]- 0xAA, 0xBB: start bytes for synchronization
- angle: absolute shaft angle encoded as a 16-bit value scaled from 0–360° to 0–65535
- rpm: filtered motor speed in 0.1 rpm steps (signed 16-bit)
- status: rotation direction (0x01 = CW, 0x02 = CCW)
- crc8: checksum over the first four payload bytes
Receiver Sketch: PID Speed Controller (XMC4700)
This sketch runs on the XMC4700. It receives the RPM value from the sensor board, runs the PID controller at 50 Hz, and sends the resulting drive command to the motor shield.
UART receiver
The receiver uses a simple state machine to parse the incoming frames. It waits for the two start bytes, then reads exactly six payload bytes and verifies the CRC before updating the measured RPM value. Frames with a CRC error are silently discarded, which makes the communication robust against occasional noise on the line. The received RPM is then smoothed once more with a small low-pass filter before it enters the PID loop.
Low-pass filter
Before the RPM value reaches the controller, a small low-pass filter smooths out any remaining measurement noise:
rpm_filtered = 0.8f * rpm_filtered + 0.2f * rpm_measured;PID controller
The controller runs at a fixed rate of 50 Hz (every 20 ms). The output is computed as:
u = u_ff + P + I + DWhere:
- Feedforward (u_ff = Kff × setpoint) provides a baseline drive level so the PID only has to correct small deviations. A good starting point for Kff is 255 /rpm_no_load from the motor datasheet. Fine-tune it until the I-term stays close to zero at steady state.
- P reacts immediately to the current error
- I removes any remaining steady state offset (clamped to prevent windup)
- D is applied to the measured speed rather than the error, which avoids a sudden output spike when the setpoint changes
Motor output
The final output is clamped to the valid PWM range and passed directly to the motor shield:
motor.set_speed((int)lroundf(pid_output));Serial Plotter
Instead of just printing numbers, we use the Serial Plotter in the Arduino IDE to visualize what the controller is doing in real time. This makes it much easier to see how the system reacts when we change the load on the motor shaft.
The sketch outputs three values at 10 Hz:
RPM, Drive Signal, Setpoint
- RPM is the filtered, measured motor speed
- Setpoint is the constant target speed we want the motor to hold
- DriveSignal represents the motor drive signal (derived from the PID output)
A quick note on the drive signal value: since RPM and PWM live on very different scales, we scale the drive signal up for the plot so both curves are visible together. This means the absolute number shown does not correspond to an actual measurement, it's not meant to be read as "the motor currently gets X volts etc". Instead, it's there purely to show the trend: when we increase the load on the shaft, you can watch this value rise as the controller pushes more drive to the motor to keep the RPM at the setpoint, and it drops back down again once the load is removed.
A Note on PID Parameters
The gains used in this project (Kp = 0.06, Ki = 0.02, Kd = 0.002, Kff = 0.2098) are tuned for the specific motor used here. If you use a different motor, you will likely need to adjust them. The tuning guide in the next step explains how to do that systematically.
The Demo: Dynamic Load Disturbance- Start the system and let the motor stabilize at a constant speed (e.g. 500 RPM, depending on your motor).
- Apply a small, controlled friction load to the wheel using a soft material such as felt or rubber. Do not use your finger as a brake, especially at higher speeds, since the wheel can become warm and the contact force is harder to control.
- Observe the Serial Plotter output. You should see:
- The measured RPM drop as you apply pressure.
- The controller output rises to compensate for the added load.
- Once the new equilibrium is reached, the RPM stays at 500 RPM.
- Reduce the friction. The load disappears, and the controller quickly reduces the output to prevent the motor from accelerating too much.
This simple test clearly demonstrates why closed-loop control is useful: an open-loop system would simply stall or slow down significantly under load, whereas our PID controller actively fights the disturbance to maintain the desired speed.
Here is the complete setup in action. The motor runs at a steady speed, we apply a small mechanical load, and the controller compensates automatically to keep the speed near the target value.
How to Tune Your PID Parameters
Finding the right values for Kp, Ki, and Kd can feel like trial and error. Here is a practical step by step approach that works well for DC motor speed control:
1. Start with P only (Ki = 0, Kd = 0)
Set Kp to a small value (e.g., 2.0).
Increase Kp slowly until the motor responds quickly to load changes.
Stop as soon as you see the RPM start to oscillate around the setpoint.
Reduce Kp slightly (e.g., by 10 to 20%) to remove the oscillation.
Result: The motor reacts fast but may still have a small steady state error (e.g., settling at 580 RPM instead of 600).
2. Add Integral action (Ki)
Set Ki to a very small value (e.g., 0.1).
Increase Ki slowly until the steady state error disappears and the motor reaches the exact setpoint.
If Ki is too high, the system will start to slowly oscillate back and forth. If this happens, reduce Ki.
Result: The motor now holds the exact setpoint even under constant load.
3. Add Derivative action (Kd)
If the motor still overshoots slightly when the load changes suddenly, you can add a small Kd (e.g., 0.1 to 0.5).
Kd adds damping and helps prevent overshoot.
If Kd is too high, the motor output will become noisy because Kd amplifies measurement noise. For many DC motors, PI control (Steps 1+2) is already sufficient.
Tip: If you are using a feedforward term (Kff), tune Kff first to get the baseline drive value correct. This makes tuning Kp and Ki much easier because the PID loop only has to correct small errors.
Alternative: The Ziegler Nichols Method
If the manual approach above feels a bit too trial and error for your taste, there is a more systematic method: Ziegler Nichols.
This classic tuning method gives you a quick way to calculate Kp, Ki, and Kd from just two measured values, instead of tuning each parameter by feel.
Step 1: Find the critical gain
Set Ki = 0 and Kd = 0. Increase Kp until the system starts to oscillate continuously with constant amplitude (neither growing nor shrinking). This value is called the critical gain Ku. Then measure the oscillation period Tu, which is the time between two peaks.
Step 2: Calculate the parameters
Depending on which controller type you want, use one of the following rules:
Ziegler Nichols tends to produce aggressive gains and often results in some overshoot. It is a good starting point to get into the right range quickly, but for a smooth motor controller like ours, we still recommend fine tuning the values afterward using the manual method described above, especially reducing Kp slightly and adjusting Ki and Kd to taste.
Also keep in mind that pushing the motor into sustained oscillation to find Ku puts some mechanical stress on the setup. If you are testing on delicate hardware, the manual tuning approach is the gentler option.
FarewellThat covers the complete build of a closed-loop DC motor speed controller using the XMC4700, the XENSIV™ TLE5012B magnetic angle sensor, and the DC-SHIELD_BTN9970LV.
We started with a simple open-loop idea, added magnetic sensing to measure the shaft angle, computed the RPM in real time, and finally closed the loop with a PID controller that actively compensates for load disturbances.
If you want to take this project further, there are a couple of natural next steps: since the TLE5012B provides absolute angle information, you can extend the same setup from pure speed control to position control (move to a target angle and hold it). On the power stage side, the motor control shield also opens the door to adding current monitoring and protection, which is especially useful if you plan to work with higher loads or more aggressive braking.
We had a blast building this, and we’d love to see what you come up with! If you build your own version or run into questions along the way, feel free to leave a comment below.
Happy building!











Comments