This project implements a basic PID controller on an STM32 microcontroller and provides a Python-based desktop GUI for real-time monitoring, parameter tuning, and response analysis.
The STM32 side reads an analog feedback signal through ADC, calculates the PID output, writes the control output to DAC, and sends live telemetry data to the PC over UART. The Python GUI communicates with the STM32 board through a serial connection, allows the user to update PID parameters, plots the system response in real time, and calculates common control response metrics such as rise time, settling time, overshoot, and steady-state error.
- STM32-based PID controller implementation
- Adjustable
Set Point,Kp,Ki, andKdvalues from the GUI - UART communication between STM32 and Python
- Real-time plotting of:
- Measured voltage
- Set point
- PID output
- DAC output generation based on PID output percentage
- ADC-based feedback reading
- Response metric calculation:
- Rise time
- Settling time
- Overshoot
- Steady-state error
- START / STOP control from the GUI
- Dynamic graph scaling
- Serial port selection from the GUI
The system is built around a closed-loop PID control structure.
+----------------+ UART Commands +----------------+
| | --------------------------> | |
| Python GUI | | STM32 |
| | <-------------------------- | |
+----------------+ UART Telemetry +----------------+
|
| DAC Output
v
Controlled System
|
| Analog Feedback
v
ADC Input
The STM32 continuously performs the following operations:
- Reads the feedback voltage from ADC.
- Calculates the control error:
error = setPoint - actualValue
- Computes the PID output.
- Limits the output between the configured minimum and maximum output values.
- Converts the PID output percentage into a 12-bit DAC value.
- Sends setpoint, voltage, and PID output values to the Python GUI over UART.
The GUI receives this data, parses it, plots the values, and calculates response metrics.
Recommended repository structure:
STM32-PID-Controller/
├── STM32/
│ ├── Core/
│ │ ├── Inc/
│ │ │ └── pid.h
│ │ └── Src/
│ │ ├── pid.c
│ │ └── main.c
│ └── STM32CubeIDE project files
│
├── GUI/
│ └── pid_controller_gui.py
│
├── README.md
└── LICENSE
If this repository is intended to focus only on the PID library and GUI logic, the structure can be simplified:
STM32-PID-Controller/
├── pid.h
├── pid.c
├── main.c
├── pid_controller_gui.py
├── README.md
└── LICENSE
- STM32 development board with:
- ADC input
- DAC output
- UART interface
- PC with USB connection to STM32
- Analog feedback signal source
- Optional controlled plant or test circuit
- Jumper wires
- Breadboard or external circuit, depending on the application
This project was written for an STM32 HAL-based environment. The code structure is suitable for STM32CubeIDE projects.
The current firmware uses the following peripherals:
| Peripheral | Purpose |
|---|---|
| ADC1 | Reads the feedback voltage |
| DAC Channel 1 | Outputs the PID control signal |
| USART2 | Communicates with the Python GUI |
| GPIO | Board LED/button configuration generated by CubeMX |
| SysTick / HAL_GetTick | Provides timing for the 100 ms control loop |
The exact pins may vary depending on the STM32 board and CubeMX configuration. For an STM32 Nucleo-F446RE-style setup, the typical configuration is:
| Function | STM32 Peripheral | Typical Pin | Description |
|---|---|---|---|
| Feedback Input | ADC1_IN0 | PA0 | Analog feedback voltage input |
| Control Output | DAC_OUT1 | PA4 | Analog PID output |
| UART TX | USART2_TX | PA2 | STM32 transmit line to PC / ST-LINK VCP |
| UART RX | USART2_RX | PA3 | STM32 receive line from PC / ST-LINK VCP |
| User LED | GPIO Output | Board-dependent | Optional status LED |
| User Button | GPIO Input Pull-up | Board-dependent | Optional button input |
For Nucleo boards, USART2 is commonly routed through the onboard ST-LINK Virtual COM Port. In that case, no external USB-to-UART converter is needed.
- ADC input voltage must stay within the valid MCU ADC input range.
- For a 3.3 V STM32 board, do not apply more than 3.3 V to the ADC pin.
- DAC output range is typically 0 V to VDDA.
- If the DAC output controls an external circuit, make sure the external circuit input impedance and voltage requirements are compatible with the STM32 DAC output.
- If an external UART adapter is used, connect GND between STM32 and the adapter.
The ADC is configured as:
| Setting | Value |
|---|---|
| ADC Instance | ADC1 |
| Channel | ADC_CHANNEL_0 |
| Resolution | 12-bit |
| Conversion Mode | Continuous conversion enabled |
| Trigger | Software start |
| Data Alignment | Right aligned |
| Sampling Time | 480 cycles |
The ADC value is converted to voltage using:
#define VSTEP 3.3/4096
voltage = (float)(adcValue * VSTEP);For a 12-bit ADC, the raw ADC range is 0-4095. With a 3.3 V reference, the approximate voltage step is:
3.3 / 4096 ≈ 0.000805 V
The DAC is configured as:
| Setting | Value |
|---|---|
| DAC Instance | DAC |
| DAC Channel | DAC_CHANNEL_1 |
| Trigger | None |
| Output Buffer | Enabled |
| Alignment | 12-bit right aligned |
The PID output is treated as a percentage value between 0.0 and 100.0. It is mapped to the DAC range using:
return (uint16_t)((percent / 100.0f) * 4095.0f + 0.5f);So:
| PID Output | DAC Value | Approximate Output |
|---|---|---|
| 0% | 0 | 0 V |
| 50% | ~2048 | ~1.65 V |
| 100% | 4095 | ~3.3 V |
The UART is configured as:
| Setting | Value |
|---|---|
| UART Instance | USART2 |
| Baudrate | 115200 |
| Word Length | 8 bits |
| Stop Bits | 1 |
| Parity | None |
| Hardware Flow Control | None |
| Oversampling | 16 |
The STM32 sends telemetry data to the Python GUI using this format:
SetPoint: 2.500 Voltage: 2.4812 PIDOutput: 75.400
The actual firmware format is:
"SetPoint: %.3f Voltage: %.4f PIDOutput: %.3f\r\n"The GUI parses this format using a regular expression, so small variations such as : or = can also be supported.
The GUI sends text-based commands to the STM32.
| Command | Example | Description |
|---|---|---|
SETPOINT: |
SETPOINT:2.5 |
Sets the PID setpoint |
SET_SP: |
SET_SP:2.5 |
Alternative setpoint command |
KP: |
KP:20.0 |
Sets proportional gain |
SET_KP: |
SET_KP:20.0 |
Alternative Kp command |
KI: |
KI:15.0 |
Sets integral gain |
SET_KI: |
SET_KI:15.0 |
Alternative Ki command |
KD: |
KD:0.2 |
Sets derivative gain |
SET_KD: |
SET_KD:0.2 |
Alternative Kd command |
START |
START |
Enables PID calculation |
STOP |
STOP |
Disables PID calculation |
When the GUI START button is pressed, it sends all PID parameters first and then sends the START command.
The PID controller is implemented in pid.c and pid.h.
The PID controller stores:
- Proportional gain:
Kp - Integral gain:
Ki - Derivative gain:
Kd - Setpoint
- Integral accumulator
- Previous error
- Output value
- Minimum output limit
- Maximum output limit
- Sampling time
The controller calculates:
error = setPoint - actualValue
Proportional term:
P = Kp * error
Integral accumulation:
integral_sum += error * sampleTime
Integral term:
I = Ki * integral_sum
Derivative term:
D = Kd * (error - previousError) / sampleTime
Final output:
output = P + I + D
The output is limited between minOutput and maxOutput.
The default firmware initializes the PID controller as:
PID_Init(&pid, 20.0f, 15.0f, 0.2f, 0.1f, 0.0f, 100.0f);
PID_SetPoint(&pid, 2.5f);| Parameter | Default Value |
|---|---|
| Kp | 20.0 |
| Ki | 15.0 |
| Kd | 0.2 |
| Sample Time | 0.1 s |
| Minimum Output | 0.0 |
| Maximum Output | 100.0 |
| Initial Setpoint | 2.5 |
The main control loop executes every 100 ms:
if (currentTime - lastTime >= 100) {
...
}This means the effective PID sample time is:
100 ms = 0.1 s
The PID initialization also uses 0.1f as the sample time. This should remain consistent unless the control loop period is changed.
The GUI is implemented with:
tkinterfor the interfacepyserialfor UART communicationmatplotlibfor real-time plottingdequebuffers for efficient live graph data storagethreadingandqueuefor safe serial reading
The GUI provides:
- COM port selection
- Baudrate selection
- Stop bit selection
- Parity selection
- Connect / Disconnect buttons
- Setpoint input
- Kp input
- Ki input
- Kd input
- START / STOP buttons
- Clear Graph button
- Real-time PID response graph
- Response metric display panel
Install the required Python packages:
pip install pyserial matplotlib opentelemetry-apitkinter is usually included with standard Python installations. If it is missing on Linux, install it using your package manager. For example:
sudo apt install python3-tkThe current GUI code imports:
from opentelemetry import metricsIf this import remains in the code, opentelemetry-api must be installed. If the import is removed later, this package is no longer required.
- Open the STM32 project in STM32CubeIDE.
- Verify the peripheral configuration:
- ADC1 enabled on the selected feedback input pin.
- DAC channel 1 enabled.
- USART2 configured at 115200 baud, 8N1.
- Build the project.
- Flash the firmware to the STM32 board.
- Open the Python GUI on the PC.
- Select the correct COM port.
- Click
Connect. - Enter the desired PID parameters.
- Click
START.
Navigate to the GUI folder and run:
python pid_controller_gui.pyThe GUI window should open with:
- Connection settings on the left side
- PID parameter inputs below the connection section
- START / STOP buttons
- Response metrics panel
- Real-time graph on the right side
Select the correct COM port and use these default serial settings:
| Setting | Value |
|---|---|
| Baudrate | 115200 |
| Stop Bits | One |
| Parity | None |
Click Connect. The status label should change from DISCONNECTED to CONNECTED.
The GUI includes input fields for:
- Set Point
- Kp
- Ki
- Kd
You can update each parameter individually using the corresponding SET button.
When START is clicked, the GUI sends all parameter values to STM32 before starting the PID controller.
Click START.
Expected behavior:
- The graph is cleared.
- Plotting is enabled.
- STM32 receives the latest setpoint and PID gain values.
- STM32 enables PID calculation.
- Voltage, setpoint and PID output values begin appearing on the graph.
Click STOP.
Expected behavior:
- GUI plotting stops.
- STM32 receives the
STOPcommand. - PID output is forced to zero on the STM32 side.
Click CLEAR GRAPH to remove existing graph data and reset response metrics.
During operation, the graph should show three curves:
| Curve | Meaning |
|---|---|
| Voltage | Measured ADC feedback voltage |
| Set Point | Desired target value |
| PID Output | PID output percentage |
The voltage curve should move toward the setpoint. The PID output curve should change according to the controller response.
A typical stable response may look like this:
Voltage starts below the setpoint
Voltage rises toward the setpoint
PID output changes rapidly at first
PID output stabilizes as voltage approaches the target
Steady-state error becomes small
The response metrics panel should display values such as:
Rise Time: 5.90 s
Settling Time: 11.90 s
Overshoot: 0.00 % (0.000 V)
Steady State Error: 0.0036 V
The exact values depend on the physical system, PID gains, setpoint, ADC noise, and DAC output behavior.
Example Output:
Rise time is calculated as the time required for the voltage response to move from 10% to 90% of the step amplitude.
rise time = t90 - t10
If the system starts very close to the setpoint, rise time may not be meaningful.
Settling time is the first time at which the voltage enters the settling band and stays there.
The GUI uses a tolerance based on approximately ±2% of the setpoint, with a minimum threshold to avoid overly small bands.
Overshoot indicates how much the voltage exceeds the setpoint.
For an upward step:
overhoot = peak voltage - setpoint
If the voltage never exceeds the setpoint, overshoot is shown as 0.00% after the response settles.
Steady-state error is calculated from the average of recent voltage samples:
steady-state error = |setpoint - average recent voltage|
This value is updated continuously while data is being received.
The STM32 sends telemetry in this format:
SetPoint: 2.500 Voltage: 2.4812 PIDOutput: 75.400
The GUI extracts the following fields:
SetPointVoltagePIDOutput
The parser accepts both : and = separators and normalizes field names by removing spaces, underscores and hyphens.
Supported examples:
SetPoint:3.00 Voltage:2.85 PIDOutput:1.50
SetPoint: 3.00 Voltage: 2.85
Setpoint:3.00, Voltage:2.85
SETPOINT=3.00 VOLTAGE=2.85
A simple test setup can be created by feeding the DAC output into an external circuit and reading the resulting feedback voltage with the ADC.
For a basic electrical loop test:
STM32 DAC_OUT1 ---> External circuit / filter / plant input
External output ---> STM32 ADC1_IN0
GND ---> Common ground
For simple debugging only, a direct DAC-to-ADC connection may be used carefully, but this does not represent a real physical plant. A real control system should include a plant or circuit whose behavior can be controlled and measured.
Check:
- Correct COM port selected
- Baudrate is 115200
- STM32 firmware is running
- USART2 is correctly configured
- ST-LINK Virtual COM Port driver is installed
STARTbutton has been pressed
Check whether the UART format includes at least:
SetPoint
Voltage
The GUI requires both fields to plot data.
Possible causes:
- ADC noise
- Very narrow y-axis scaling
- Low UART numeric precision
- Unfiltered feedback signal
- Unstable PID gains
The GUI uses dynamic y-axis scaling, so small ADC variations may look visually large when the voltage is already close to the setpoint.
Check:
STARTcommand was sentpidEnabledis set to1- Setpoint differs from actual voltage
- PID gains are not all zero
- ADC value changes correctly
The value displayed in the GUI entry field is not automatically active on the STM32 until it is sent. Press the corresponding SET button or press START, which sends all parameters before enabling PID control.
Possible future improvements:
- Add PID anti-windup based on output saturation behavior
- Add derivative filtering
- Add ADC moving average or low-pass filtering
- Add CSV logging from the GUI
- Add automatic COM port detection
- Add selectable graph time window
- Add save/load PID parameter presets
- Add serial command acknowledgment from STM32
- Add error messages for invalid UART commands
- Add a real-time numeric dashboard for voltage and PID output
- Do not apply voltages higher than the STM32 ADC reference voltage to ADC pins.
- Always connect common ground when using external circuits.
- DAC output current capability is limited; do not drive heavy loads directly from the DAC pin.
- Use buffer circuits or drivers when controlling motors, heaters, valves, or other power devices.
- Test PID parameters carefully before connecting the output to a real actuator.
This project is licensed under the MIT License. See the LICENSE file for details.