Microcontrollers in Circuits

Basic Electronics

Course 1 · Chapter 10 · Microcontrollers in Circuits

Nine chapters have built the parts of a robot’s electronics one at a time: voltage and current, passive parts, semiconductors, logic, motors and power. A microcontroller board is where they meet. Seen as a circuit part, it is a small chip that can read voltages (logic levels and analogue levels) and produce voltages (logic levels and PWM), with all the limits of any other part: a supply voltage it must stay inside, a few milliamps per pin, an input that expects a certain impedance and a certain range. This chapter is about those limits and about connecting the board to the circuits of Basic Electronics 1 to Basic Electronics 9 without damaging either side. It is not a programming course: each program is short, explained line by line only where the electronics matters, and every circuit is given twice, once for a real breadboard and once for a free browser simulator, with the readings you should expect.

Four boards are used, each where it is the most suitable one, and each time the reason is stated. The Arduino Uno (5 V logic, 10-bit ADC) is the board for 5 V sensors and for learning, and the one that Tinkercad Circuits simulates well. The Raspberry Pi Pico (3.3 V, 12-bit ADC, flexible 16-bit PWM on any pin, MicroPython) is the board for clean 3.3 V work and motor PWM. The ESP32 (3.3 V, Wi-Fi and Bluetooth, many ADC channels and PWM channels) is the board when the robot must be commanded wirelessly. The Raspberry Pi (a Linux computer with GPIO pins and no ADC) is the board when the job is software-heavy (a camera, a network service, a screen) and the electronics is simple. Basic Electronics 11 compares them side by side and wraps up the course.

1. The board as one part in a circuit

Every board shows the same four faces to the outside world, whatever its name:

  • Power in. A USB socket, a barrel jack or a pin (VIN, VSYS), feeding an onboard regulator (Basic Electronics 9) that makes the 5 V or 3.3 V the chip actually runs from. The regulator’s spare current is what you may take from the 5 V or 3.3 V pin for sensors: a few tens to a few hundred milliamps, never motors or servos.
  • Ground. Several GND pins, all one net. Every external supply that talks to the board by a signal must share this ground (the common ground of Basic Electronics 7 and Basic Electronics 9).
  • GPIO pins. General-purpose input and output pins that are configured as a digital input, a digital output, an analogue input (ADC), a PWM output or a special function (serial, I2C, SPI).
  • Reset and boot behaviour. At power-on and reset, GPIO pins are inputs (floating or pulled) until the program configures them, for tens to hundreds of milliseconds. Any circuit that must not act in that interval, such as a motor driver, needs a defined default (section 8).

The numbers that matter for each board

These are typical datasheet and manufacturer figures from memory; the exact limits depend on the exact chip, the exact board and the pin, so confirm them on the datasheet before you rely on them (Basic Electronics 11 returns to this).

Arduino Uno (ATmega328P)Raspberry Pi Pico (RP2040)ESP32 dev boardRaspberry Pi (e.g. Pi 4)
Logic level5 V3.3 V3.3 V3.3 V, not 5 V tolerant
Current per output pin20 mA recommended, 40 mA absolute maximum; about 200 mA for the whole chip4 mA by default, selectable up to 12 mA; about 50 mA total (check the datasheet)About 20 mA recommended, 40 mA absolute maximum (varies with the pin and the pad setting)16 mA per pin, about 50 mA across all pins (the commonly quoted figures)
ADC10-bit, 6 channels, reference 5 V (or 1.1 V internal)12-bit, 3 pins plus an internal temperature sensor; reference is the 3.3 V supply; effective resolution closer to 9 or 10 bits because of noise12-bit, many channels; ADC2 cannot be used while Wi-Fi runs; nonlinear near 0 V and near full scale; needs attenuation set for a 0 to about 3.1 V rangeNone: add an external ADC chip (for example an MCP3008 over SPI, 10-bit)
PWM6 pins, 8-bit, 490 or 980 Hz by default (timer registers change this)16 channels on any pin, 16-bit counter with an adjustable frequencyLEDC peripheral: 16 channels, any output pin, resolution trades against frequency2 hardware channels (4 pins), plus software PWM that jitters
Interrupt inputsExternal interrupts on D2 and D3 only; pin-change interrupts on all pinsAny GPIOAny GPIOAny GPIO, through the operating system (latency tens to hundreds of microseconds, not guaranteed)
Internal pull-up20 to 50 kΩabout 50 to 80 kΩ (typical)about 45 kΩ (typical)about 50 kΩ (typical)
Decouple the board, not just the chips
A running processor draws current in sharp bursts, and a 100 nF capacitor across the supply at each chip (Basic Electronics 9) keeps those bursts off the rails. Boards already have these on the board, but a sensor or driver mounted on a breadboard still needs its own 100 nF at its supply pins, as close as the board allows. When a program works until a motor starts, the supply, not the code, is the first suspect.

2. Digital outputs and the pin-current budget

An output pin is a pair of tiny switches: one to the supply (drives the pin high) and one to ground (drives it low). It behaves as a voltage source with a small series resistance (about 25 to 40 Ω on an AVR pin), so it holds its voltage only while the current stays small. The first design rule is therefore a current budget: the current taken from any one pin, and from all pins together, stays within the figures of the table in section 1. The second is that the pin is a signal source: anything that needs more than a few milliamps is switched by a transistor or driver (Basic Electronics 6 and Basic Electronics 9) and the pin supplies only the base or gate current.

LED on a pin (series resistor, the Ohm's-law rule of Basic Electronics 1): R = (V(pin) - V(LED)) / I I = (V(pin) - V(LED)) / R Uno (5 V pin), red LED with V(LED) = 2.0 V, target about 10 mA: R = (5 - 2.0) / 0.010 = 300 ohm -> standard 330 ohm: I = 3.0 / 330 = 9.09 mA Pico (3.3 V pin), same LED, target about 4 mA (the default drive): R = (3.3 - 2.0) / 0.004 = 325 ohm -> standard 330 ohm: I = 1.3 / 330 = 3.94 mA (the resistor dissipates I^2 R = 0.00394^2 x 330 = 5.1 mW: any 1/4 W resistor) The LED is a little dimmer at 3.3 V (4 mA) than at 5 V (9 mA), but still clearly visible: the eye responds to light roughly logarithmically. Pin-current budget for several LEDs (Uno, V(LED) = 2.1 V, 270 ohm each): I = 2.9 / 270 = 10.7 mA per pin (under 20 mA) Six of them together: 6 x 10.7 = 64 mA (well under the 200 mA chip total) Twenty of them would need 215 mA: over the chip total, even though every single pin is within its own limit. Check both limits. A load that needs more current (a relay coil, a motor, a strip) goes through a switch. Relay coil 5 V, 70 ohm: I = 5 / 70 = 71 mA (far above a pin's budget) NPN switch, forced gain 10: I(base) = 71 / 10 = 7.1 mA R(base) = (5 - 0.7) / 0.0071 = 602 ohm -> standard 560 ohm: I(base) = 4.3 / 560 = 7.7 mA Same switch from a 3.3 V pin: R(base) = (3.3 - 0.7) / 0.0071 = 364 ohm -> standard 330 ohm: I(base) = 2.6 / 330 = 7.9 mA (more than the Pico's default 4 mA drive: use a logic-level MOSFET instead, whose gate takes no steady current, as in Basic Electronics 6) The flyback diode across the coil is not optional (Basic Electronics 5 and Basic Electronics 9).

3. Digital inputs, pull-ups and pull-downs

An input pin is a very high impedance (about 1 µA leakage or less) that decides “high” above a threshold VIH and “low” below VIL, as in Basic Electronics 8. The point of an input is that, left unconnected, it floats: it picks up mains hum and your hand, and reads at random. A switch that connects the pin only to ground leaves it floating while open, so a resistor must hold it at a defined level. A pull-up resistor to the supply makes the pin read high when the switch is open and low when it is closed (the pin is “active low”). Every board has an internal pull-up that the program can turn on (INPUT_PULLUP on an Arduino, Pin.PULL_UP in MicroPython), which saves the external resistor.

Button to ground, pull-up resistor Rp to the supply Vcc: Vcc o-----[ Rp ]-----+----> input pin | [ switch ] | 0 V o----------------+ Open: the pin sits at Vcc (the input leakage of 1 uA across Rp = 10 k drops only 10 mV) Closed: the pin is at 0 V, and the supply gives away I = Vcc / Rp while the button is held Choosing Rp is a compromise: too small -> wastes current when pressed (Vcc / Rp) and loads the contact more than needed too large -> slow edges (rise time about 2.2 x Rp x C(pin and wire)) and a pin easily pulled by noise or leakage Rp I when pressed, 5 V I when pressed, 3.3 V rise time with 150 pF of wire and pin 4.7 k 1.06 mA 0.70 mA 2.2 x 4700 x 150e-12 = 1.6 us 10 k 0.50 mA 0.33 mA 3.3 us 50 k (internal) 0.10 mA 0.066 mA 16.5 us For a switch on a short wire the internal pull-up (about 50 k) is fine. For a long cable (200 pF and noise), a sensor or a rapid signal, use an external 4.7 k to 10 k. A button also bounces for 1 to 5 ms on closing (Basic Electronics 8): debounce it in software (ignore changes for 20 ms) or with the RC and Schmitt trigger of that chapter.

4. The ADC: turning a voltage into a number

An analogue-to-digital converter compares the input voltage with a reference voltage Vref and returns a whole number from 0 to 2N − 1 for an N-bit converter. The step size, one LSB (least significant bit), is the smallest change it can report. The two formulas you need are the code for a given input and the voltage for a given code.

N-bit ADC, reference Vref: LSB = Vref / 2^N code = floor( Vin / Vref x 2^N ) (limited to 2^N - 1) Vin is about code x LSB (the true value is within one LSB above it) The quantisation error is up to +/- half an LSB (reading (code + 0.5) x LSB removes the bias). Uno N = 10, Vref = 5 V: LSB = 5 / 1024 = 4.883 mV reading 512: 512 x 4.883 mV = 2.500 V half an LSB = 2.44 mV Pico N = 12, Vref = 3.3 V: LSB = 3.3 / 4096 = 0.806 mV reading 2048: 2048 x 0.806 mV = 1.650 V (MicroPython's read_u16() gives this 12-bit value shifted up to 16 bits) Uno with the 1.1 V internal reference: LSB = 1.1 / 1024 = 1.074 mV (4.5 times finer, but the input must stay below 1.1 V) Reading a TMP36 temperature sensor (output V = 0.5 V + 10 mV per degree C, so 0.750 V at 25 C): Uno at 5 V: one LSB = 4.883 mV = 0.488 C at 25 C: Vin = 0.750 V code = floor(0.750 / 5 x 1024) = floor(153.6) = 153 temperature = (153 x 5 / 1024 - 0.5) x 100 = (0.7471 - 0.5) x 100 = 24.7 C Uno at 1.1 V: one LSB = 1.074 mV = 0.107 C a range from -50 C to 60 C: the finer step is worth it when the signal is small. The reference sets the scale: a 5 V reference that droops to 4.8 V under load reads every value 4 % too high. USB-powered boards have a few percent of reference error; measure the board's real 5 V with a meter and put that figure in the program for better accuracy.

Fitting a larger voltage into the ADC: the divider

The ADC input must stay between 0 V and Vref (and never above the board’s supply, or the chip is damaged). A battery pack is higher, so a resistive divider (Basic Electronics 1) scales it down. Design it in three steps: choose the ratio from the full-charge voltage, then pick resistors so the source impedance the ADC sees is not too high, then add a small capacitor.

Divider: V(pin) = V(bat) x R2 / (R1 + R2) the ratio k = R2 / (R1 + R2) must satisfy k x V(bat, max) is at most Vref (leave a few percent spare) Battery voltage from the code: V(bat) = code x LSB / k one LSB of the battery = LSB / k The ADC input prefers a source impedance of about 10 k or less (the Uno's datasheet recommends 10 k): the divider's own R1 || R2 is that impedance. A 100 nF capacitor from the pin to ground holds the input steady (it gives the ADC's sampling capacitor a reservoir) and filters noise. Example 1 (the plan for Circuit 2 below, Pico): a 4-cell NiMH pack, 5.6 V full (1.4 V per cell), 4.8 V nominal, 4.4 V as the low-battery limit. R1 = R2 = 10 k (k = 0.5) full 5.6 V: pin = 2.80 V code = floor(2.80 / 3.3 x 4096) = 3475 (under 3.3 V with plenty of margin) low 4.4 V: pin = 2.20 V code = floor(2.20 / 3.3 x 4096) = 2730 One LSB of the battery = 0.806 mV / 0.5 = 1.61 mV. Source impedance 10 k || 10 k = 5 k. Divider current 5.6 V / 20 k = 0.28 mA (negligible against a 2000 mAh pack). Example 2 (Uno measuring a 3-cell LiPo, 12.6 V full): R1 = 22 k, R2 = 10 k: k = 10 / 32 = 0.3125 12.6 V: pin = 3.9375 V code = floor(3.9375 / 5 x 1024) = 806 one LSB of the battery = 4.883 mV / 0.3125 = 15.6 mV source impedance 22 k || 10 k = 6.9 k Example 3 (Pico measuring the same 12.6 V pack): R1 = 33 k, R2 = 10 k: k = 10 / 43 = 0.2326: 12.6 V -> 2.930 V (code 3637); 11.1 V -> 2.581 V (code 3204) source impedance 7.7 k, one LSB of the battery = 0.806 mV / 0.2326 = 3.46 mV. A pin with a divider whose output could exceed the supply (a battery plugged in with the board off) can be protected by a 1 k series resistor and, for a robust design, a clamp diode; for a first build, never connect the divider to the pack before checking the divider's output with a meter.
Never put more than the supply on an input
The absolute maximum on a pin is about the supply plus 0.3 V (3.3 V boards: 3.6 V). A 5 V sensor output on a 3.3 V board pin, or a divider that you built backwards, kills the pin, and sometimes the chip. Section 7 shows the level-shifting divider; for any signal that could be higher than the pin’s supply, measure it first.

5. PWM: duty cycle, average voltage and resolution

Basic Electronics 8 built PWM from a 555 and Basic Electronics 9 used it for a motor. A microcontroller makes it with a hardware timer: a counter running from the chip’s clock fclk counts from 0 up to a top value and the output is high while the counter is below a compare value. The frequency is the clock divided by the number of steps; the duty cycle is the compare value divided by the top.

Duty D = compare / top Average voltage = D x V(high) (the motor or an RC filter follows the average, Basic Electronics 9) Uno analogWrite(pin, 128): D = 128 / 255 = 50.2 % average = 0.502 x 5 V = 2.51 V analogWrite(pin, 90): D = 35.3 %: 1.76 V on a 5 V pin, or 2.12 V on a 6 V motor supply The steps, the frequency and the resolution are tied together: steps = f(clk) / f(PWM) bits = floor( log2(steps) ) Pico (125 MHz clock) at 20 kHz: 125e6 / 20e3 = 6250 steps -> 12 bits (6250 levels; the 16-bit counter has room, this is the best resolution at 20 kHz) ESP32 (80 MHz clock for the LEDC timers) at 20 kHz: 80e6 / 20e3 = 4000 steps -> 11 bits (check your board's LEDC clock; this figure is from the datasheet's usual 80 MHz) Uno Timer 1 in 8-bit phase-correct mode: 16e6 / 510 = 31.4 kHz with 255 levels (the setting used in Basic Electronics 9); the default analogWrite is 490 Hz or 980 Hz The trade-off in one line: doubling the frequency halves the number of levels. 20 kHz is the usual choice (silent) and every board offers it at 8 bits or better. PWM as a cheap digital-to-analogue converter: a resistor and a capacitor (Basic Electronics 4) average the output. R = 10 k, C = 1 uF, f(c) = 1 / (2 pi R C) = 15.9 Hz, time constant 10 ms The ripple on the capacitor (peak to peak) is approximately dV = V x D (1 - D) / (R C f) largest at D = 0.5: at 490 Hz: 5 x 0.25 / (10000 x 1e-6 x 490) = 0.255 V peak to peak (about 5 % of the supply: poor) at 31 372 Hz: 5 x 0.25 / (10000 x 1e-6 x 31372) = 3.98 mV peak to peak (less than one LSB of the Uno's ADC) So a fast PWM and a slow RC give a decent analogue output. The price is a settling time of several time constants: to settle within 0.4 % (1 part in 256) takes ln(256) = 5.5 time constants = 55 ms.

Mapping a command onto a motor

Two corrections from Basic Electronics 9 belong in any motor program: a dead zone (the motor will not move below about 18 % duty, so a command of 1 % must map to 0, and a command of 100 % to 100 %, with the rest spread linearly between 18 % and 100 %) and a ramp (the duty rises at a limited rate, such as 2 % every 20 ms, so a change from 0 to 100 % takes 1.0 s and the start-up current is not a stall pulse). For a command c between 0 and 1 above a small threshold, the duty is 18 + 82 × c per cent: half way (c = 0.5) is 59 %, which on a 4.8 V battery is an average of 0.59 × 4.8 = 2.83 V.

6. Interrupts: reacting to an edge without polling

A program that checks a pin in a loop (polling) can miss a pulse shorter than the loop. An interrupt makes the hardware stop the main program the instant a pin changes, run a short interrupt service routine (ISR), and return to exactly where it left off. It is the right tool for a pulse you must not miss: a wheel encoder, a bumper switch, a fast echo or a stop button. The cost is paid in the ISR’s run time, and in three rules: keep it short, share data through a variable declared volatile (Arduino C++) or a plain global (MicroPython), and do not print or wait inside it.

A wheel encoder: one sensor pulse per revolution of a slotted disc on the MOTOR shaft, 12 pulses per revolution, gearbox 50:1 (the gear motor of Basic Electronics 9): Motor at 6900 rpm = 115 rev/s pulse rate = 115 x 12 = 1380 interrupts per second output shaft speed = 6900 / 50 = 138 rpm An ISR of about 10 us (an Arduino's overhead plus a counter increment): CPU load = 1380 x 10 us = 1.38 % (negligible) Speed by counting in a fixed window (here 100 ms): counts in the window = 1380 x 0.1 = 138 motor rpm = counts x 60 / (pulses per rev x window) = 138 x 60 / (12 x 0.1) = 6900 rpm One count of resolution = 60 / (12 x 0.1) = 50 rpm of the motor = 1 rpm of the output shaft (the longer the window, the finer the resolution, the slower the update). A rule of thumb: count over a window that gives at least 50 to 100 counts, so one count of error is 1 to 2 %. The shared-variable trap on an 8-bit Arduino: a 16- or 32-bit counter is copied in several steps, and an ISR can change it half way. Copy it with interrupts disabled: noInterrupts(); copy = counter; interrupts(); Only the Uno's D2 and D3 are true external interrupt pins; on the Pico and the ESP32 any GPIO will do (on a Raspberry Pi the operating system adds an unpredictable delay, so an encoder counted at 1000 pulses per second is better handled by a Pico or an Arduino beside the Pi).

7. Level shifting: mixing 3.3 V and 5 V

Basic Electronics 8 introduced level shifting. In the other direction (5 V output to a 3.3 V input) you can use a resistive divider, which is the cheapest shifter and works for any signal slower than a few megahertz.

5 V signal -> 3.3 V pin: signal o-----[ R1 ]-----+-----> 3.3 V input V(out) = V(in) x R2 / (R1 + R2) | [ R2 ] | 0 V o------+ R1 = 1 k, R2 = 2 k: V(out) = 5 x 2000 / 3000 = 3.33 V (the pin's V(IH) for a 3.3 V part is about 0.7 x 3.3 = 2.3 V: passes; maximum 3.6 V: just inside) divider current = 5 V / 3 k = 1.67 mA source impedance R1 || R2 = 667 ohm speed: with 60 pF of pin and wire the time constant is 667 x 60e-12 = 40 ns, so a rise time of about 2.2 x 40 ns = 88 ns (fine to a few MHz) R1 = 1.8 k, R2 = 3.3 k: V(out) = 5 x 3.3 / 5.1 = 3.24 V, 0.98 mA, 1.16 k, slightly less current and a little more margin from the 3.6 V limit The usual use: the 5 V echo pin of an HC-SR04 ultrasonic sensor into a Raspberry Pi or Pico GPIO. 3.3 V signal -> 5 V pin: usually no shifter is needed for an Uno: its V(IH) is about 0.6 x 5 = 3.0 V, so a 3.3 V output is accepted, with only about 0.3 V of margin. If the link is noisy or the margin matters, use a logic-level shifter module (a MOSFET-based bidirectional one, Basic Electronics 8) or a 74HC-series chip powered from 5 V that has TTL-level inputs (a 74HCT part). A divider cannot go UP in voltage, and cannot shift a bidirectional line such as I2C: that needs the MOSFET module.

8. Driving a motor from a board, safely

A motor driven by a board adds three new ways to fail: the board may start up with the motor pins in an undefined state, the program may hang with the motor on, and the motor’s own supply may dip enough to reset the board mid-move (Basic Electronics 9). The remedy is not clever code but a short list of habits, each of which costs a resistor or a line of code.

  • A default of off, in hardware. Put a 100 kΩ pull-down resistor on every driver input (the PWM pin, the direction pins, and the standby pin held low means asleep), so that while the board is booting and its pins float the motor is not driven.
  • Separate supplies, common ground. The motor runs from its own battery through the driver; the board runs from USB or a regulator; the only electrical link between them is the common ground and the signal lines.
  • Start low and ramp. Power up the program with the duty at 0, and raise it by a few per cent per cycle.
  • A watchdog. Most boards have a timer that resets the chip if the program has not “fed” it recently. If the program hangs, the watchdog resets the board and the pull-downs switch the motor off. (In MicroPython: WDT(timeout=2000), then wdt.feed() every time round the loop.)
  • Watch the battery. A divider into an ADC pin (section 4) lets the program cut the motor when the pack falls below its safe limit, and a fuse in the pack lead covers what the program cannot.
  • Watch the current. A motor that stalls draws V/R (Basic Electronics 9). A small sense resistor in the motor’s ground return turns that current into a voltage the ADC can read: a 0.22 Ω resistor gives 0.22 V per amp, so the 1.09 A stall of motor M1 reads 0.24 V (297 counts on a Pico) against 0.055 V (68 counts) at the normal 0.25 A, and the program stops the motor if the reading stays above about 0.8 A (0.176 V, 218 counts) for half a second. The resistor wastes up to 0.24 V of the motor’s supply and dissipates 0.22 × 1.092 = 0.26 W at stall: use a 0.5 W part.
  • A physical way to stop. A switch in the battery lead (a mechanical relay or a plain toggle) beats any software when something goes wrong.

9. The right board for each job

JobBest boardWhy (and what the others lack)
Reading a 5 V analogue sensor (TMP36, a 5 V line sensor array) and flashing outputs; the first board to learn onArduino Uno5 V logic and 5 V ADC range match 5 V sensors without shifting; 6 ADC pins; Tinkercad Circuits simulates it well; a mistake rarely destroys it, since a 40 mA pin and a socketed chip are forgiving. The Pico and the ESP32 would need level shifting on 5 V sensors, and a Raspberry Pi has no ADC.
Motor PWM at 20 kHz with 3.3 V drivers and a clean ADC measurement of a batteryRaspberry Pi PicoPWM at 20 kHz on any pin with 12-bit resolution (an Uno needs register tricks to leave its 490 Hz default); 3.3 V matches the TB6612 and DRV8833 logic; low cost; MicroPython and Wokwi make it quick to test. Not the best choice if you need wireless or lots of ADC channels.
A robot driven from a phone, with Wi-Fi or Bluetooth built inESP32Radio on the chip, two cores (one for networking), many ADC and PWM channels. Its limits: ADC2 is unavailable while Wi-Fi is running; the ADC is noisy and nonlinear near the ends; 3.3 V only.
A camera, a screen, a database or an AI model, with a few simple switches or LEDsRaspberry PiA full Linux computer: Python libraries, networking, USB devices. Not good at hard timing (an encoder at 1000 pulses per second) or at analogue input (no ADC), so a Pico or an Arduino usually handles the motors and sensors beside it.

10. Build it: four circuits, each on a breadboard and in a simulator

Each circuit below names its board and the reason, then gives the parts, the wiring (as pin-to-pin connections, because the hole positions on your own breadboard will differ), the program and the simulator steps. Menu names in the simulators change from time to time; where I am less sure of an exact name the text says so, and searching the part list by name always works.

Before you power anything
Check the wiring against the list with the power off, then connect USB last. Use a meter to confirm every divider output is within the pin’s range before the pin is connected. Do not connect a motor pack until the program is running and the motor driver inputs read 0 V.

Circuit 1 — Arduino Uno: a TMP36 temperature reading, a debounced interrupt button and an LED

Why the Uno: the TMP36 is a 5 V analogue sensor, which suits the Uno’s 5 V supply and 5 V ADC range directly; D2 is a true interrupt pin; Tinkercad simulates this circuit with the sensor’s temperature adjustable by hand.

Parts: Arduino Uno, TMP36 (TO-92 package), push button, red LED, 330 Ω resistor, 100 nF capacitor, breadboard and jumpers.

Wiring (breadboard): TMP36, flat face towards you, pins left to right: 1 = +Vs, 2 = Vout, 3 = GND pin 1 -> Uno 5V pin 2 -> Uno A0 pin 3 -> Uno GND 100 nF between pin 1 and pin 3, close to the sensor Button: one leg -> Uno D2, the opposite leg -> Uno GND (no resistor: the program turns on the internal pull-up) LED: Uno D8 -> 330 ohm -> LED anode (long leg); LED cathode (short leg, flat side) -> Uno GND Use the breadboard's rails for 5 V and GND: Uno 5V to the top + rail, Uno GND to the top - rail. Check the TMP36 orientation first: reversed, it gets hot in seconds. Feel it with a finger; if it is hot, disconnect.
const int PIN_BUTTON = 2; const int PIN_LED = 8; const int PIN_TEMP = A0; const float VREF = 5.0; // measure the real 5 V pin with a meter and put it here for better accuracy volatile unsigned long lastPress = 0; volatile unsigned int pressCount = 0; volatile bool ledOn = false; void onButton() { unsigned long now = millis(); if (now - lastPress > 20) { // ignore bounces for 20 ms pressCount++; ledOn = !ledOn; digitalWrite(PIN_LED, ledOn); lastPress = now; } } void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BUTTON, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), onButton, FALLING); } void loop() { int code = analogRead(PIN_TEMP); float volts = code * VREF / 1024.0; float tempC = (volts - 0.5) * 100.0; noInterrupts(); // copy the shared counter safely unsigned int presses = pressCount; interrupts(); Serial.print("code "); Serial.print(code); Serial.print(" volts "); Serial.print(volts, 3); Serial.print(" tempC "); Serial.print(tempC, 1); Serial.print(" presses "); Serial.println(presses); delay(500); }

Simulator, Tinkercad Circuits (tinkercad.com, free account): create a new Circuit; from the component list drag in an Arduino Uno, a breadboard (small), a Temperature Sensor [TMP36], a pushbutton, a red LED and a resistor (set it to 330 Ω in the part’s properties). Wire as above (the sensor’s pins are labelled when you hover; the pushbutton has four legs joined in pairs, so use two diagonally opposite legs). Open Code, choose Text, paste the program, press Start Simulation and open the Serial Monitor. Click the sensor and drag its temperature slider (the range is wide, from about −40 to 125 °C) to 25 °C; click the button in the simulation.

Circuit 2 — Raspberry Pi Pico: a battery monitor, a speed knob and 20 kHz motor PWM, with a watchdog

Why the Pico: the TB6612FNG accepts 3.3 V logic, the Pico makes 20 kHz PWM on any pin with 12-bit resolution, and its ADC measures a 5 V-class battery through a divider of two equal resistors. The program is the short list of habits of section 8: an off default, a ramp, a dead-zone map, a low-battery cut-off and a watchdog.

Parts: Raspberry Pi Pico with headers, TB6612FNG module, a small 6 V hobby motor (M1 of Basic Electronics 9), 4 × AA NiMH cells in a holder (4.8 V), 2 A fast-blow fuse in the pack’s positive lead, two 10 kΩ resistors (divider), 100 nF, 100 µF, 10 kΩ potentiometer, four 100 kΩ resistors (pull-downs), a switch in the pack lead, breadboard.

Wiring (Pico pin numbers are the physical pin positions; GP numbers are the names used in the program): Pack + -> switch -> fuse -> TB6612 VM and the same point -> 10 k (R1) -> node N -> 10 k (R2) -> GND (the divider) Node N -> Pico GP26 (pin 31, ADC0); 100 nF from node N to GND, close to the Pico Pack - -> TB6612 GND and Pico GND (pin 38) (the common ground, one point) 100 uF across TB6612 VM and GND (+ leg to VM), at the module; 100 nF across the motor terminals TB6612 VCC -> Pico 3V3(OUT) (pin 36) TB6612 PWMA -> Pico GP15 (pin 20); AIN1 -> GP14 (pin 19); AIN2 -> GP13 (pin 17); STBY -> GP12 (pin 16) 100 k from each of PWMA, AIN1, AIN2 and STBY to GND (the hardware default is OFF) Motor -> TB6612 AO1 and AO2 Speed knob: pot end 1 -> Pico 3V3(OUT), pot end 2 -> GND, wiper -> GP27 (pin 32, ADC1) Power the Pico from USB. Connect the pack's switch LAST, after the program is running.
from machine import ADC, Pin, PWM, WDT import time R1 = 10000.0 # divider top resistor R2 = 10000.0 # divider bottom resistor VREF = 3.3 # the Pico's ADC reference is its 3.3 V supply CUTOFF_V = 4.4 # low-battery limit for 4 NiMH cells DEAD = 18.0 # duty (%) below which the motor does not turn STEP = 2.0 # ramp: at most 2 % per 20 ms (0 to 100 % in 1 s) batt_adc = ADC(Pin(26)) knob_adc = ADC(Pin(27)) stby = Pin(12, Pin.OUT, value=0) # asleep until we say so ain1 = Pin(14, Pin.OUT, value=0) ain2 = Pin(13, Pin.OUT, value=0) pwm = PWM(Pin(15)) pwm.freq(20000) pwm.duty_u16(0) def read_fraction(adc, samples=16): total = 0 for _ in range(samples): total += adc.read_u16() return total / samples / 65535 # 0.0 to 1.0 (full scale reads about 65520, a 0.02 % difference) def battery_volts(): pin_volts = read_fraction(batt_adc) * VREF return pin_volts * (R1 + R2) / R2 def set_duty(percent): percent = max(0.0, min(100.0, percent)) pwm.duty_u16(int(percent / 100 * 65535)) ain1.value(1) # forward: AIN1 high, AIN2 low ain2.value(0) stby.value(1) wdt = WDT(timeout=2000) # reset the board if the loop stops for 2 s speed = 0.0 tick = 0 while True: wdt.feed() batt = battery_volts() knob = read_fraction(knob_adc) if batt < CUTOFF_V or knob < 0.05: target = 0.0 else: target = DEAD + (100.0 - DEAD) * knob if target > speed: speed = min(target, speed + STEP) else: speed = max(target, speed - STEP) set_duty(speed) tick += 1 if tick % 25 == 0: # print twice a second print("battery %.2f V knob %.2f target %.0f %% duty %.0f %%" % (batt, knob, target, speed)) time.sleep_ms(20)

Simulator, Wokwi (wokwi.com, free): start a new project for the Raspberry Pi Pico with MicroPython (the starter list names may differ slightly) and replace the sample main.py with the program above. Wokwi has no TB6612 that I can count on (check the part list; if one is offered, wire it as above), so represent the driver input by an LED: from GP15 through a 330 Ω resistor to an LED and to GND, and use two potentiometers (the + button opens the parts list; the Wokwi potentiometer part has three pins, a signal pin SIG and the two ends): one on GP27 as the speed knob, one on GP26 as the divider midpoint (with the ends on 3V3 and GND, the wiper reads 0 to 3.3 V, which stands in for the 0 to 6.6 V a 10 k / 10 k divider would pass). Start the simulation and watch the terminal text under the editor. Turn the GP27 pot and the LED’s brightness follows the duty (the LED averages the 20 kHz pulses, as your eye would). If your Wokwi version offers a logic analyzer part, connect it to GP15 to see the pulse widths.

Circuit 3 — ESP32: a potentiometer commanding a hobby servo

Why the ESP32: the same circuit works with a pot today and a command from a phone over Wi-Fi tomorrow, without a second board; the 16-channel LEDC peripheral can make the 50 Hz servo pulse on almost any pin. The pot goes on an ADC1 pin (GPIO34), not an ADC2 pin, because ADC2 stops working when Wi-Fi is on. The servo’s own supply is separate, as in Basic Electronics 9.

Parts: ESP32 dev board (30- or 38-pin), SG90 or similar 9 g servo, 4 × AA NiMH holder (4.8 V), 470 µF electrolytic, 220 Ω resistor, 10 kΩ potentiometer, breadboard.

Wiring (the board's labels are printed on it: use the GPIO numbers): Servo supply: pack + -> servo red wire; pack - -> servo brown or black wire; 470 uF across pack + and pack - (the stripe to pack -), next to the servo Pack - also -> ESP32 GND (the ONLY link between the supplies, Basic Electronics 9) Servo signal (orange or yellow) -> 220 ohm -> ESP32 GPIO18 Pot: one end -> ESP32 3V3, other end -> GND, wiper -> ESP32 GPIO34 (an input-only pin on ADC1) Power the ESP32 from USB, connect the servo pack last.
from machine import ADC, Pin, PWM import time adc = ADC(Pin(34)) adc.atten(ADC.ATTN_11DB) # about 0 to 3.1 V range: nonlinear near the ends, so clamp servo = PWM(Pin(18), freq=50) def angle_to_duty(angle): angle = max(0.0, min(180.0, angle)) pulse_ms = 1.0 + angle / 180.0 # 1.0 ms = 0 deg, 2.0 ms = 180 deg return int(pulse_ms / 20.0 * 65535) def read_fraction(samples=8): total = 0 for _ in range(samples): total += adc.read_u16() return total / samples / 65535 while True: angle = read_fraction() * 180.0 servo.duty_u16(angle_to_duty(angle)) time.sleep_ms(20)

Simulator, Wokwi: start a new ESP32 project with MicroPython (the project templates may change names), add a servo and a potentiometer from the parts list, wire the servo’s signal to GPIO18, its supply pins to 5V and GND (the simulator does not need the separate pack; the real circuit does) and the potentiometer’s wiper to GPIO34, its ends to 3V3 and GND. Paste the program, start the simulation and drag the pot slider: the servo horn follows. At the middle of the pot the program calls angle_to_duty(90).

Circuit 4 — Raspberry Pi: a button, an LED and an HC-SR04 echo through a divider

Why the Raspberry Pi: it is the right board when the job needs an operating system (a web page, a camera, a screen) and a handful of simple signals. It is the wrong board for an analogue sensor (no ADC) or a precise pulse train (the operating system can delay code by a fraction of a millisecond). Its GPIO pins are 3.3 V and not 5 V tolerant, so the HC-SR04’s 5 V echo signal goes through the 1 k / 2 k divider of section 7.

Parts: any Raspberry Pi with the 40-pin header and Raspberry Pi OS, push button, red LED, 330 Ω resistor, HC-SR04 ultrasonic sensor, 1 kΩ and 2 kΩ resistors, breadboard.

Wiring (physical header pin numbers; GPIO numbers are the names the program uses): Button: one leg -> GPIO17 (pin 11), other leg -> GND (pin 9) (the program turns on the internal pull-up) LED: GPIO27 (pin 13) -> 330 ohm -> LED anode; LED cathode -> GND (pin 14) I = (3.3 - 2.0) / 330 = 3.9 mA: well inside the Pi's 16 mA per pin HC-SR04: VCC -> 5 V (pin 2); GND -> GND (pin 6); TRIG -> GPIO23 (pin 16) ECHO -> 1 k -> GPIO24 (pin 18); the junction of that 1 k and the pin also goes through 2 k to GND V(GPIO24) = 5 V x 2 / 3 = 3.33 V Never connect ECHO straight to a GPIO pin.
from gpiozero import Button, LED, DistanceSensor from signal import pause import time led = LED(27) button = Button(17, bounce_time=0.02) # internal pull-up on by default; 20 ms debounce button.when_pressed = led.toggle sensor = DistanceSensor(echo=24, trigger=23, max_distance=2.0) try: while True: print("distance %.2f m" % sensor.distance) time.sleep(0.5) except KeyboardInterrupt: pass

Simulator note: I do not know of a free browser simulator that runs Raspberry Pi OS with its GPIO, so the code is tested on the real Pi. The circuit (the divider and the LED) can be checked in Falstad (below) and the logic of the button and LED in Wokwi, which also provides a Pico and several Arduino-compatible boards, with the equivalent MicroPython calls (Pin(17, Pin.IN, Pin.PULL_UP) and Pin(27, Pin.OUT)). Do not rely on a simulator’s sensor part for the echo timing.

Falstad: the divider, the LED and the PWM filter

Falstad Circuit Simulator (falstad.com/circuit) has no microcontroller, but it checks every circuit-level calculation of this chapter. Part names are in the Draw menu (menu names may differ a little between versions; the right-click menu on a part offers its settings). Level-shift divider: a voltage source set to a square-wave (a 2-terminal source from Inputs and Sources; set the waveform to square, amplitude 2.5 V and DC offset 2.5 V, so it swings from 0 to 5 V), a 1 kΩ resistor, a 2 kΩ resistor to ground and the scope on the junction: the high level reads 3.33 V. Change 2 k to 1 k and the high level falls to 2.50 V. LED: a 3.3 V DC source, a 330 Ω resistor and a LED (Diodes group, or Outputs and Labels, depending on version): the current reads about 3.9 mA with a 2.0 V LED model (Falstad’s default LED model may drop a little more than 2.0 V, so expect 3.5 to 3.9 mA). PWM filter: a square-wave source (0 to 5 V, 490 Hz, 50 % duty, which the source’s duty setting provides), a 10 kΩ resistor, a 1 µF capacitor to ground, and the scope on the capacitor: the average is 2.5 V with a ripple of about 0.26 V peak to peak; change the frequency to 31.37 kHz and the ripple falls to about 4 mV.

Expected readings

MeasurementCalculatedReal (typical)Simulator
1: TMP36 output at 0 / 25 / 50 °C0.500 / 0.750 / 1.000 VWithin about ±2 °C (the TMP36 is accurate to about ±2 °C at 25 °C)Same
1: ADC code at 0 / 25 / 50 °C (5 V reference)102 / 153 / 204±2 counts, and the real reference may be 4.7 to 5.1 V102 / 153 / 204
1: temperature printed at those codes−0.2 / 24.7 / 49.6 °C±2 °C of the true temperature; the digits jump by 0.5 °C (one LSB)Same
1: D2 idle / pressed5 V / 0 V (internal pull-up gives 0.1 to 0.25 mA when pressed)4.9 to 5.0 V / under 0.1 V5 V / 0 V
1: LED current, D8 high9.1 mA (3.0 V across 330 Ω)8 to 9.5 mAAbout 9 mA
1: presses counted per press1 (the 20 ms debounce)1; two if the contacts are very noisy: raise the delay to 50 ms1
2: divider output at 5.6 / 4.8 / 4.4 V pack2.80 / 2.40 / 2.20 V; ADC counts 3475 / 2978 / 2730Within 20 counts (the Pico’s ADC has noise and an offset error)Whatever the pot gives
2: duty at knob 0.559 % (18 + 82 × 0.5); duty_u16 = 38 665Printed target 59 %; speed ramps up in 0.5 sLED at about 59 % brightness
2: motor average volts at knob 0.5 on 4.8 V0.59 × 4.8 = 2.83 V, minus the driver’s small drop2.6 to 2.8 V on the motor terminals (a DC range, meter averages)(no driver)
2: PWM frequency on GP1520 000 Hz; period 50 µs20 kHz on a frequency meterLogic analyzer: 20 kHz (if the part is offered)
2: battery below 4.4 VTarget 0 %, the duty ramps down to 0 in under 1 sSameSet the stand-in pot below the 2.2 V point
3: servo pulse at pot 0 / 0.5 / 1.01.0 / 1.5 / 2.0 ms; duty_u16 3277 / 4915 / 65540 / 90 / about 180 degrees ± 10 (servos vary)Horn at about 0 / 90 / 180 degrees
3: a 1.72 ms pulse129.6 degrees; duty_u16 5636——
3: ADC reading near full scale (3.3 V on the pot)Clamped by the program at 180 degreesMay read 3.1 to 3.2 V at the top because of the ADC’s attenuation; the horn may stop a few degrees shortReaches full scale
4: V(GPIO24) with a 5 V echo through 1 k / 2 k3.33 V3.2 to 3.4 V3.33 V in Falstad
4: V(GPIO24) with 1 k / 1 k2.50 V (only 0.2 V above the 2.3 V threshold)2.4 to 2.55 V: works, with little margin2.50 V
4: LED current3.9 mA3.5 to 4.0 mA3.5 to 3.9 mA
PWM filter ripple, 10 k / 1 µF, D = 0.5, 5 V0.255 V p-p at 490 Hz; 3.98 mV p-p at 31.4 kHzWithin 20 %0.25 V, 4 mV

Real readings spread because the resistors, the sensor and the supplies are all loose-tolerance parts and the ADC reference is only as steady as the supply. The structure does not vary: the code follows the pin voltage to within one or two LSB; the duty determines the average to within the driver’s drop; a pull-up holds an input high until the switch pulls it low; and a resistive divider gives the ratio of its resistors whatever the signal.

11. Challenges

Challenge 1: Pins, pull-ups and the ADC
(a) A red LED (2.1 V) is driven from an Uno pin at about 12 mA. Find the calculated resistor, choose the nearest value from 220, 270, 330, 390 Ω and find the actual current. How many such LEDs can the Uno drive at once (6 LEDs, 12 LEDs, or 20 LEDs) if the chip total is 200 mA and each pin is limited to 20 mA? (b) A button on a 3.3 V board has an external 4.7 kΩ pull-up. Find the current when pressed and the rise time with 150 pF. Compare with the internal 50 kΩ pull-up. (c) Find the LSB of an Uno (10 bit, 5 V) and of a Pico (12 bit, 3.3 V). What voltage does a reading of 300 on the Uno represent, and what temperature is that from a TMP36? What reading do you expect on the Pico for 1.65 V? (d) Design a divider to measure a 3-cell LiPo pack (12.6 V) with a Pico, using 33 kΩ and 10 kΩ: find the pin voltage at 12.6 V and 11.1 V, the codes, the source impedance and the size of one LSB at the battery.
View solution
Challenge 2: PWM and interrupts
(a) An Uno writes analogWrite(pin, 90). Find the duty cycle and the average voltage on a 5 V pin and on a 6 V motor supply. (b) For PWM at 25 kHz, find the number of steps and the resolution in bits of a Pico (125 MHz) and an ESP32 (80 MHz), and the frequency of the Uno’s Timer 1 in 8-bit phase-correct mode (16 MHz, 510 steps). (c) A PWM output at 5 V and D = 0.5 feeds an RC filter of 4.7 kΩ and 2.2 µF. Find the time constant, the ripple at 490 Hz and at 31.4 kHz, and the time to settle within 1 part in 256. (d) A motor at 9000 rpm has a 7-pulse-per-revolution encoder on its shaft and a 30:1 gearbox. Find the pulse rate, the CPU load of a 12 µs ISR, the number of counts in a 50 ms window, the motor speed represented by one count, and the output shaft speed.
View solution
Challenge 3: Level shifting, safe motors and choosing a board
(a) An HC-SR04’s 5 V echo must reach a Raspberry Pi pin. With 1.8 kΩ and 3.3 kΩ, find the pin voltage, the divider current, the source impedance and the rise time with 30 pF. Why can this divider not be used to carry a 3.3 V signal up to 5 V? (b) The 6 V motor M1 (stall current 1.09 A, running 0.25 A) has a 0.22 Ω sense resistor in its ground return. Find the voltage and the Pico ADC count at the running current, at the stall current and at the 0.8 A trip point, the current represented by one count, and the power in the resistor at stall. (c) The program of Circuit 2 uses DEAD = 18, STEP = 2 and a 20 ms loop: find the duty for a knob reading of 0.5, the average voltage on a 4.8 V pack, and the time for the duty to ramp from 0 to 100 %. (d) For each job, choose a board (Uno, Pico, ESP32 or Raspberry Pi) and give one reason: (i) a line-following robot reading five 5 V analogue sensors; (ii) a robot driven by a phone over Wi-Fi; (iii) a robot identifying objects with a camera; (iv) a motor controller at 20 kHz with a TB6612 and a battery monitor.
View solution

Quick reference

  • Pins: a pin is a small voltage source and a high-impedance sensor; budget the current per pin and in total; switch anything above a few milliamps with a transistor or driver; LED R = (Vpin − VLED)/I
  • Inputs: never leave one floating; pull-up Rp 4.7 to 10 kΩ (external) or about 50 kΩ (internal); rise time about 2.2 RpC; debounce in software or hardware
  • ADC: LSB = Vref/2N; code = floor(Vin/Vref × 2N); error ±½ LSB; Uno 4.88 mV, Pico 0.806 mV; a smaller reference gives finer steps for a small signal; source impedance 10 kΩ or less and a 100 nF at the pin
  • Divider into an ADC: k = R2/(R1 + R2), k × Vmax at most Vref; Vbat = code × LSB / k; TMP36 = 0.5 V + 10 mV/°C
  • PWM: Vavg = D × V; steps = fclk/fPWM, bits = floor(log2 steps); 20 kHz is silent; RC filter ripple V D(1 − D)/(R C f); dead zone about 18 %; ramp the duty
  • Interrupts: for pulses you must not miss; ISR short, shared variable volatile, copy with interrupts off on an 8-bit chip; load = rate × ISR time; speed = counts × 60 / (pulses per rev × window)
  • Level shifting: 5 V to 3.3 V by a divider (1 k / 2 k gives 3.33 V); 3.3 V to 5 V usually fine into an Uno (VIH about 3.0 V) or use a MOSFET module; never 5 V on a 3.3 V pin
  • Safe motors: 100 kΩ pull-downs on driver inputs (default off); separate supply and common ground; ramp from zero; watchdog; low-battery cut-off through a divider; current sense (0.22 Ω gives 0.22 V/A); fuse and a hard switch
  • Boards: Uno for 5 V sensors and learning; Pico for 3.3 V work and motor PWM; ESP32 for wireless; Raspberry Pi for software-heavy jobs (no ADC, loose timing)

Next, Basic Electronics 11 compares the Arduino, Raspberry Pi Pico, ESP32 and Raspberry Pi and others side by side (logic level, speed, I/O, power, price, wireless, ease of use), gives a table recommending the best board for each robot job, shows how to read a datasheet, covers test equipment and soldering, and wraps up the course with a recap and the path on to Robotics Fundamentals and Programming for Robotics.