Applications of Mechatronic Systems
🎯 (SE-11-03)
📌 Outline applications of mechatronic systems in a variety of specialised fields
Mechatronics combines mechanical engineering, electronics and computing so that a machine can sense its surroundings, decide what to do and act on the decision. Every application below follows the same loop: sensors measure something, a controller running a program decides, and actuators change the physical world. Only the sensors, the actuators and the rules differ from field to field.
A short overview of how mechanical, electrical, and software engineering combine to create intelligent mechatronic systems.
Sense, decide, act in five fields
| Field | Mechatronic system | What it senses → what it does |
|---|---|---|
| Agriculture | Automated irrigation | Soil moisture → switches a pump on and off |
| Manufacturing | Pick-and-place robot arm | Camera finds a part → gripper picks it up and places it |
| Home | Robot vacuum | Bump and distance sensors → steers around furniture |
| Transport | Automatic train doors | Platform position and obstruction sensors → opens or holds the doors |
| Healthcare | Insulin pump with a glucose sensor | Sensor under the skin reports glucose level → pump adjusts the insulin it delivers |
Why use a mechatronic system, and what does it cost?
- Benefits: consistent repeatable results, work in places that are dangerous or remote, continuous operation, and fine control that a person cannot match (a drone corrects its balance many times a second).
- Trade-offs: higher up-front cost, specialist skills to design and repair, new failure modes (a sensor fault or software bug can make the machine act wrongly), safety risks where machines share space with people, and changes to the jobs people do.
Hardware Requirements and Code Development
🎯 (SE-11-01, SE-11-03)
📌 Identify the hardware requirements to run a program and the effect on code development
- assessing the relationship of microcontrollers and the central processing unit (CPU)
- the influence of instruction set and opcodes
- the use of address and data registers
Microcontrollers and the CPU
The central processing unit (CPU) is the part of a computer that fetches and executes instructions. A microcontroller is a whole small computer on one chip: a CPU core plus flash memory (holds the program), RAM (holds variables while running) and peripherals such as GPIO pins, analog-to-digital converters (ADCs) and PWM outputs. A microprocessor is mainly the CPU, so a computer built around one needs separate RAM, storage and input/output chips. Single-board computers such as the Raspberry Pi put a microprocessor, memory and ports on one board and run a full operating system. The CPU is therefore a part of a microcontroller, and the microcontroller is what most mechatronic systems are built around.
| Microcontroller (Arduino Uno class, Raspberry Pi Pico class) | Microprocessor board (Raspberry Pi class) | |
|---|---|---|
| On the chip | CPU, flash, RAM and peripherals together | CPU cores; RAM and storage (an SD card) are separate |
| Typical scale | Uno class: 8-bit chip, 2 KB of RAM, 16 MHz. Pico class: 32-bit dual-core Arm chip, 264 KB of RAM | 64-bit multi-core Arm chip, gigabytes of RAM, clock in the GHz range |
| Operating system | None, or a small runtime; your program is all that runs | A full OS such as Linux runs many programs at once |
| Timing | Predictable: you can switch a pin within microseconds | The OS can pause your program, so timing varies |
| Power | Low; can sleep on a small battery | Higher; needs a proper supply and a clean shutdown |
| Best for | Motor control, sensors, simple loops | Cameras, image processing, web servers, machine learning |
Many systems use both: a microprocessor for vision and planning, and a microcontroller for the fast, safety-critical motor loop.
Effect on code development
- Memory limits data choices. With 2 KB of RAM you cannot store a long list of readings or load a large library.
- Word size affects arithmetic. On an Uno an
intis 16 bits, so it overflows past 32 767; counting milliseconds needs a larger type. The 32-bit Pico does not have this problem at that scale. - Small chips may lack floating-point hardware, so decimal maths is done in software and is slow; integers are faster. Code is also built for one instruction set: a program compiled for AVR will not run on Arm.
- Language trades speed for convenience. Compiled C++ is fast and compact; MicroPython is quicker to write but needs more memory and runs slower.
- Voltage and pins matter. A Pico's pins are 3.3 V and must not be driven at 5 V; an Uno's pins are 5 V. The number of ADC and PWM pins limits how many sensors and motors you can connect.
Instruction set and opcodes
A CPU repeats the fetch, decode, execute cycle: fetch the next instruction from memory, decode which operation it asks for, then execute it. The instruction set is the complete list of operations a particular CPU understands. Each machine instruction holds an opcode (which operation) and its operands (which registers, addresses or values to use). A compiler or interpreter translates your Python or C++ into these instructions, so the instruction set decides what is fast, what is slow and what code can run at all. AVR and Arm are two instruction set families; RISC-V is a third.
The example below uses an invented 16-bit teaching instruction set (not a real chip) to show how an opcode and operands share one instruction word.
| Assembly | Opcode | Meaning | Instruction word |
|---|---|---|---|
LOAD R1, [0x20] | 0001 | Copy the value at address 0x20 into register R1 | 0x1120 |
LOAD R2, [0x21] | 0001 | Copy the value at address 0x21 into R2 | 0x1221 |
ADD R1, R2 | 0010 | R1 ← R1 + R2 | 0x2120 |
STORE [0x22], R1 | 0011 | Copy R1 into address 0x22 | 0x3122 |
Address and data registers
Registers are the CPU's own tiny, very fast storage. Data registers hold the values being worked on, such as a sensor reading waiting to be compared. Address registers hold the address of a memory location to read or write (the memory address register, MAR). The program counter is a special register holding the address of the next instruction, and the instruction register holds the instruction being decoded. An address is the number that identifies one memory location; the address bus carries it and the data bus carries the value. A 16-bit address can identify 216 = 65 536 locations.
Trace of the four instructions above, with 7 stored at 0x20 and 5 at 0x21:
| Step | Program counter after fetch | Instruction executed | R1 | R2 | Memory 0x22 |
|---|---|---|---|---|---|
| Start | 0x00 | — | ? | ? | ? |
| 1 | 0x01 | LOAD R1, [0x20] | 7 | ? | ? |
| 2 | 0x02 | LOAD R2, [0x21] | 7 | 5 | ? |
| 3 | 0x03 | ADD R1, R2 | 12 | 5 | ? |
| 4 | 0x04 | STORE [0x22], R1 | 12 | 5 | 12 |
On a microcontroller, the peripherals are reached the same way: this is memory-mapped I/O. Reading a pin or setting a PWM duty cycle is a read or write of a special register at a fixed address, and Pin.value() in MicroPython hides that detail.
A beginner-friendly walkthrough of a microcontroller (the Arduino) — how to read inputs, drive outputs, and upload code to control hardware.
Sensors, Actuators and End Effectors
🎯 (SE-11-06)
📌 Identify and describe a range of sensors, actuators and end effectors/manipulators within existing mechatronic systems
- motion sensors
- light level sensors
- hydraulic actuators
- robotic grippers
A sensor converts a physical quantity (light, heat, movement) into an electrical signal the controller can read. An actuator converts an electrical signal, plus a source of power, into physical action. An end effector is the tool at the end of a robot arm that touches the work; the arm and joints that position it are the manipulator.
Sensors (Inputs)
Devices that convert physical properties into electrical signals.
- Motion: PIR (Passive Infrared) sensors detect the change in infrared radiation as a warm body moves across their field of view.
- Light: LDRs (Light Dependent Resistors) change resistance based on ambient light levels.
Actuators (Outputs)
Components that convert electrical energy into physical motion.
- Hydraulic: Utilise pressurised fluid to generate high force (e.g., in excavators).
- Electric: DC motors or Servos that provide rotational movement.
Manipulators
The parts of the system that physically interact with objects.
- Robotic Gripper: An end effector designed to grasp and release objects.
- Degrees of Freedom: The number of independent movements a robot can make (see the next sections).
Motion sensors
"Motion" covers several measurements. A PIR sensor gives a digital HIGH when it sees a warm body moving, but cannot say how far away it is. An ultrasonic sensor times the echo of a sound pulse to find distance. An accelerometer or gyroscope (often together in an inertial measurement unit, IMU) reports acceleration and rotation, so a drone can tell it is tilting. A rotary encoder counts pulses as a shaft turns, giving speed and position.
Light level sensors
An LDR's resistance falls as light increases. A microcontroller cannot read resistance directly, so the LDR is wired with a fixed resistor as a voltage divider and the resulting voltage is read by an ADC. A photodiode responds faster, and a digital lux sensor sends a calibrated reading over a serial bus.
| Sensor | Measures | Signal to the controller | Example use |
|---|---|---|---|
| PIR | Movement of a warm body | Digital (HIGH or LOW) | Automatic door, security light |
| Ultrasonic | Distance, from echo time | Digital pulse whose width is timed | Robot obstacle avoidance |
| IMU | Acceleration and rotation | Digital data over I2C or SPI | Drone or self-balancing robot |
| Rotary encoder | Shaft rotation | Digital pulses | Wheel speed, arm position |
| LDR | Light level | Analog voltage, read by an ADC | Automatic night light |
| Thermistor or TMP36 | Temperature | Analog voltage, read by an ADC | Thermostat, greenhouse vent |
from machine import Pin, PWM
import utime
sensor = Pin(14, Pin.IN) # PIR motion sensor: 1 = movement
servo = PWM(Pin(15), freq=50) # Actuator: a servo needs a 50 Hz signal
while True:
if sensor.value() == 1:
servo.duty_u16(5000) # about a 1.5 ms pulse: move to the centre position
else:
servo.duty_u16(2000) # about a 0.6 ms pulse: return to the home position
utime.sleep(0.1)
from machine import ADC, Pin
import utime
ldr = ADC(26) # LDR voltage divider on an ADC pin
lamp = Pin(15, Pin.OUT)
DARK_BELOW = 20000 # found by measuring: readings run 0 (dark) to 65535 (bright)
while True:
lamp.value(1 if ldr.read_u16() < DARK_BELOW else 0)
utime.sleep(0.5)
Hydraulic actuators
A hydraulic actuator uses an electric pump to push an incompressible liquid (oil) through valves into a cylinder, where it pushes a piston. Pressure acts equally on every surface, so force = pressure × piston area. Oil at 10 MPa (10 000 000 Pa) on a piston of area 0.005 m² gives 10 000 000 × 0.005 = 50 000 N, enough to lift several tonnes. That is why excavators and press brakes use hydraulics. The costs are a pump and hoses, oil that can leak, heat and noise, and slower, less precise control than an electric motor.
| Actuator | How it is driven | Strength | Limitation |
|---|---|---|---|
| DC motor | PWM for speed, an H-bridge for direction | Simple, cheap, continuous rotation | No position feedback unless an encoder is added |
| Servo | A pulse whose width sets the angle | Holds a position; feedback is built in | Limited range (often about 180°) |
| Stepper motor | One pulse per step through a driver | Precise positioning without a sensor | Can lose steps if overloaded; draws current while holding |
| Hydraulic cylinder | Pump and valves | Very high force | Leaks, heat, complex plumbing |
| Pneumatic cylinder | Compressed air and valves | Fast and clean | Air compresses, so less force and precision |
Robotic grippers
A robotic gripper is the most common end effector. A parallel-jaw gripper closes two flat fingers, an angular gripper pivots them, a vacuum gripper holds smooth flat objects with suction, and a soft gripper uses flexible fingers for fragile items such as fruit. Other end effectors include a welding torch, a paint nozzle or a 3D printer's extruder. A gripper usually has one degree of freedom (open or close), and the code limits the grip force so it does not crush the part.
Data in Mechatronic Systems
🎯 (SE-11-04)
📌 Use different types of data and understand how it is obtained and processed in a mechatronic system, including diagnostic data and data used for optimisation
Types of data
The word "data" covers several kinds of value, and each is obtained and processed differently.
| Type | Example | How it is obtained | How it is processed |
|---|---|---|---|
| Analog sensor data | Temperature as a voltage | Sampled by an ADC | Converted to a number, scaled to °C, filtered |
| Digital sensor data | PIR HIGH or LOW; a Boolean | Read from a pin, or from a serial bus such as I2C | Compared or counted; may need debouncing |
| Control data | Setpoint, gain, servo angle | Set by the designer or the user | Combined with sensor data to give an output |
| Diagnostic data | Motor current, battery voltage, error count | Sensors that watch the system itself | Checked against limits to raise an alarm |
| Optimisation data | Logged temperature and vent angle over a day | Recorded to a file or memory card | Analysed to tune gains and save energy |
Processing and diagnostic data
Mechatronic systems process raw analog signals (via ADCs) into digital data for decision-making. Diagnostic data involves monitoring internal states, such as motor temperature, battery voltage or current draw, to identify potential hardware failures before they occur. For example, a motor that suddenly draws much more current than usual may be stalled, so the program stops it before it overheats. Optimisation data is used to refine control algorithms, such as adjusting the duty cycle of a PWM signal to achieve maximum motor efficiency with minimal heat generation. Engineers use logged data to perform predictive maintenance, replacing components before a failure happens.
Analog-to-Digital Conversion (ADC)
Sensors output continuous analog voltages. Microcontrollers use Analog-to-Digital Converters to sample these voltages and convert them to digital numbers for processing. An ADC has a resolution (for example 10 or 12 bits), which sets how many levels it can represent: 2n levels for n bits.
ADC formula: count = (measured voltage ÷ reference voltage) × 2n, limited to a maximum of 2n − 1. Some textbooks use 2n − 1 as the multiplier, which changes the result by about one count.
A 10-bit ADC (1024 levels, counts 0 to 1023) with a 5 V reference measuring 2.5 V gives (2.5 ÷ 5) × 1024 = 512. One count is 5 V ÷ 1024 ≈ 4.9 mV, the smallest change the ADC can notice. Software then uses the count to make decisions, for example switching on a cooling fan when a temperature crosses a threshold.
Try it
Analog-to-digital converter explorer
See how the number of bits changes the count a program reads from a sensor. Enable JavaScript to use this tool. In short: an n-bit ADC has 2^n levels, so each step is the reference voltage divided by 2^n.
Worked example: from raw count to a decision
A TMP36 temperature sensor outputs 10 mV per °C with 0.5 V at 0 °C. A 10-bit ADC (5 V reference) reads a count of 158. The steps are: count → volts → °C → decision.
- Volts = 158 × 5 ÷ 1024 = 0.771 V.
- Temperature = (0.771 − 0.5) × 100 ≈ 27.1 °C.
- 27.1 °C is above the 26 °C setpoint, so the vent should open.
Because one count is about 4.9 mV, this sensor and ADC can only resolve steps of about 0.5 °C. Logging the readings gives optimisation data:
VREF = 5.0
STEPS = 1024 # 10-bit ADC
def count_to_celsius(count):
volts = count * VREF / STEPS # counts to volts
return (volts - 0.5) * 100 # TMP36: 10 mV per degree, 0.5 V at 0 degrees
print(round(count_to_celsius(158), 1))
log = [(0, 24.8), (10, 26.9), (20, 29.4), (30, 28.1), (40, 26.2), (50, 25.9)] # (minute, deg C)
hottest = max(temp for minute, temp in log)
minutes_hot = sum(10 for minute, temp in log if temp > 28) # samples are 10 min apart
print("Peak:", hottest, "C; above 28 C for about", minutes_hot, "min")
27.1
Peak: 29.4 C; above 28 C for about 20 min
If the greenhouse spent 20 minutes above 28 °C, the designer might raise the proportional gain so the vent opens sooner. That is data used for optimisation.
Software to Control Interactions and Dependencies
🎯 (SE-11-02)
📌 Experiment with software to control interactions and dependencies within mechatronic systems
- motion constraints
- degrees of freedom
- combination of subsystems
- combination of sensors, actuators and end effectors to create viable subsystems
Motion Constraints and Degrees of Freedom
Motion Constraints are limitations on how a mechanical system can move. Some come from the physical design: a joint that can only turn through a set range, a gripper that can only open and close, a differential-drive robot that can turn on the spot but cannot slide sideways. Others are added in software: a maximum speed or acceleration, and keep-out zones that the arm must not enter. Software must respect these constraints when it generates movement commands, because exceeding them can damage the mechanism, the work or a person.
def clamp(value, low, high):
return max(low, min(high, value))
SHOULDER_LIMITS = (10, 170) # degrees the joint may move through
ELBOW_LIMITS = (0, 135)
def safe_angles(shoulder, elbow):
return clamp(shoulder, *SHOULDER_LIMITS), clamp(elbow, *ELBOW_LIMITS)
print(safe_angles(90, 45))
print(safe_angles(200, -20))
(90, 45)
(170, 0)
Degrees of Freedom (DOF) is the number of independent movements a mechanism can make, usually one for each joint that can move on its own. A joint may rotate (revolute) or slide along a line (prismatic). More degrees of freedom let the end effector reach more positions and orientations, but each one needs an actuator, a sensor and more code.
| Mechanism | Degrees of freedom | What the software must control |
|---|---|---|
| Gripper | 1 | Open or close, and the grip force |
| Pan-and-tilt camera mount | 2 | Turn left or right; tilt up or down |
| Two-joint planar arm | 2 | Shoulder and elbow angles, which together set where the hand is on a flat surface |
| Six-axis industrial arm | 6 | Six joint angles, giving any position and orientation in 3D space |
Try it
Two-joint arm: degrees of freedom and limits
Move two joints, set a target and switch joint limits on or off. Enable JavaScript to use this tool. In short: each independent joint is one degree of freedom, and limits written in code restrict where the end effector can go.
Combination of Subsystems
Complex mechatronic systems are built by combining simpler subsystems. For example, an automated warehouse robot combines: (1) a vision subsystem to identify items, (2) a navigation subsystem to move through the warehouse, (3) a gripper subsystem to pick items, and (4) a communication subsystem to report status. The subsystems depend on each other, so software must coordinate them safely. In the code below, the gripper only opens when the robot is stationary, and the robot only drives when the gripper is closed, so it cannot drag an open gripper through a shelf.
class Rover:
def __init__(self):
self.speed = 0
self.gripper_open = False
def open_gripper(self):
if self.speed != 0: # dependency: only when stationary
return "REFUSED: rover is moving"
self.gripper_open = True
return "gripper open"
def drive(self, speed):
if self.gripper_open: # dependency: only with the gripper closed
return "REFUSED: gripper is open"
self.speed = speed
return "driving at " + str(speed)
rover = Rover()
print(rover.drive(50))
print(rover.open_gripper())
print(rover.drive(0))
print(rover.open_gripper())
print(rover.drive(50))
driving at 50
REFUSED: rover is moving
driving at 0
gripper open
REFUSED: gripper is open
Combination of Sensors, Actuators, and End Effectors
A viable subsystem has every part it needs to do one job: a sensor to know the state, an actuator to change it, and where relevant an end effector to do the work. A 3D printer's subsystems show this:
| Subsystem | Sensors | Actuators | End effector | Software dependency |
|---|---|---|---|---|
| Hot end | Temperature sensor | Heater element | Extruder nozzle | Do not extrude until the nozzle reaches temperature |
| Motion axes | Limit switches (home position) | Stepper motors on X, Y and Z | The print head they carry | Home each axis before printing |
| Filament feed | Filament run-out sensor | Extruder stepper | Nozzle | Pause the job if the filament runs out |
Software must synchronise these subsystems: heating the nozzle only when filament is loaded, coordinating several motors so the print head moves smoothly, and monitoring temperature feedback.
System diagram
Inside a mechatronic system
Sensors feed the microcontroller, the control algorithm decides, and actuators act. Power reaches every part.
What to notice
- Data flows in one direction: sense, decide, act.
- Different parts need different voltages, so the power system matters as much as the code.
Try thisRedraw this for a 3D printer. Which sensors and actuators does it need?
Experiment: finding a safe gripper speed
To experiment is to change one thing on purpose, measure the result and draw a conclusion. Suppose a gripper sometimes crushes a paper cup. A fair test changes only the closing speed and keeps everything else the same.
- Question and hypothesis: a slower closing speed will grip the cup without crushing it.
- Variables: change the closing speed (independent); measure whether the cup is held or crushed (dependent); keep the cup, the gripper and the force limit constant (controlled).
- Method: run each speed three times and record every result, so one odd trial does not mislead you.
- Conclusion: choose the fastest speed that never crushes the cup, then change the code and re-test.
| Closing speed | Trial 1 | Trial 2 | Trial 3 |
|---|---|---|---|
| 100% | Crushed | Crushed | Held |
| 60% | Held | Crushed | Held |
| 30% | Held | Held | Held |
These are sample results. They support 30% as the safe speed, and they show why three trials are used: at 60% the same setting gave different results.
Power, Battery and Material Requirements
🎯 (SE-11-03, SE-11-06)
📌 Determine power, battery and material requirements for components of a mechatronic system
Before you build, work out how much power the system needs, how long a battery will last, and what the parts should be made of. Getting this wrong leads to flat batteries, resets when motors start, or a frame that bends.
Power and current requirements
Two relationships do most of the work. Ohm's law, V = I × R, links voltage, current and resistance. Power is P = V × I. Add up the current every part draws, and find the worst case: a motor draws far more when it starts or stalls than when it runs freely.
| Requirement | Engineering Detail |
|---|---|
| Power & Battery | Current Draw Calculation: Total system current is calculated by summing the individual component currents: Total Current (mA) = Σ(Component Current)If a microcontroller draws 50 mA, two DC motors draw 300 mA each, and an LED draws 20 mA, total = 50 + 300 + 300 + 20 = 670 mA. A 5V supply must provide at least 670 mA, and a margin of about 50% is sensible, so choose a supply rated for about 1 A.Actuators often require separate power supplies from the controller to avoid "brownouts" (voltage drops) or component damage caused by the high current demands of motors. |
| Material Choice | Choosing based on weight, strength, and environmental conditions.Using carbon fibre for lightweight drone frames vs steel for industrial grippers. |
Worked example: the greenhouse power budget
The current figures below are typical example values; in a real design, read them from each part's datasheet.
| Part | Supply | Current (worst case) |
|---|---|---|
| Microcontroller board | 5 V rail | 60 mA |
| Temperature and soil sensors | 5 V rail | 10 mA |
| Vent servo (when it moves under load) | 5 V rail | 500 mA |
| 5 V rail total | 570 mA, so with a 50% margin about 860 mA: use a 1 A supply | |
| Water pump | Separate 12 V supply | 400 mA (4.8 W) |
Estimating battery life
Battery life (hours) ≈ battery capacity (mAh) ÷ average current (mA). A 2000 mAh battery powering the 670 mA system above lasts about 2000 ÷ 670 ≈ 3 hours. In practice, allow 20–30% less, because batteries lose capacity as they age and as they get cold: 2000 × 0.75 ÷ 670 ≈ 2.2 hours. Use the average current for battery life and the peak current to size the supply and wiring.
| Battery | Voltage per cell | Notes |
|---|---|---|
| Alkaline AA | 1.5 V | Cheap, not rechargeable, voltage sags under heavy load |
| NiMH AA | 1.2 V | Rechargeable; four in series give 4.8 V |
| Lithium-ion or LiPo | 3.7 V nominal | High capacity for its weight; needs a matching charger and protection circuit |
Try it
Power budget calculator
Add up the current of each part, size the supply and estimate battery life. Enable JavaScript to use this tool. In short: total current is the sum of each part's current, the supply must cover the worst case plus a margin, and battery life is capacity divided by average current.
Regulating voltage and limiting current
- Linear regulator: steps voltage down by turning the excess into heat. Dropping 9 V to 5 V at 500 mA wastes (9 − 5) × 0.5 = 2 W, which needs a heatsink and wastes battery. A buck converter is far more efficient.
- Current-limiting resistor: an LED that drops about 2 V on a 5 V supply at 15 mA needs R = (5 − 2) ÷ 0.015 = 200 Ω. Choose the next standard value, 220 Ω, which gives about 14 mA.
- Safety: fit a fuse on the battery lead, check polarity before connecting (reversed power can destroy a board), keep lithium batteries away from punctures and heat, and never leave them charging unattended.
Material requirements
Choose materials against the job: how strong and stiff the part must be, how much it can weigh, how it will be made, what it costs and where it will be used (heat, water, sunlight).
| Material | Strengths | Weaknesses | Suits |
|---|---|---|---|
| Aluminium | Strong for its weight (about 2.7 g/cm³), easy to machine, does not rust | Costs more than plastic; needs tools | Robot chassis, brackets, arm links |
| Steel | Very strong and stiff, cheap | Heavy (about 7.8 g/cm³), rusts unless coated | Industrial grippers, fixed frames |
| 3D-printed plastic (PLA or ABS) | Cheap, quick, any shape | Weaker; PLA softens in a hot car or sunny greenhouse | Prototype parts, sensor mounts |
| Acrylic sheet | Laser-cuts cleanly, light, clear | Brittle, cracks at screw holes | Enclosures, flat chassis plates |
| Carbon fibre composite | Very stiff and light | Costly; conducts electricity; splinters when cut | Drone frames |
A vent arm in a sunny greenhouse is outdoors, so PLA would sag on hot days. Aluminium or a heat-resistant plastic such as ABS is a better choice, even though a PLA part is quicker to print.
Wiring Diagrams
🎯 (SE-11-06)
📌 Develop wiring diagrams for a mechatronic system, considering data and power supply requirements
A wiring diagram shows every connection in a mechatronic system: which pin connects to which component, and how power reaches each part. Draw it before you build, and update it whenever the circuit changes. A schematic uses standard electronic symbols; a pictorial or breadboard diagram shows the parts as they look. Either works, provided every connection is labelled.
- Data connections: label each microcontroller pin and what it does, such as D9 → servo signal or A0 ← light sensor. Say whether each pin is a digital input, digital output, PWM output or analog input.
- Power supply: show voltage and ground for every component, and the current each supply can deliver. Give motors their own supply so they don't cause the microcontroller to brown out.
- Standard symbols and colours: red for positive, black for ground, and other colours for signals.
| Component | Signal pin | Power | Ground |
|---|---|---|---|
| Servo motor | D9 (PWM) | External 5 V | Common GND |
| Ultrasonic sensor | D7 trigger, D8 echo | 5 V from board | GND |
| Light sensor (LDR) | A0, at the joint between the LDR and a 10 kΩ resistor | LDR from the 5 V pin | 10 kΩ resistor to GND |
The LDR row is a voltage divider: the two resistors share the supply voltage, and the voltage at the joint changes as the LDR's resistance changes, which is what the analog pin reads.
Interface details a diagram must get right
- Common ground. Every supply and the microcontroller must share a ground, or the signals have no reference and behave randomly.
- Logic levels. A 3.3 V board such as the Pico must not be driven with a 5 V signal. Use a level shifter or a voltage divider on signals that come from 5 V parts.
- Pin current limits. A microcontroller pin can supply only a few tens of milliamps at most (check the datasheet). Motors, pumps and relays need a driver, such as a transistor, MOSFET or H-bridge, with their own supply.
- Flyback diode. Motors, relays and solenoids produce a voltage spike when switched off. A diode across the coil, pointing the right way, protects the driver.
- Pull-up resistors. A button wired between a pin and ground leaves the pin floating when open. Enabling the pin's internal pull-up, as in
Pin.PULL_UP, makes it read 1 when open and 0 when pressed.
Worked example: the greenhouse
| Component | Pin | Type | Power |
|---|---|---|---|
| TMP36 temperature sensor | GP26 (ADC0) | Analog input | 3.3 V from the board |
| Soil moisture sensor | GP27 (ADC1) | Analog input | 3.3 V from the board |
| Emergency stop button | GP14 | Digital input with pull-up | None: the other side goes to GND |
| Vent servo | GP15 | PWM output, 50 Hz | Separate 5 V supply |
| Pump driver | GP16 | Digital output | Pump on its own 12 V supply, with a flyback diode |
Wiring diagram
Data and power in the greenhouse controller
Solid lines carry signals and dashed lines carry power. The 12 V pump never draws its current through the board.
What to notice
- The servo and pump each have their own supply; only their signals come from the board.
- Every pin is labelled with the same name the code uses.
Try thisAdd a second servo for a side vent. Which pin would you use, and does the 5 V supply still have enough current?
Before power-on: check the diagram against the build with the power off; test continuity and polarity with a multimeter; connect grounds first; and add one part at a time, testing each.
Online Tool Suggestion: Try Tinkercad Circuits or Wokwi to develop wiring diagrams and simulate Arduino/ESP32 hardware connections virtually before building.
Designing for People with Disability
🎯 (SE-11-05)
📌 Determine specialist requirements that influence the design and functions of mechatronic systems designed for people with disability
Accessibility Considerations
When designing mechatronic systems for users with disabilities, engineers must consider:
Mobility Disabilities
- Prosthetics and Exoskeletons: Systems controlled by electromyography (muscle signals), requiring real-time sensor fusion and responsive actuation.
- Wheelchair Automation: Systems with joystick sensitivity adjustment, voice command control, or autonomous navigation for obstacle avoidance.
Visual Impairments
- Haptic Feedback: Vibration patterns in controllers to provide tactile information instead of visual displays.
- Spatial Audio: Sound-based navigation cues to guide users around obstacles.
- Obstacle Detection: Ultrasonic or infrared sensors to alert users to hazards.
Speech and Hearing Impairments
- Visual Feedback Systems: LED indicators, screen displays for status information.
- Sign Language Recognition: Computer vision systems to interpret sign language commands.
- Text-Based Interfaces: Keyboard or touch input for users with speech difficulties.
Severe Mobility Limitations
- Sip-and-Puff Control: Systems activated by gentle air pressure from the mouth for users with very limited mobility.
- Eye Gaze Tracking: Cameras or head-mounted sensors detecting where the user looks to control a cursor or robotic arm.
- Brain-computer interfaces: EEG or implanted sensors that interpret brain activity to control external devices. These are mostly at the research and trial stage.
Specialist requirements often add complexity and cost, but they fundamentally expand accessibility and enable people with disabilities to perform tasks they could not otherwise accomplish independently.
Determining the specialist requirements
To determine requirements, ask the people who will use the system, and their carers and therapists, what they need, and involve them in testing. Guessing what a user needs leads to devices that are unused. Then turn each need into a requirement you can test.
| Need | Design response | Testable requirement |
|---|---|---|
| Cannot press small buttons | Large switches, or a single switch with scanning | Every control can be operated with a 5 cm target or one switch |
| Cannot see a display | Audio prompts and haptic feedback | Every status change is announced by sound or vibration |
| Cannot hear an alarm | Flashing light and vibration | Alerts are visible from across the room |
| Tires quickly | Low actuation force, and automation of repeated tasks | Any control needs less than a set, small force |
| May move unpredictably near the device | Speed and force limits, soft stops, an emergency stop within reach | Motion stops within a set time when the stop is pressed |
Other requirements include reliability (a failed wheelchair controller is a safety risk, so it needs a fail-safe stop), adjustability (people differ and change over time), comfort, cost, and privacy for any data collected. Australia's Disability Discrimination Act 1992 makes it unlawful to discriminate against people with disability in areas such as education, employment and access to goods and services, which is one reason accessible design matters beyond good practice.
Worked example: single-switch scanning
A user who can press only one large switch controls a room menu. The system highlights each option in turn, and a press selects the one currently lit. The scan speed is a configuration setting, because users differ.
BEGIN scanMenu
index ← 0
selected ← FALSE
WHILE selected = FALSE
HIGHLIGHT option[index]
WAIT scanSeconds
IF switch pressed during the wait THEN
selected ← TRUE
ELSE
index ← index + 1
IF index = 4 THEN
index ← 0
ENDIF
ENDIF
ENDWHILE
PERFORM option[index]
END scanMenu
Developing Control Algorithms
🎯 (SE-11-02, SE-11-08)
📌 Develop, modify and apply algorithms to control a mechatronic system
A control algorithm reads sensors, decides what to do, and drives actuators, over and over. You develop one from the task, modify it when testing shows a problem, and apply it to the real hardware.
Developing an algorithm
- Define the inputs and outputs. What does the algorithm read (two reflectance sensors) and what can it command (left and right wheel speeds)?
- Write the rules in plain language, including the case where nothing matches.
- Express them in pseudocode and a flowchart using NESA notation.
- Trace the algorithm with test data, including boundary values, before writing any code.
- Code it, run it on the hardware, and record what happens.
BEGIN followLine
WHILE running
left ← READ leftSensor
right ← READ rightSensor
IF left = dark AND right = light THEN
turnLeft()
ELSE
IF right = dark AND left = light THEN
turnRight()
ELSE
driveForward()
ENDIF
ENDIF
ENDWHILE
END followLine
Flowchart
Line-following robot
The same algorithm as the pseudocode: read two sensors, choose one of three actions, repeat while the robot is running.
What to notice
- The loop test running? sits at the top, and every action returns to it.
- Each action is a subprogram (turnLeft, turnRight, driveForward) that is defined elsewhere.
Try thisTrace the flowchart with both sensors reading dark. Which action runs, and is that what a junction should do?
Trace table
A sensor is compared with a threshold found by calibration. Here, readings run from 0 (dark) to 1023 (light), and a reading below 400 counts as dark.
| Step | left | right | left = dark? | right = dark? | Action |
|---|---|---|---|---|---|
| 1 | 850 | 820 | No | No | driveForward |
| 2 | 300 | 800 | Yes | No | turnLeft |
| 3 | 820 | 250 | No | Yes | turnRight |
| 4 | 310 | 290 | Yes | Yes | driveForward (the ELSE branch) |
| 5 | 830 | 810 | No | No | driveForward |
DARK_BELOW = 400 # readings run 0 (dark) to 1023 (light); found by calibration
def decide(left, right):
left_dark = left < DARK_BELOW
right_dark = right < DARK_BELOW
if left_dark and not right_dark:
return "turnLeft"
elif right_dark and not left_dark:
return "turnRight"
else:
return "driveForward"
for left, right in [(850, 820), (300, 800), (820, 250), (310, 290), (830, 810)]:
print(left, right, decide(left, right))
850 820 driveForward
300 800 turnLeft
820 250 turnRight
310 290 driveForward
830 810 driveForward
Modifying the algorithm
turnLeft() and turnRight() to slow one wheel rather than stop it gives smoother steering. Record each change and why you made it.The zig-zag happens because the robot only has three actions, so it is always over-correcting. A better rule steers in proportion to how far the line is off centre: the further the difference between the two readings, the harder it turns.
BASE = 60 # base wheel speed, percent
KP = 0.05 # change in speed for each unit of sensor difference
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def wheel_speeds(left, right):
correction = (right - left) * KP # positive: the line is under the left sensor
return clamp(BASE - correction, 0, 100), clamp(BASE + correction, 0, 100)
for left, right in [(850, 820), (300, 800), (820, 250)]:
l, r = wheel_speeds(left, right)
print(left, right, round(l, 1), round(r, 1))
850 820 61.5 58.5
300 800 35.0 85.0
820 250 88.5 31.5
| Version | Problem found in testing | Change made | Result |
|---|---|---|---|
| 1 | Zig-zags on straight lines | None yet: three fixed actions | Reaches the end, slowly and jerkily |
| 2 | Overshoots on sharp bends at 60% speed | Proportional steering, KP = 0.05 | Smooth on straights; still overshoots the sharpest bend |
| 3 | Overshoot on sharp bends | Lower BASE speed to 45% | Completes the whole track |
The rows are sample entries that show how to keep a change log; the code for version 2 is above. Change one thing at a time so you know which change helped.
Applying the algorithm
To apply the algorithm, connect it to the real sensors and motors: replace the test numbers with readings from the ADC, pass the returned wheel speeds to the motor driver, and calibrate the threshold on the actual track under the actual lighting. A track that reads 300 in the classroom may read 500 under bright sunlight.
A second algorithm: avoiding obstacles
BEGIN avoidObstacles
WHILE running
distance ← READ ultrasonicSensor
IF distance < 15 THEN
reverse()
turnRight()
ELSE
IF distance < 40 THEN
slowDown()
ELSE
driveForward()
ENDIF
ENDIF
ENDWHILE
END avoidObstacles
| Step | distance (cm) | distance < 15? | distance < 40? | Action |
|---|---|---|---|---|
| 1 | 80 | No | No | driveForward |
| 2 | 35 | No | Yes | slowDown |
| 3 | 12 | Yes | — | reverse, turnRight |
| 4 | 40 | No | No | driveForward (boundary: not below 40) |
| 5 | 15 | No | Yes | slowDown (boundary: not below 15) |
Steps 4 and 5 test the boundary values, where most decision errors hide. Order matters too: the closest range must be tested first, or a 12 cm reading would match the "slow down" rule and the robot would never reverse.
Open and Closed Control Systems
🎯 (SE-11-02)
📌 Explore the algorithmic patterns, code and applications for open and closed control systems
The choice of control system depends on the task requirements for accuracy and self-correction. Engineers often use decision trees to map out these control paths.
Open loop and closed loop
| System Type | Analysis / Description | Pros/Cons |
|---|---|---|
| Open Loop | Operates on a fixed sequence or timer without feedback.A sprinkler system on a timer. | Pros: Simple, low cost. Cons: Cannot adjust to changes (e.g., if it rains). |
| Closed Loop | Uses sensor feedback to adjust output in real-time.A thermostat maintaining room temperature. | Pros: Accurate, self-correcting. Cons: Complex, potential for instability. |
More examples show the pattern. Open loop: a toaster's timer, a washing machine's fixed cycle, a stepper motor told to take 200 steps (it assumes every step happened). Closed loop: a car's cruise control (measures speed), a servo (an internal sensor reports its angle), and a drone that reads its tilt hundreds of times a second. Many systems combine both: a stepper-driven 3D printer axis moves open loop, but a limit switch closes a loop at start-up to find the home position.
Block diagram
Open loop and closed loop control
A closed loop adds a sensor so the controller can compare what is happening with what it wants.
What to notice
- Only the closed loop measures its output.
- Compare works out the error: setpoint minus the measured value.
Try thisA greenhouse vent opens at 26 °C. Label each block of the closed loop for it.
The parts of a closed loop
- Setpoint: the value you want (26 °C).
- Sensor and feedback: the measured value that is fed back to the controller.
- Error: setpoint minus measured value. Some systems, such as cooling, define it the other way round so a positive error opens the vent.
- Controller: the algorithm that turns the error into a command.
- Actuator and process: the device that acts (vent, heater) and the thing being controlled (air temperature).
- Disturbance: anything outside the loop that pushes the process away from the setpoint, such as a cloud or an open door.
Worked example: a thermostat with a hysteresis band
The simplest closed loop is on/off control: heater on when too cold, off when warm enough. If the controller switched at exactly one temperature, the heater would flick on and off many times a minute as the reading wobbles around it, wearing out a relay. A hysteresis band (also called a dead band) fixes this with two thresholds. In the band between them, the heater keeps whatever state it already has.
BEGIN thermostat
lowLimit ← 19
highLimit ← 21
heaterOn ← FALSE
WHILE running
temp ← READ temperatureSensor
IF temp < lowLimit THEN
heaterOn ← TRUE
ELSE
IF temp > highLimit THEN
heaterOn ← FALSE
ENDIF
ENDIF
SET heater TO heaterOn
WAIT 1 second
ENDWHILE
END thermostat
Flowchart
Thermostat with a hysteresis band
Two decisions set the heater state; when neither is true, the heater state does not change.
What to notice
- When the temperature is between 19 and 21 both decisions answer No, so heaterOn keeps its old value.
- The variable heaterOn is the controller's memory; without it there is no band.
Try thisChange the limits to 19.5 and 20.5. What happens to the number of times the heater switches, and to the temperature swing?
| Step | temp (°C) | temp < 19? | temp > 21? | heaterOn after |
|---|---|---|---|---|
| 1 | 18.2 | Yes | — | TRUE |
| 2 | 19.4 | No | No | TRUE (kept) |
| 3 | 20.3 | No | No | TRUE (kept) |
| 4 | 21.2 | No | Yes | FALSE |
| 5 | 20.6 | No | No | FALSE (kept) |
| 6 | 19.8 | No | No | FALSE (kept) |
| 7 | 18.9 | Yes | — | TRUE |
The band trades accuracy for stability: the temperature swings between about 19 and 21 °C but the heater switched only twice in seven readings. A narrower band gives a steadier temperature and more switching; a wider one saves the relay and allows a bigger swing. A sensor with noise larger than the band would still cause chatter, so the band is chosen with the noise in mind.
Choosing a control pattern
| Pattern | How it decides | Suits | Weakness |
|---|---|---|---|
| Open loop (timer or fixed sequence) | Runs a schedule; never checks | Predictable tasks where the outcome is not critical | Cannot correct errors or disturbances |
| On/off with hysteresis | Two thresholds and a remembered state | Heaters, pumps, fans | Always swings around the setpoint |
| Proportional | Output grows with the error | Vents, speed, position | Can settle slightly away from the setpoint |
| PID | Adds the accumulated and the changing error | Drones, precise motors | Needs tuning; can become unstable |
The simulator in the closed-loop section later on this page lets you compare these patterns on a heated room.
Features of Autonomous Control
🎯 (SE-11-02)
📌 Outline the features of an algorithm and program code used for autonomous control
Autonomous control means the system carries out its task without a person steering it. It is more than an automatic timer: an autonomous system senses its surroundings, chooses between actions and adapts when conditions change. Its program is usually organised as sense, decide, act, repeated continuously.
- Sensor Fusion: Combining data from multiple sensors (e.g., distance sensors, IMU accelerometers, temperature sensors) to build a comprehensive understanding of the environment. Simple algorithms average sensor readings; advanced algorithms use Kalman filters for optimal estimation.
- Decision Logic: Complex conditionals and state machines that determine what action to take based on current sensor readings and system state. Example: if an obstacle is detected within 20 cm, turn left when the left sensor is clear, otherwise turn right.
- PID Control Loop: Proportional-Integral-Derivative control adjusts actuator output based on error (difference between desired and actual state). Common in maintaining speed, temperature, or position.
- State Management: Tracking the current state of the system (idle, moving, gripper_open, gripper_closed) and transitioning between states safely based on sensor feedback and logic.
- Real-Time Response: Meeting strict timing requirements, responding to sensors and issuing commands within specified time windows. Delays in real-time systems can cause crashes or safety failures.
- Fallback Safety Logic: Default safe states (e.g., "all motors stop" if communication is lost) to prevent uncontrolled operation. A watchdog timer restarts the controller if the program stops responding.
State management in code
A robot vacuum shows how states and events combine. Each state lists the events it responds to and the state each event leads to; any event that does not apply is ignored.
| State | Event | Next state |
|---|---|---|
| IDLE | start | CLEANING |
| CLEANING | obstacle | AVOIDING |
| CLEANING | battery_low | RETURNING |
| AVOIDING | clear | CLEANING |
| RETURNING | docked | CHARGING |
| CHARGING | battery_full | IDLE |
| any state except IDLE and CHARGING | fault | ERROR (motors stop) |
TRANSITIONS = {
"IDLE": {"start": "CLEANING"},
"CLEANING": {"obstacle": "AVOIDING", "battery_low": "RETURNING", "fault": "ERROR"},
"AVOIDING": {"clear": "CLEANING", "fault": "ERROR"},
"RETURNING": {"docked": "CHARGING", "fault": "ERROR"},
"CHARGING": {"battery_full": "IDLE"},
"ERROR": {"reset": "IDLE"},
}
def next_state(state, event):
return TRANSITIONS[state].get(event, state) # an event that does not apply changes nothing
state = "IDLE"
for event in ["start", "obstacle", "clear", "battery_low", "docked", "battery_full", "obstacle"]:
new = next_state(state, event)
print(state, "+", event, "->", new)
state = new
IDLE + start -> CLEANING
CLEANING + obstacle -> AVOIDING
AVOIDING + clear -> CLEANING
CLEANING + battery_low -> RETURNING
RETURNING + docked -> CHARGING
CHARGING + battery_full -> IDLE
IDLE + obstacle -> IDLE
Keeping the transitions in a table makes the behaviour easy to check and extend: adding a state means adding a row, not rewriting nested if statements. The last line shows the safe default: an obstacle event while idle is ignored.
Autonomous algorithms typically use flowcharts, state diagrams, and pseudocode to be designed clearly before implementation. Testing autonomous systems requires extensive simulation and careful incremental real-world validation.
State diagram
A traffic light as a finite state machine
The system is always in exactly one state, and events move it to the next.
What to notice
- Timers trigger the normal cycle; an emergency signal can interrupt any state.
- Autonomous control code often follows a state machine like this.
Try thisAdd a Pedestrian crossing state. Which state should it follow, and what ends it?
Try it
State machine: a pedestrian crossing
Send events to a state machine and see which rule fires. Enable JavaScript to use this tool. In short: a controller is in one state, an event may move it to another state if a condition allows it, and it remembers things such as a pending request.
Producing a Mechatronic System
🎯 (SE-11-01, SE-11-02, SE-11-05, SE-11-06, SE-11-07)
📌 Design, develop and produce a mechatronic system for a real-world problem
- software control
- mechanical engineering
- electronics and mathematics
Start with the real-world problem
The school's seedlings die over long weekends because nobody opens the vent on hot days or waters the beds. A good design starts from the problem, then turns it into requirements that can be tested.
| Requirement | Type | How it will be tested |
|---|---|---|
| Keep air temperature at or below 26 °C | Functional | Heat the sensor to 29 °C and check that the vent opens about 30° |
| Water when soil moisture falls below 30% and stop above 45% | Functional | Move the sensor between dry and wet samples and watch the pump |
| Never run the pump for more than 30 seconds at a time | Safety | Disconnect the moisture sensor and confirm the pump stops |
| Run from a 5 V supply and a separate 12 V pump supply | Constraint | Measure the current on each supply |
| Cost less than a set budget | Constraint | Check the bill of materials |
Integrated System Design
Creating a complete mechatronic system requires simultaneous consideration of three domains:
Software Control
Algorithms and code that orchestrate sensor input, decision logic, and actuator output. Must be reliable, efficient, and responsive. In the greenhouse: read the sensors, decide with a proportional vent rule and a hysteresis pump rule, drive the servo and pump, and stop safely on a fault.
Mechanical Engineering
The physical structure, joints, materials, and mechanisms. Must support the required range of motion, handle the forces generated, and integrate smoothly with electronics. In the greenhouse: a lever from the servo to the vent flap. A servo horn 2 cm long turning through 90° moves the linkage along an arc of length s = r × θ = 2 cm × (π ÷ 2) ≈ 3.1 cm, which is how far the vent can open. The servo must also be strong enough to lift the flap; a longer lever gives more travel but needs more torque.
Electronics and Mathematics
Power circuits, signal conditioning, control mathematics (PID tuning), and sensor calibration. Requires understanding electrical properties of components and mathematical models of system behaviour. In the greenhouse: the ADC conversion from a count to °C, a straight-line calibration for the moisture sensor, the proportional gain, and the power budget.
Success requires communication and iteration between all three disciplines. For example, software delays might come from inadequate motor response, which is a mechanical/control issue; noise in sensor readings might require better filtering (electrical) or algorithm adjustments (software).
Working through the build
- Divide the system into subsystems (sensing, vent, watering, power, user interface) and give each a clear input and output.
- Choose the parts against the requirements and the power budget.
- Draw the wiring diagram and pin map before touching a wire.
- Simulate, then build one subsystem at a time, testing each on its own.
- Integrate, test the whole system, then evaluate it against the requirements table.
Documenting the build
Documentation lets someone else (or you, next term) understand, repair and improve the system. It also provides the evidence that the system meets its requirements.
| Document | What it records |
|---|---|
| Requirements and design notes | The problem, the requirements and the reasons for each design choice |
| Wiring diagram and pin map | Every connection, kept up to date with each change |
| Bill of materials | Every part, quantity, supplier and cost |
| Commented, versioned code | Why each section exists; a version number or date for each release |
| Test log | The test, the date, the expected and actual result, and the outcome |
| Change log | Each modification, the problem that prompted it, and whether it worked |
| Photographs and a safety note | The finished build and the hazards (electrical, moving parts, batteries) with how each is controlled |
Driving Mechatronic Devices
🎯 (SE-11-05, SE-11-06)
📌 Implement algorithms and design programming code to drive mechatronic devices
Code must correctly interface with hardware. Each kind of device needs a matching kind of signal from the microcontroller:
| Interface | What it does | Used for |
|---|---|---|
| Digital output | Sets a pin HIGH or LOW | LED, relay or pump driver |
| PWM output | Switches a pin on and off quickly; the fraction of time on is the duty cycle | Motor speed, LED brightness, servo angle |
| ADC input | Turns a voltage into a number | Temperature, light and soil sensors |
| I2C, SPI, UART | Serial buses that exchange bytes with other chips | IMUs, displays, other microcontrollers |
| Interrupt | Runs a function the moment a pin changes | Emergency stop, encoder pulses |
- PWM (Pulse Width Modulation): Varying the duty cycle (on-time vs off-time) of a square wave to control motor speed or LED brightness. At a 25% duty cycle on a 3.3 V pin, the motor sees an average of about 0.25 × 3.3 ≈ 0.8 V.
- Analog-to-Digital Conversion: Reading analog sensor values (e.g., 0–5 V) as digital numbers (0–1023) for processing.
- Serial Communication: Sending commands to and receiving data from sensors, actuators, and other microcontrollers. I2C uses two wires shared by many devices, each with its own address.
- Interrupt Handling: Responding immediately to urgent events (e.g., collision detection) without waiting for the main program loop.
Driving a servo
A hobby servo expects a pulse every 20 ms (50 Hz). The pulse width sets the angle: many servos treat 0.5 ms as 0° and 2.5 ms as 180° (check the datasheet, as some use 1 to 2 ms). So 90° needs 500 + 90 × 2000 ÷ 180 = 1500 µs, a duty cycle of 1.5 ÷ 20 = 7.5%.
from machine import Pin, PWM
vent = PWM(Pin(15), freq=50) # servos expect a 50 Hz signal
def set_angle(degrees):
# 0.5 ms pulse = 0°, 2.5 ms pulse = 180°
pulse_us = int(500 + degrees * 2000 / 180)
vent.duty_ns(pulse_us * 1000)
set_angle(90) # open the vent halfway
Driving a DC motor
A DC motor needs more current than a pin can give, so it is driven through an H-bridge motor driver. Two direction pins choose forward or reverse, and a PWM pin sets the speed. Keep the decision (what should the pins be?) in a pure function that can be tested on a laptop, and the hardware writes in a small separate function.
def motor_command(speed):
"""Turn a speed from -100 to 100 into (direction pin 1, direction pin 2, duty percent)."""
speed = max(-100, min(100, speed))
if speed > 0:
return (1, 0, speed)
if speed < 0:
return (0, 1, -speed)
return (0, 0, 0)
for s in (100, 40, 0, -60, 250):
print(s, motor_command(s))
100 (1, 0, 100)
40 (1, 0, 40)
0 (0, 0, 0)
-60 (0, 1, 60)
250 (1, 0, 100)
The last line shows why clamping matters: a command of 250 is limited to 100 before it reaches the driver.
Driving a stepper motor
A stepper turns a fixed angle for each pulse from its driver. A common motor has a 1.8° step angle, so one revolution is 360 ÷ 1.8 = 200 steps and a 90° turn is 50 steps. Microstepping splits each step: with 16 microsteps a 90° turn is 800 pulses. The program counts pulses instead of reading a sensor, which is open loop control, so an overloaded motor can silently lose steps.
Try it
PWM explorer
Change the duty cycle of a PWM signal or the pulse width for a servo. Enable JavaScript to use this tool. In short: average voltage = duty cycle × supply voltage, and a servo angle is set by the width of a pulse repeated every 20 ms.
Polling and interrupts
Polling checks an input each time round the main loop. It is simple, but an event that arrives during a long sleep() is noticed late. An interrupt makes the CPU pause the main program and run a short handler function as soon as the event occurs. Keep handlers very short: set a flag or switch off an output, and leave printing and slow work to the main loop. The emergency stop in the user-interface section uses this approach.
Simulations and Prototypes
🎯 (SE-11-06, SE-11-08)
📌 Develop simulations and prototypes of a potential mechatronic system to test programming code
Simulation vs Prototyping
Simulations (Wokwi, TinkerCAD, Gazebo) allow testing code in a risk-free virtual environment. Useful for debugging algorithms, testing edge cases, and exploring design variations before building hardware. However, simulations don't capture real-world issues like friction, vibration, electromagnetic interference, or component tolerances.
Physical prototypes identify these real-world factors. Starting with a minimal viable prototype (MVP) and iteratively improving it is more cost-effective than building a perfect system immediately. Prototyping also allows testing with actual users and refining the design based on their feedback.
| Simulation | Physical prototype | |
|---|---|---|
| Cost and risk | Free; nothing can break | Costs parts; mistakes can damage them |
| Speed of change | Edit and rerun in seconds | Rewiring and rebuilding take time |
| Realism | Ideal sensors, no friction or noise | Shows real noise, friction and power problems |
| Best for | Checking logic and wiring early | Tuning and final testing |
Developing a simulation
A circuit simulator such as Wokwi can run MicroPython on a virtual Raspberry Pi Pico with virtual sensors and servos. You can also write your own simulation in code. The idea is to replace the physical world with a model: a function that says how the sensor reading changes when the actuator acts. The control code runs against the model exactly as it would against the real hardware.
def vent_angle_for(temp, setpoint=26, kp=10):
return max(0, min(180, (temp - setpoint) * kp))
class SimGreenhouse:
"""A very simple model: the sun warms the air, an open vent cools it."""
def __init__(self, temp=24.0, outside=20.0, sun=0.30):
self.temp = temp
self.outside = outside
self.sun = sun # degrees gained per minute
def step(self, vent_angle):
cooling = 0.0005 * vent_angle * (self.temp - self.outside)
self.temp += self.sun - cooling
return self.temp
def run(kp, minutes=60):
room = SimGreenhouse()
angle = 0
for _ in range(minutes):
temp = room.step(angle)
angle = vent_angle_for(temp, kp=kp)
return round(room.temp, 1), round(angle)
for kp in (10, 30, 60):
print("KP =", kp, "-> temperature and vent angle after 60 min:", run(kp))
KP = 10 -> temperature and vent angle after 60 min: (31.2, 52)
KP = 30 -> temperature and vent angle after 60 min: (28.4, 72)
KP = 60 -> temperature and vent angle after 60 min: (27.4, 82)
The simulation found a design fault without touching hardware: with KP = 10 the greenhouse settles at 31 °C, five degrees above the setpoint, because the vent only opens 52° at that temperature. A higher gain fixes most of it. The model's numbers are invented for the exercise, so the simulation shows the behaviour of the algorithm and not the true temperatures. Before trusting a model, compare it with measurements from the real greenhouse and adjust it until they agree.
Fake hardware for testing
The same idea works for a single function: replace read_temperature() with a fake that returns a scripted list of values, including bad ones (a stuck sensor, a very high spike). Then check that the control code responds safely. Faults that are dangerous or awkward to create on a real system, such as an unplugged sensor, are easy to simulate.
Stages of prototyping
| Stage | What it is | What it tests |
|---|---|---|
| Mock-up | Cardboard, paper or foam model | Size, layout, reach and how people would use it |
| Breadboard prototype | Parts plugged together without soldering, on a temporary frame | That the wiring works and the code drives the parts |
| Working prototype | Soldered or screwed connections, real mechanism, real power | Repeatability, heat, vibration, battery life |
| Final build | Printed circuit board or tidy wiring, enclosure, cable management | Long-run reliability and safety |
Programming a Closed Loop Control System
🎯 (SE-11-02, SE-11-07)
📌 Design, develop and implement programming code for a closed loop control system
Closed loop systems use sensor feedback to adjust actuator output continuously. Implementation requires:
- Clearly defining the setpoint (desired state) and feedback (actual state).
- Calculating the error (setpoint − feedback, or feedback − setpoint if the system cools).
- Applying a control algorithm (on/off with hysteresis, proportional, PID, fuzzy logic, state machine) to generate corrective output.
- Iterating: reading sensors, calculating, and updating actuators in a tight loop. The right rate depends on how fast the system changes: a greenhouse might loop once a second, a self-balancing robot hundreds of times a second.
Proportional control
SETPOINT = 26 # °C we want to hold
KP = 10 # degrees of vent per °C of error
while True:
temp = read_temperature() # feedback: the actual state
error = temp - SETPOINT # this loop cools, so hotter than the setpoint = positive error
angle = max(0, min(180, error * KP)) # bigger error → wider vent, limited to the servo's range
set_angle(angle) # act on the error
sleep(1) # repeat once a second
This is proportional control: the correction grows with the error. Unlike an open loop timer, it keeps adjusting when a cloud passes or the door is left open. A trace of five readings shows the behaviour:
| Reading | temp (°C) | error = temp − 26 | error × KP | Vent angle after limiting |
|---|---|---|---|---|
| 1 | 27.0 | 1.0 | 10 | 10° |
| 2 | 28.5 | 2.5 | 25 | 25° |
| 3 | 27.5 | 1.5 | 15 | 15° |
| 4 | 26.4 | 0.4 | 4 | 4° |
| 5 | 25.8 | −0.2 | −2 | 0° (cannot be negative) |
On/off control with hysteresis
The pump in the greenhouse uses the on/off pattern from the previous part. Its two thresholds (30% and 45% moisture) form the band, and the pump keeps its current state between them:
PUMP_ON_BELOW = 30
PUMP_OFF_ABOVE = 45
def pump_next_state(pump_on, moisture):
if moisture < PUMP_ON_BELOW:
return True
if moisture > PUMP_OFF_ABOVE:
return False
return pump_on # inside the band: keep the current state
pump = False
for moisture in [50, 35, 28, 33, 40, 46, 41]:
pump = pump_next_state(pump, moisture)
print(moisture, "ON" if pump else "OFF")
50 OFF
35 OFF
28 ON
33 ON
40 ON
46 OFF
41 OFF
Adding an integral term: PID
The simulation earlier showed proportional control settling above the setpoint: when the error is small, so is the correction, so a small error can persist. This is steady-state error. An integral term adds up the error over time, so a persistent error keeps growing the correction until the error is gone. A derivative term reacts to how fast the error is changing, which damps overshoot. Together with the proportional term they make a PID controller. Add the code below to the simulation from the previous part (it reuses SimGreenhouse):
class PID:
def __init__(self, kp, ki=0.0, low=0, high=180):
self.kp, self.ki = kp, ki
self.low, self.high = low, high
self.integral = 0.0
def update(self, error, dt=1):
"""Return the vent angle for this error (measured minus setpoint)."""
command = self.kp * error + self.ki * self.integral
if self.low < command < self.high: # anti-windup: stop adding up while the vent is at its limit
self.integral += error * dt
return max(self.low, min(self.high, command))
def run_pid(controller, minutes=240):
room = SimGreenhouse()
angle = 0
for _ in range(minutes):
temp = room.step(angle)
angle = controller.update(temp - 26)
return round(room.temp, 1), round(angle)
print("P only :", run_pid(PID(kp=30)))
print("P and I:", run_pid(PID(kp=30, ki=1.0)))
P only : (28.4, 72)
P and I: (26.0, 100)
The integral term removed the 2.4 °C offset. Anti-windup matters: if the vent is fully open and the room is still too warm, an unchecked integral would keep growing and the controller would overshoot for a long time after conditions change.
| Increase… | Response speed | Overshoot | Steady-state error | Watch for |
|---|---|---|---|---|
| Kp (proportional) | Faster | Increases | Reduces but does not remove | Oscillation if too high |
| Ki (integral) | Slightly faster | Can increase | Removes | Windup; slow oscillation |
| Kd (derivative) | Little change | Reduces | No direct effect | Amplifies sensor noise |
Explore it: closed loop simulator
The simulator below runs a heated room under three controllers. It shows the ideas above with a heater instead of a vent: heat leaks out through the walls, and the controller decides how hard to run the heater.
This simulator needs JavaScript. Without it, the ideas are: an open loop heater runs at a fixed output and the room drifts when the weather changes; an on/off controller with a band keeps the room within a few degrees of the setpoint but switches on and off; a proportional controller settles a little away from the setpoint, and a larger Kp reduces that offset.
Integrating Sensors, Actuators and End Effectors
🎯 (SE-11-07)
📌 Apply programming code to integrate sensors, actuators and end effectors/manipulators
Each part may work on its own, but the system only works when sensors, actuators and end effectors run together under one program. Integrate in small steps: add one component, test it, then add the next.
Integrating Components into Viable Subsystems
Each subsystem (gripper, movement, vision) must be coded, tested, and debugged in isolation before integration. Components share power supplies, communication buses, and may have timing dependencies—careful integration prevents conflicts.
while True:
temp = read_temperature()
moisture = read_moisture() # 0 = dry, 100 = soaked
set_angle(vent_angle_for(temp)) # vent subsystem
if moisture < 30 and not pump.value():
pump.on() # watering subsystem
elif moisture > 45:
pump.off() # gap between 30 and 45 stops rapid on/off
sleep(1)
sleep() in one subsystem delays every other one. Keep each step short and share one main loop.Complete worked example: the greenhouse program
Here is the finished controller. It is split into two files. control.py holds the decisions and imports no hardware, so it can be tested on a laptop. main.py connects those decisions to the pins in the wiring table. Copy both files onto the board; MicroPython runs main.py at power-on. Comments such as (6) refer to the explanation table below.
# control.py - the decision logic only. It imports no hardware modules,
# so it can be tested on a laptop with ordinary Python.
SETPOINT = 26 # °C to hold
KP = 10 # degrees of vent per °C of error
DRY_RAW, WET_RAW = 45000, 20000 # calibration: readings in dry air and in water
PUMP_ON_BELOW = 30 # % moisture
PUMP_OFF_ABOVE = 45
def raw_to_celsius(raw, vref=3.3):
volts = raw / 65535 * vref
return (volts - 0.5) * 100 # TMP36: 10 mV per °C, 0.5 V at 0 °C
def raw_to_moisture(raw):
percent = (DRY_RAW - raw) * 100 / (DRY_RAW - WET_RAW)
return max(0, min(100, percent))
def sensor_ok(temp):
return -10 < temp < 60 # anything outside this range means a fault
def vent_angle_for(temp, setpoint=SETPOINT, kp=KP):
return max(0, min(180, (temp - setpoint) * kp))
def pump_next_state(pump_on, moisture):
if moisture < PUMP_ON_BELOW:
return True
if moisture > PUMP_OFF_ABOVE:
return False
return pump_on # inside the band: keep the current state
def smooth(readings, new, size=5):
readings.append(new)
if len(readings) > size:
readings.pop(0) # keep only the last 5 readings
return sum(readings) / len(readings)
# main.py - greenhouse controller for a Raspberry Pi Pico-class board (MicroPython)
from machine import Pin, PWM, ADC
import utime
import control
# (1) Hardware, matching the wiring table
temp_adc = ADC(26) # TMP36 on GP26
soil_adc = ADC(27) # soil moisture sensor on GP27
vent = PWM(Pin(15), freq=50) # servo signal on GP15
pump = Pin(16, Pin.OUT, value=0) # pump driver on GP16, starts OFF
stop_button = Pin(14, Pin.IN, Pin.PULL_UP) # emergency stop: reads 0 when pressed
# (2) State
PUMP_MAX_MS = 30000 # never run the pump longer than 30 s
stopped = False
pump_locked = False
pump_started = 0
readings = []
def set_angle(degrees): # (3) output helper
vent.duty_ns(int(500 + degrees * 2000 / 180) * 1000)
def emergency_stop(pin): # (4) interrupt handler: short and safe
global stopped
pump.off()
stopped = True
stop_button.irq(trigger=Pin.IRQ_FALLING, handler=emergency_stop)
while not stopped: # (5) main loop
raw_temp = control.raw_to_celsius(temp_adc.read_u16()) # (6) sense
moisture = control.raw_to_moisture(soil_adc.read_u16())
if not control.sensor_ok(raw_temp): # (7) fail safe
pump.off()
print("SENSOR FAULT: pump off")
else:
temp = control.smooth(readings, raw_temp) # (8) decide and act: vent
angle = control.vent_angle_for(temp)
set_angle(angle)
wanted = control.pump_next_state(pump.value() == 1, moisture) # (9) decide and act: pump
if wanted and not pump_locked:
if not pump.value():
pump_started = utime.ticks_ms()
pump.on()
if utime.ticks_diff(utime.ticks_ms(), pump_started) > PUMP_MAX_MS:
pump.off()
pump_locked = True # needs a restart: something is wrong
print("PUMP TIMEOUT")
else:
pump.off()
print("temp %.1f C vent %d deg soil %d%% pump %s" % # (10) log
(temp, round(angle), round(moisture), "ON" if pump.value() else "OFF"))
utime.sleep(1)
vent.duty_ns(0) # (11) stop driving the servo
print("STOPPED")
Line-by-line explanation
| Part | What it does and why |
|---|---|
control.py constants | The setpoint, the gain, the two calibration readings and the pump thresholds are named values at the top, so a change is made in one place. DRY_RAW and WET_RAW come from calibration (see the testing section). |
raw_to_celsius | Converts the ADC value (0 to 65535) to volts, then to °C using the TMP36's 10 mV per °C and 0.5 V offset. |
raw_to_moisture | A straight-line calibration: the dry reading maps to 0% and the wet reading to 100%. max(0, min(100, …)) keeps the result in range. |
sensor_ok | A sanity check. A disconnected or shorted sensor gives an impossible temperature, and the program must not act on it. |
vent_angle_for | Proportional control: 10° of vent for each °C above 26, limited to the servo's 0 to 180° range. |
pump_next_state | Hysteresis: on below 30%, off above 45%, otherwise the previous state. |
smooth | A moving average of the last five readings. |
| (1) Hardware | Creates an object for each device, using the pin numbers from the wiring table. The pump pin starts at 0 so the pump is off at power-on. The button uses the internal pull-up, so it reads 0 when pressed. |
| (2) State | Variables that persist between loops: whether the emergency stop was pressed, whether the pump has been locked out, when the pump started, and the recent readings. |
(3) set_angle | Turns an angle into a pulse width (0.5 to 2.5 ms) and sends it to the servo. |
(4) emergency_stop | An interrupt handler. It runs the moment the button is pressed, switches the pump off and sets a flag. It does no slow work, and the main loop checks the flag. |
(5) while not stopped | The main loop repeats until the flag is set. |
| (6) Sense | Reads both sensors and converts them to °C and %. |
| (7) Fail safe | If the temperature is impossible, switch the pump off and report the fault instead of acting on bad data. |
| (8) Vent | Smooths the temperature, calculates the vent angle and moves the servo. |
| (9) Pump | Asks the hysteresis function what the pump should do. When it turns on, the start time is recorded; if it has run for more than 30 seconds the program switches it off and locks it out, in case the moisture sensor has failed and the pump would otherwise flood the bed. |
| (10) Log | Prints one line per second: this is diagnostic data for testing and for optimisation. |
| (11) After the loop | Stops sending pulses to the servo and reports that the system has stopped. |
temp 27.0 C vent 10 deg soil 50% pump OFF
temp 27.7 C vent 17 deg soil 35% pump OFF
temp 27.7 C vent 17 deg soil 28% pump ON
temp 27.3 C vent 13 deg soil 33% pump ON
temp 27.0 C vent 10 deg soil 46% pump OFF
STOPPED
Read the log: the temperature readings are smoothed, so the vent follows the average and not each spike; the pump switches on at 28% soil moisture, stays on at 33% (inside the band) and switches off at 46%. The vent angles are small because the temperature is only just above the setpoint.
Control Algorithms That Enhance Performance
🎯 (SE-11-08)
📌 Implement specific control algorithms that enhance the performance of a mechatronic system
Control Algorithm Optimisation
Performance means how quickly, accurately, smoothly and reliably the system does its job. Specific algorithms enhance it:
| Technique | Problem it solves | Cost or risk |
|---|---|---|
| PID tuning | Slow response, overshoot, steady-state error | Needs testing; wrong gains oscillate |
| Acceleration ramping | Jerky starts, wheel slip, current surges | Slower to reach full speed |
| Sensor filtering | Noisy or spiky readings | Adds lag: the filtered value trails the truth |
| Non-blocking timing | A long sleep() delays every other task | Slightly more complex code |
- PID Tuning: Adjusting proportional, integral, and derivative gains to achieve fast, stable response without overshoot. A practical method: start with Ki and Kd at zero; raise Kp until the system responds quickly; if it oscillates, back Kp off; then add a little Ki to remove the steady-state error, and Kd only if overshoot remains. Change one gain at a time and keep a record.
- Acceleration Ramping: Gradually increasing motor speed to reduce jerky movements and mechanical stress.
- Sensor Filtering: Averaging or smoothing noisy sensor readings to improve decision quality.
Sensor filtering
readings = []
def smoothed_temperature():
readings.append(read_temperature())
if len(readings) > 5:
readings.pop(0) # keep the last 5 readings
return sum(readings) / len(readings) # moving average
Without smoothing, a single noisy reading can make the vent twitch open and shut. Averaging the last five readings makes the system calmer, and the servo lasts longer. Different filters suit different noise, so compare them on the same data:
def moving_average(values):
return sum(values) / len(values)
def median(values):
ordered = sorted(values)
return ordered[len(ordered) // 2]
def exponential(previous, new, alpha=0.3):
return alpha * new + (1 - alpha) * previous
readings = [24.0, 24.2, 30.9, 24.1, 24.3] # 30.9 is a one-off spike
print("Average:", round(moving_average(readings), 1))
print("Median :", median(readings))
smoothed = readings[0]
for value in readings[1:]:
smoothed = exponential(smoothed, value)
print("Exponential:", round(smoothed, 1))
Average: 25.5
Median : 24.2
Exponential: 25.1
The true temperature is about 24.2 °C. The average and the exponential filter are both pulled up by the spike; the median ignores it. The average is the simplest choice for random noise; the median is better for occasional spikes; the exponential filter needs no list of past readings, so it uses little memory. Every filter adds lag, so a heavily filtered sensor reacts late to real change.
Acceleration ramping
Jumping a motor from 0 to full speed draws a large current surge, can make wheels slip and jolts a payload. A ramp limits how much the command can change each loop:
def ramp(current, target, max_step=10):
if target > current:
return min(target, current + max_step)
return max(target, current - max_step)
speed = 0
for _ in range(6):
speed = ramp(speed, 45)
print(speed, end=" ")
print()
10 20 30 40 45 45
Timing without blocking
Calling sleep(1) stops the whole program for a second, so a button press or a second sensor cannot be checked. Instead, record when the last control step ran and act only when enough time has passed:
import utime
last_step = utime.ticks_ms()
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_step) >= 1000: # one second has passed
last_step = now
control_step() # read, decide, act
check_buttons() # runs on every pass, many times a second
ticks_diff is used because the tick counter wraps around; subtracting the values directly would fail at the wrap.
A User Interface to Control the System
🎯 (SE-11-07)
📌 Design, develop and implement a user interface (UI) to control a mechatronic system
Even autonomous systems often need interfaces for:
- Manual Override: Emergency stop buttons, mode selection switches.
- Status Display: LED indicators, LCD screens showing system state, error codes, sensor readings.
- Configuration: Buttons to set parameters (speed, sensitivity, operating mode).
- Accessibility: Voice control, touchless interfaces for users with disabilities.
UI code must be responsive and not interfere with real-time control loops. Keeping the interface code separate from the control logic (an idea related to the Model-View-Controller pattern) improves maintainability: you can change the display without touching the control algorithm.
Designing the interface
- Who uses it, and for what? The greenhouse users are students and a teacher: check the state, switch to manual, and stop everything in an emergency.
- Choose inputs and outputs that suit those tasks.
- Give feedback for every action (a press should change the display or a light) and make errors hard to make.
- Test with real users and change what confuses them.
| Element | Type | Purpose | Design note |
|---|---|---|---|
| Emergency stop button | Input | Stop the pump at once | Large, red, easy to reach; wired to an interrupt |
| Mode button | Input | Switch between AUTO and MANUAL | Debounced; the display shows the new mode straight away |
| Open and close buttons | Input | Move the vent by hand in MANUAL | Steps of 10° within the servo limits |
| 16 × 2 character display | Output | Temperature, vent angle, soil moisture and mode | Fixed layout so values do not jump about |
| Status LED and buzzer | Output | Show a fault without reading the display | Different patterns for different faults; not colour alone |
Interface code
Buttons "bounce": one press can make several rapid contacts. The code ignores any press that comes too soon after the previous one. The functions below hold the interface decisions, so they can be tested without a display or buttons.
MODES = ["AUTO", "MANUAL"]
def next_mode(mode):
return MODES[(MODES.index(mode) + 1) % len(MODES)]
def accept_press(now_ms, last_ms, gap_ms=200):
"""Ignore contact bounce: a new press must come at least gap_ms after the last one."""
return now_ms - last_ms >= gap_ms
def manual_angle(angle, button):
step = 10 if button == "open" else -10
return max(0, min(180, angle + step))
def status_lines(temp, angle, moisture, mode):
line1 = "T:%4.1fC V:%3d" % (temp, round(angle))
line2 = "Soil:%3d%% %s" % (round(moisture), mode)
return line1.ljust(16)[:16], line2.ljust(16)[:16]
mode, last = "AUTO", -1000
for t in (0, 30, 500): # the press at 30 ms is contact bounce
if accept_press(t, last):
mode, last = next_mode(mode), t
print(t, mode)
angle = 170
for b in ("open", "open", "close"):
angle = manual_angle(angle, b)
print(b, angle)
for line in status_lines(27.3, 13, 33, "AUTO"):
print("|" + line + "|")
0 MANUAL
30 MANUAL
500 AUTO
open 180
open 180
close 170
|T:27.3C V: 13 |
|Soil: 33% AUTO |
The press at 30 ms is ignored, so the mode changes only twice. The second open stays at 180 because the angle is clamped to the servo's range.
The emergency stop
stop_button = Pin(14, Pin.IN, Pin.PULL_UP)
stopped = False
def emergency_stop(pin):
global stopped
pump.off() # switching a pin is fast and safe inside a handler
stopped = True # the main loop sees this flag and shows "STOPPED"
# an interrupt reacts at once, even in the middle of the main loop
stop_button.irq(trigger=Pin.IRQ_FALLING, handler=emergency_stop)
The handler only switches the pump off and sets a flag. Updating a display inside a handler is slow and can fail, so the main loop does it. For a machine that can hurt someone, a software stop is not enough: a real emergency stop is also wired to cut the power to the actuators, so it works even if the program has crashed.
Unit Testing Control Algorithms
🎯 (SE-11-06, SE-11-07)
📌 Create and use unit tests to determine the effectiveness and repeatability of each component’s control algorithm
Mechatronic systems require rigorous testing of each component. A unit test checks one small piece (a function or a single device) against a known input and an expected result. Testing has two aims: effectiveness (does the algorithm do the right thing?) and repeatability (does it do the same thing every time?).
- Sensor Tests: Verify sensors return expected values for known inputs (temperature sensor in ice vs boiling water).
- Actuator Tests: Confirm motors respond correctly to commands (PWM 50% = half speed).
- Algorithm Tests: Verify logic produces correct outputs (e.g., a collision avoidance routine turns away from obstacles).
- Integration Tests: Test subsystems working together (e.g., vision detects object + gripper activates + movement ceases).
- Stress Tests: Run systems continuously for extended periods to identify degradation or reliability issues.
Unit tests for the control algorithm
Because the decisions live in control.py with no hardware imports, the tests run on a laptop. Choose test data on purpose: a normal value, the boundaries (the edges of each rule), and values outside the expected range.
| Function | Input | Expected output | Why this case |
|---|---|---|---|
vent_angle_for | 20 °C | 0° | Below the setpoint: the vent stays shut |
vent_angle_for | 29 °C | 30° | Normal case: (29 − 26) × 10 |
vent_angle_for | 60 °C | 180° | Above the servo's range: the output is limited |
raw_to_moisture | The dry and wet readings | 0% and 100% | Calibration end points |
raw_to_moisture | Drier than dry, wetter than wet | 0% and 100% | Out-of-range readings are limited |
pump_next_state | 29%, 40% (on and off), 46% | on, stays on, stays off, off | Below, inside and above the hysteresis band |
sensor_ok | −40 °C | False | A disconnected sensor must be rejected |
# test_control.py - runs on a laptop with ordinary Python: python3 test_control.py
import control
def test_closed_when_cool():
assert control.vent_angle_for(20) == 0
def test_opens_in_proportion():
assert control.vent_angle_for(29) == 30
def test_never_exceeds_servo_range():
assert control.vent_angle_for(60) == 180
def test_moisture_end_points():
assert control.raw_to_moisture(control.DRY_RAW) == 0
assert control.raw_to_moisture(control.WET_RAW) == 100
def test_moisture_is_limited_to_0_and_100():
assert control.raw_to_moisture(60000) == 0 # drier than the dry reading
assert control.raw_to_moisture(10000) == 100 # wetter than the wet reading
def test_pump_hysteresis():
assert control.pump_next_state(False, 29) is True # below the band: on
assert control.pump_next_state(True, 40) is True # inside the band: stays on
assert control.pump_next_state(False, 40) is False # inside the band: stays off
assert control.pump_next_state(True, 46) is False # above the band: off
def test_bad_sensor_rejected():
assert control.sensor_ok(24.0)
assert not control.sensor_ok(-40) # a disconnected sensor
def test_celsius_conversion():
assert abs(control.raw_to_celsius(14894) - 25) < 0.1 # 0.75 V from a TMP36 is 25 °C
failed = 0
tests = [f for name, f in sorted(globals().items()) if name.startswith("test_")]
for test in tests:
try:
test()
print("PASS", test.__name__)
except AssertionError:
print("FAIL", test.__name__)
failed += 1
print(len(tests) - failed, "passed,", failed, "failed")
PASS test_bad_sensor_rejected
PASS test_celsius_conversion
PASS test_closed_when_cool
PASS test_moisture_end_points
PASS test_moisture_is_limited_to_0_and_100
PASS test_never_exceeds_servo_range
PASS test_opens_in_proportion
PASS test_pump_hysteresis
8 passed, 0 failed
Tests protect against mistakes made later. If someone removed the max(0, …) from vent_angle_for, the vent would be commanded to a negative angle when the room is cool, and test_closed_when_cool would fail straight away. A test that fails for the right reason is a success: it found the bug before the greenhouse did. (MicroPython has no built-in test framework, which is another reason to keep logic in a file that also runs on a laptop.)
Testing and calibrating sensors
Real sensors are not perfect: two identical parts can read differently, and readings drift with temperature and age. Calibration compares the sensor with a known reference and corrects the difference. A two-point calibration uses a low and a high reference and draws a straight line between them.
- Measure the sensor in a low reference (ice water, close to 0 °C) and a high reference (boiling water, close to 100 °C at sea level; use a reference thermometer if you have one). Say the sensor reads 1.8 °C and 97.5 °C.
- Work out the gain: (100 − 0) ÷ (97.5 − 1.8) ≈ 1.045.
- Correct every later reading: corrected = 0 + (reading − 1.8) × 1.045.
- Check the result against a third reference, such as a room-temperature thermometer.
def make_calibration(raw_low, ref_low, raw_high, ref_high):
gain = (ref_high - ref_low) / (raw_high - raw_low)
return lambda raw: ref_low + (raw - raw_low) * gain
correct = make_calibration(1.8, 0.0, 97.5, 100.0)
print(round(correct(1.8), 1), round(correct(97.5), 1), round(correct(25.0), 1))
0.0 100.0 24.2
The soil moisture sensor is calibrated the same way: record its raw reading in dry air (DRY_RAW) and in a glass of water, up to its marked line (WET_RAW), and the program maps the range between them to 0–100%. Record every calibration with the date, the references used and the values found, and repeat it when a sensor is replaced.
Repeatability
Repeatability testing ensures the system behaves consistently across multiple trials—critical for autonomous systems where unpredictable behaviour is unacceptable. Logging sensor data and actuator commands during tests helps engineers debug failures and improve reliability. Set a pass criterion before testing, then compare.
trials = [30, 29, 31, 30, 30] # vent angle (degrees) after heating the sensor to 29 C, five runs
target, tolerance = 30, 3
mean = sum(trials) / len(trials)
spread = max(trials) - min(trials)
print("mean", mean, "spread", spread, "PASS" if abs(mean - target) <= tolerance and spread <= 2 * tolerance else "FAIL")
mean 30.0 spread 2 PASS
Because the algorithm is a separate function, it can be tested on a laptop without any hardware. Repeatability is then checked on the real system by running the same test several times, for example heating the sensor to 29 °C five times and confirming the vent reaches about 30° each time. The trial values above are example data; on a real system you record your own. A small spread means the system is repeatable; a large spread points to a loose linkage, a noisy sensor or unstable code.
A test plan and log
| Test | Type | Pass criterion | Result (example entry) |
|---|---|---|---|
| Temperature sensor in ice water and boiling water | Sensor | Within ±2 °C of the references after calibration | Pass: 0.0 and 100.0 °C |
| Servo commanded to 0°, 90° and 180° | Actuator | Vent flap reaches each position without buzzing or stalling | Pass |
| Control functions (the unit tests above) | Algorithm | All tests pass | 8 of 8 pass |
| Heat the sensor and dry the soil together | Integration | Vent opens and pump runs without the controller resetting | Pass |
| Run for 24 hours | Stress | No resets, no drift greater than 1 °C | To be done |
A test log records the date, the test, the expected and actual result, and what was changed after a failure, so that anyone can see that the system was checked and how it improved.