The previous lecture described a design method on a small example, with a single sensor and a single power output. A small example has the advantage of clarity, but it hides exactly what makes real design difficult: contention between subsystems for the same microcontroller resources. This lecture takes the same method through a project that has these difficulties in abundance - an autonomous mobile robot built for a student competition, followed from the rulebook to the lessons learned in the field.
1Scope and structure of the lecture6 min
The robot has a four-wheel chassis, of which two motors provide differential drive, four line sensors, two ultrasonic sensors, two optical encoders, an emergency button and a serial link. Almost every module studied in this course comes into play at the same time, and an ATmega328P with twenty usable pins offers fewer resources than the sum of the subsystems demands - design consists, to a large extent, of choosing the trade-offs.
- The method: description, specification, partitioning with interfaces fixed in advance, finite state machine, code in layers tested separately, a test plan linked to requirements (Lecture 10)
- A resource margin (at least 20%) is the practical rule - we will see what happens when there is none left (Lecture 10)
- Draw up a resource budget (pins, timers, ADC channels, interrupts) and explain the trade-offs imposed when it saturates
- Explain why counting encoder pulses is done in an interrupt, while computing speed is done in the main loop
- Describe the three physical phenomena that must be handled when driving a motor through an H-bridge: dead zone, command jump, emergency stop
- Calculate the position error of a line-following robot from a proportional controller and explain the limits of the coefficient Kp
- Recognize why the field-testing level cannot be replaced by the lab
2Context and project theme6 min
The competition setting imposes exactly what is usually missing from teaching projects: a specification written by someone else, which cannot be negotiated, a fixed deadline and a public trial in which the system either works or does not. The organizers publish the rulebook, the track dimensions, the size limits and the mandatory safety components - an emergency stop button reachable from outside, an automatic stop when the link is lost, power from the team's own batteries. All of these go directly into the specification, with no discussion.
The robot must autonomously follow a route marked with tape, go around obstacles, continuously report its own status, and be stoppable at any time from a remote control. What makes the project hard is not the algorithmic part - modest here, a proportional controller and a five-state machine - but fitting within the resources and meeting the timing.
3The robot specification9 min
The competition rulebook is the description of the system, in ordinary language, written by someone who does not design electronics: "the robot must follow the marked route" and "it must avoid collisions" - unverifiable phrasing. The team's first technical act is turning these into a specification with measurable requirements, through questions put to the organizer: how wide is the tape? What is the minimum turning radius? Does the route have intersections? Every answer becomes a requirement.
| Code | Requirement | Acceptance criterion |
|---|---|---|
| CF1 | Follows the 19mm tape, min. curvature 250mm | 10 laps with no loss of the route |
| CF3 | Obstacle at <250mm: stop within ≤300ms | measuring the stopping distance, 20 repeats |
| CF5 | Emergency stop: motors to zero within ≤50ms | oscilloscope, 20 repeats |
| CF7 | Odometry: error under 3% over 2m straight | 10 trials, compared with a tape measure |
| CN4 | No sensor fault produces a maximum command | disconnecting each sensor in turn |
CF4, CN3 and CN4 do not appear anywhere in the rulebook - they come from the list of questions about abnormal situations. CN4 expresses a non-obvious principle: a disconnected sensor produces, at the ADC input, an undefined value, and if the algorithm treats it as valid, the result can be a maximum command on the motor - exactly the dangerous behavior.
Properly designed, it physically interrupts the power stage supply, through a contact in series with the H-bridge, and only secondarily notifies the microcontroller through a digital input. A button that only sets a bit read by the program does not protect exactly in the case that matters most: the one where the program has hung.
4System partitioning and the resource budget13 min
With the requirements fixed, partitioning follows: line sensors → position; encoders → speeds; ultrasonics → echo; operator interface → button/START/LED; output → PWM and direction to the H-bridge, frames to the USART. Each subsystem delivers a single quantity and completely hides its implementation behind it - if the type of line sensor or the bridge model changes, only a single subsystem gets rewritten.
Missing from the subsystem table: any module using the synchronous buses from Lecture 09. The team initially wanted a display and an inertial sensor on I2C, but both were dropped - the reason becomes clear immediately: there were no pins left.
The resource budget
The Arduino Uno board offers twenty input-output pins. The budget is organized into four chapters: pins, timers, analog channels, interrupt sources.
| Pin | Direction | Signal |
|---|---|---|
| PD0, PD1 | USART | Remote control commands / log |
| PD2, PD3 | Input | INT0, INT1 - left/right encoders |
| PD4, PD7 | Output | Left/right motor direction |
| PB1, PB2 | Output | OC1A, OC1B - motor PWM |
| PB0, PB3 | Input | PCINT0/3 - ultrasonic echo |
| PC0...PC3 | Analog | The four line sensors |
| PC4, PC5 | Analog / Input | Battery voltage / emergency button |
No pin was left free. Consequences, all conscious decisions: (1) four line sensors, not six - position is known more coarsely, the robot cuts corners at high speed. (2) I2C disappears - PC4/PC5 were given to the voltage sensor and the emergency button. (3) there is no free test pin left - the compromise solution was reusing the status LED pin, at the cost of an LED that blinks erratically during measurements.
5Timers, analog channels and interrupts10 min
Timer1 generates the two PWM signals for the motors, in 8-bit fast PWM mode, prescaling 8: the resulting frequency is 16,000,000/(8·256) = 7812.5 Hz, above the audible range, which removes the motor whine. Timer2 produces the 10 ms tick of the loop, in CTC mode, prescaling 1024, compare value 155: the period is 1024·156/16,000,000 = 9.98 ms. Timer0 is left to the microsecond time base, for measuring echoes.
The datasheet recommends capture on ICP1 for precise measurement of a pulse width - but here Timer1 is already occupied by 8-bit PWM, its counter rolls over every 128 µs, while an echo can last up to 17.5 ms. The accepted compromise: reading the microsecond time base on the two edges, 4 µs resolution. Sound travels round-trip about 1cm in 58 µs, so 4 µs means under a millimeter - well under the sensor's own error. The compromise rests on a calculation, not an impression.
The analog-to-digital converter has six accessible channels - four for the line sensors, one for the battery voltage. At a prescaling of 128, the ADC clock is 125 kHz, one conversion takes 13 periods (104 µs); the five conversions in one cycle consume about 520 µs, over 5% of the 10 ms period - not much, but not negligible either.
Interrupt sources used: INT0/INT1 (encoders), the pin-change vector of port B (echoes), Timer2's compare match (the system tick), USART reception (commands). The pin-change vector is shared by an entire port - the routine must figure out which pin changed, by comparing the current value with the stored one, and it fires on both edges, which is useful here, because we need both.
The wheels have an 80mm diameter, the encoder discs 60 slots, the designed maximum speed 0.6 m/s. The ISR takes measured about 4 µs, including context saving.
See the solution
Wheel circumference: π·80 = 251.3 mm; with 60 slots, one pulse every 251.3/60 = 4.19 mm. At 600 mm/s, one wheel produces 600/4.19 = 143 pulses/s. With two wheels and counting on a single edge: 286 interrupts/s, so 286·4 µs = 1.14 ms of processor time per second, that is 0.11%. Even doubling the resolution by counting both edges, the load stays under a quarter of a percent. Counting in the interrupt is practically free - which justifies choosing it over polling.
6Program architecture and the state machine13 min
The architecture is the hybrid one from Lecture 10: interrupts capture the fast events, a main loop paced by a 10 ms tick makes the decisions. The golden rule: nothing slow is allowed into the loop. If the loop waited synchronously for a sensor's echo, it would stall for up to 17.5 ms - more than the entire tick period; the robot would drive blind and run off the route in a curve. That is why the echo is measured through interrupts, and the log is deposited into a circular buffer. Measured with the oscilloscope, the processing pulse width was about 1.1 ms - 11% of the period, well under the 50% limit required by CN1, a margin that later allowed odometry to be added.
The state machine
| State | Event | Next state |
|---|---|---|
| WAITING | START | LINE FOLLOWING |
| LINE FOLLOWING | obstacle detected | AVOIDING |
| LINE FOLLOWING | line below threshold | LINE LOST |
| AVOIDING | avoidance finished | LINE FOLLOWING |
| LINE LOST | 2s with no line | WAITING |
| any state | emergency button / STOP | EMERGENCY |
volatile uint8_t tickFlag = 0; /* raised by Timer2's ISR */
int main(void) {
motorsStopImmediately(); /* safe state BEFORE configuration */
portsInit(); pwmInit(); tickInit(); encodersInit();
ultrasonicInit(); adcInit(); usartInit(38400);
sei();
for (;;) {
if (!tickFlag) continue; /* waiting for the tick; no active delay */
tickFlag = 0;
odometryUpdate(); lineSensorsRead();
ultrasonicSchedule(); powerCheck();
if (emergencyButton() || stopCommand()) {
motorsStopImmediately(); state = ST_EMERGENCY;
}
switch (state) {
case ST_FOLLOWING:
if (frontDistance() < D_OBSTACLE_MM) { avoidanceStart(); state = ST_AVOIDING; }
else if (!lineVisible()) { timerStart(&tSearch, 2000); state = ST_LINE_LOST; }
else lineController();
break;
/* ... the other cases, one per state ... */
default: /* impossible state: preventive stop */
motorsStopImmediately(); state = ST_EMERGENCY; break;
}
logFillBuffer(); /* deposits bytes, does not wait for transmission */
}
}
The emergency conditions are checked before the switch statement, so they apply from any state, and the default branch leads the system into the safe state - if the state variable ends up, for whatever reason, at an impossible value, the robot stops.
7Motor control: dead zone, ramp, emergency stop10 min
DC motors are driven with PWM, not with a variable continuous voltage - the winding inductance smooths the current, the motor behaves as if it received the average voltage, and the switching element dissipates little. Direction is obtained by reversing polarity through an H-bridge. The absolute rule: the two arms of the same branch never conduct at the same time, otherwise it is a direct short-circuit across the battery.
Three physical phenomena must be handled explicitly. The dead zone: below a certain duty cycle, the torque does not overcome the static friction in the gearbox, the motor stays still but draws current. Left uncompensated, the controller produces, for small errors, commands that never turn into any movement. Compensation compresses the useful range of the command over the interval between the measured threshold and the maximum value. The command jump: an abrupt transition from zero to maximum draws a current limited only by the winding resistance - the battery voltage sags (which can reset the microcontroller), the wheels slip (which breaks odometry). The solution: a ramp, limiting the command's variation per cycle; with a step of 8 units per 10 ms, the transition 0→255 takes 319 ms, imperceptible but enough to remove the current spike.
CF5 requires zero within ≤50 ms; the ramp would need over 300 ms. The immediate-stop function writes zero directly into the compare registers, with no rate limiting - an exception justified by safety, which must be written as a comment, otherwise a later reader "fixes" it.
#define PWM_MIN 40 /* measured: below this threshold the motor does not turn */
#define RAMP_STEP 8 /* maximum command variation in a 10 ms tick */
/* Compresses the useful range 1..255 over PWM_MIN..PWM_MAX */
static uint8_t pwmFromCommand(int16_t magnitude) {
if (magnitude <= 0) return 0;
if (magnitude > 255) magnitude = 255;
return (uint8_t)(PWM_MIN + ((uint32_t)magnitude * (PWM_MAX - PWM_MIN)) / 255);
}
static int16_t applyRamp(int16_t current, int16_t desired) {
if (desired > current + RAMP_STEP) return current + RAMP_STEP;
if (desired < current - RAMP_STEP) return current - RAMP_STEP;
return desired;
}
/* Emergency stop: DELIBERATELY bypasses the ramp, for the 50 ms requirement. */
void motorsStopImmediately(void) {
OCR1A = 0; OCR1B = 0;
appliedCmdLeft = 0; appliedCmdRight = 0;
}
8Odometry from encoders10 min
Each drive wheel carries a slotted disc, read by an LED-phototransistor pair. With a 60-slot disc and an 80 mm wheel, one edge corresponds to 4.19 mm of travel.
A single-channel encoder cannot distinguish the direction of rotation. The rigorous solution - a quadrature encoder - would need two more interrupt inputs, which we do not have. The compromise solution: the routine adds to the counter using the sign of the command given to the motor, assuming the wheel turns the way it was told to. The assumption is false when braking (inertia carries the wheel further) and when a wheel is blocked and pushed backward - both introduce odometry errors, accepted as the price of saving pins.
volatile int32_t pulseCountLeft = 0, pulseCountRight = 0;
volatile int8_t directionLeft = +1, directionRight = +1; /* written by motorsCommand(), read by the ISR */
ISR(INT0_vect) { pulseCountLeft += directionLeft; } /* rising edge, left encoder */
ISR(INT1_vect) { pulseCountRight += directionRight; }
/* The counter is 4 bytes; the read is protected, but the previous state of the I
bit is restored, so we do not enable interrupts in a section where they were already off. */
int32_t encoderRead(volatile int32_t *pCounter) {
int32_t v; uint8_t sreg = SREG;
cli(); v = *pCounter; SREG = sreg;
return v;
}
In a 10 ms cycle, the counters increase by 12 pulses (left) and 10 (right). The robot's wheel track is 150 mm. Find the speeds and the quantization error.
See the solution
Directly on the 10 ms window: 12·4.19/0.01 = 5030 mm/s - absurd for a small robot. The reason: at 0.5 m/s, in 10 ms only about 1 pulse per wheel arrives, the result is dominated by quantization. Redone on a 100 ms window, with the same 12 and 10 pulses: vl=503 mm/s, vr=419 mm/s, so v=(503+419)/2=461 mm/s and ω=(419−503)/150=−0.56 rad/s (turning to the right). Quantization introduces an uncertainty of one pulse, that is 4.19/0.1=41.9 mm/s, about 8% of the speed. Practical conclusion: the calculation window must be longer than the control cycle - the counters are read every 10 ms, but speed is computed over the last ten readings, with a 100 ms delay that is acceptable, because speed is used for reporting, not for line following.
9Line following: the proportional controller11 min
The four line sensors are LED-phototransistor pairs facing the floor. The first mandatory operation is normalization: for each sensor, a calibration on the track before the trial records the value over open floor and over the tape, and the read value is brought, through linear interpolation, onto a common 0-100 scale. Without it, differences between individual phototransistors would make the sensors incomparable, and the computed position would be systematically shifted.
The line position is a weighted average: each sensor is assigned a conventional position (−3, −1, +1, +3), and the error is the center of mass of the normalized responses. The sum of the responses serves a double purpose - if it is below a threshold, no sensor sees the tape, the line is lost (the concrete implementation of requirement CN4).
#define KP_LINE 18 /* command units per unit of error */
#define LINE_THRESHOLD 40 /* normalized sum below which the line is lost */
static const int8_t position[4] = { -3, -1, +1, +3 };
int16_t lineError(const uint8_t n[4], int16_t *pSum) {
int16_t sum = 0; int32_t moment = 0;
for (uint8_t i = 0; i < 4; i++) { sum += n[i]; moment += (int32_t)position[i] * n[i]; }
*pSum = sum;
if (sum < LINE_THRESHOLD) return 0; /* undefined error: line is lost */
return (int16_t)(moment * 100 / sum);
}
void lineController(void) {
int16_t sum, e = lineError(nSensor, &sum);
int16_t u = (int16_t)(((int32_t)KP_LINE * e) / 100);
motorsCommand(BASE_SPEED + u, BASE_SPEED - u);
}
The correction is proportional to the error, added to one wheel's command and subtracted from the other's. With Kp too small, the robot reacts sluggishly, cutting corners on the inside; with Kp too large, it oscillates with growing amplitude around the line. The upper limit of Kp is set by the total delay in the loop (the cycle period, the motors' time constant, the robot's inertia).
The normalized sensors read 5, 20, 90, 35 (left→right). Base speed 120, Kp=18.
See the solution
Sum = 5+20+90+35 = 150 (above the threshold of 40, the line is visible). Moment = (−3·5)+(−1·20)+(1·90)+(3·35) = −15−20+90+105 = 160. The error (hundredths) = 160·100/150 = 106, that is 1.06 units to the right. Correction u = 18·106/100 = 19. Commands: left 120+19=139, right 120−19=101 - the robot steers to the right, toward the tape. The maximum possible error is ±3 units (±300 hundredths), so u ∈ [−54, 54], and the commands stay within [66, 174] - they never saturate at 255, a good property: if it saturated at maximum error, the robot would lose the ability to correct further, exactly in the toughest situation. All calculations used integer arithmetic, with an explicit scale factor of 100 - on a microcontroller with no floating point, this is not a matter of style, but the difference between a few microseconds and a few hundred.
10The test plan, with a field level8 min
The plan follows the four levels from Lecture 10, with one peculiarity: the last level cannot be done in the lab. A robot that is flawless on the workbench can fail on the track for reasons that have nothing to do with the program - the room's lighting, the floor's grip.
Module level: the pure functions (position error, dead-zone compression, the ramp) compiled for the host computer, tried at their limits. Subsystem level: wheels lifted off the ground, an increasing command to find the dead-zone threshold; ten manual turns to check 600 encoder pulses. Integration level: with the motors running, do the encoders still count correctly, or do bridge disturbances produce spurious edges? Field level: complete scenarios on the track.
| Test | Requirement | How it is performed | Criterion |
|---|---|---|---|
| T2 | - | wheels lifted, increasing command | dead-zone threshold identical ±5 on both wheels |
| T6 | - | motors at maximum command, wheels blocked | counters do not increase from spurious edges |
| T8 | CF5 | emergency button at maximum speed, 20 repeats | bridge command to zero in <50ms |
| T11 | - | random characters on the serial interface | invalid frames ignored, no lockup |
11What worked and what did not8 min
A case study that reports only successes is of no use - the mistakes repeat with regularity in every generation of teams.
What worked was whatever had been decided on paper before it was built: partitioning with written interfaces allowed the traction module to be replaced halfway through the project with no change anywhere else in the program; the state machine gave defined behavior to unforeseen situations; the rule "nothing slow in the loop" never had to be reconsidered.
What did not work was whatever had been decided on the fly. The first mistake: calibrating the sensors with values compiled into the program - at the competition, the lighting and the floor were different, the robot could no longer see the tape at all. The remedy: a calibration started through the start button, with the robot walked manually over the tape. The second: sharing the same supply between the electronics and the motors - under sudden acceleration, the voltage sagged enough that the microcontroller reset in the middle of the track. The fixes: a large capacitor at the regulator input, the acceleration ramp, and a voltage supervisor circuit. The third: trusting odometry as a source of absolute position - errors accumulate, and after a few meters the estimated position drifts visibly away from the real one; useful for short maneuvers, not for long-term navigation without periodic correction.
They were discovered in the move from the lab to the real environment - exactly the testing level teams schedule last and cut first when time runs out. The practical conclusion: get onto the real track as early as possible, with an incomplete robot, not late with a finished one.
Nothing that made this project possible was new knowledge about the ATmega328P - the timer registers, the interrupts, the ADC chain were all already known from earlier lectures. What decided the outcome was the order of decisions, the discipline of writing interfaces before implementations, and the habit of verifying by measurement, not by assumption - the difference between knowing how to program a microcontroller and knowing how to design a system with a microcontroller.
12Common mistakes4 min
- We calibrate the sensors once, in the lab, and hard-code the thresholds. The lighting and floor color at the competition differ from the lab - the robot no longer sees the tape at all. Calibration must be done on site, through a procedure started on site (a dedicated button), never assumed from compiled values.
- The electronics and the motors are powered from the same battery, to keep things simple. The current spike under sudden acceleration makes the voltage sag enough that the microcontroller resets. A large capacitor at the regulator input, an acceleration ramp, and a voltage supervisor circuit, applied together.
- Odometry gives us the robot's position, so we can use it for long-distance navigation. Quantization, wheel slip and the assumed (not measured) direction of the wheels accumulate - the error grows without bound with distance. Odometry is useful only for short maneuvers, where the error has no time to build up; long distances need an independent periodic correction.
13Summary and glossary5 min
A complete project shows that the method from Lecture 10 behaves the same way under the pressure of insufficient resources as on the small example: interfaces written in advance allow parallel development and the replacement of a subsystem without breaking the rest; the resource budget (pins, timers, ADC channels, interrupts) turns "this microcontroller should be enough" into a verifiable list, and its saturation forces explicit, documented trade-offs rather than accidental ones; the state machine makes dangerous combinations impossible by construction; the rule "nothing slow in the loop" is checked by measurement with an oscilloscope, not by assumption; motor control requires explicitly handling the dead zone, the command jump, and the emergency-stop exception; odometry counts in an interrupt and computes in the loop, over a window long enough to dominate quantization noise; line following is a simple proportional controller, but with mandatory normalization and Kp limited by the loop's total delay; and the test plan adds, compared to lab projects, a field level that cannot be replaced. The final lesson has nothing to do with registers, but with discipline: interfaces before implementation, measurement before assumption.
14Review questions7 min
- Turn the requirement "the robot must avoid collisions" into two or three verifiable requirements, with numeric values and a measurement method.
- Explain why the emergency stop button must not act only through software.
- Why could Timer1's capture module not be used to measure the echo?
- A colleague moves the speed calculation from the main loop into the encoder ISR, "to keep it more current". What breaks?
- What is the dead zone of a geared motor, and what behavior does it produce, if uncompensated, in a line-following robot?
- Describe the procedure for calibrating the line sensors on the track and explain why values compiled into the program cannot replace it.
15Closing the discipline2 min
This is the last lecture of the Microcontrollers and Microprocessors discipline. The whole journey - from the bits and registers of an isolated microcontroller to an autonomous system that must work correctly on the first try, on an unknown track - used, at every step, the same principles: measure, do not assume; write the interface before the implementation; test in layers.
The integrated application of all of these notions - acquisition, decision, control, with no
delay() - is practiced in Laboratory
07, the discipline's final lab session.