A Mechanical Watch Keeps Time Because the Escapement Is a Sampled Control Loop
Every mechanical movement splits into two subsystems that fail for different reasons. The power train stores and delivers energy. The escapement regulates how f...
The Power Train Moves Energy; It Does Not Keep Time
Every mechanical movement splits into two subsystems that fail for different reasons. The power train stores and delivers energy. The escapement regulates how fast that energy leaves. Read any teardown of a modern calibre and the same split shows up, and it is the split, not the gear cutting, that makes the thing keep time.
Start at the barrel. A coiled mainspring stores potential energy; as it unwinds, the barrel gear turns. That rotation is far too slow and too weak to drive a seconds hand once per minute. The train multiplies speed across four wheels: barrel, center wheel, third wheel, fourth wheel. Each large wheel drives a small pinion mounted on the next shaft. A big driver turning a small pinion multiplies speed; the inverse arrangement reduces it.
package movement
// Pair is one wheel/pinion mesh in the going train.
type Pair struct {
Name string
Wheel int // teeth on the large gear
Pinion int // teeth on the pinion it drives
}
// Figures for a generic 4 Hz Swiss lever. The barrel makes one turn
// roughly every six hours when fully wound.
var goingTrain = []Pair{
{"barrel->center", 72, 12},
{"center->third", 64, 8},
{"third->fourth", 60, 8},
}
// FourthWheelRPM returns the rotation rate of the fourth wheel, which
// carries the seconds hand directly on its extended arbor.
func FourthWheelRPM(barrelRPM float64) float64 {
rpm := barrelRPM
for _, p := range goingTrain {
if p.Pinion == 0 {
return 0
}
rpm *= float64(p.Wheel) / float64(p.Pinion)
}
return rpm
}
// HandIsCalibrated asserts the train produces one turn per minute.
func HandIsCalibrated() bool {
const barrelPeriodHours = 6.0
barrelRPM := 1.0 / (barrelPeriodHours * 60)
got := FourthWheelRPM(barrelRPM) // 360x -> 1.000 rpm
return got > 0.99 && got < 1.01
}
The ratios multiply to 360, so a barrel turning once per six hours yields a fourth wheel turning once per minute. That wheel passes straight through the center of the movement; the seconds hand mounts on the far end of its arbor. The minute hand runs off the center wheel through a separate reduction, and the hour hand off a further 12:1 stage.
Two engineering constraints fall out of the train. First, torque scales inversely with speed: the barrel turns forcefully, but by the escape wheel the available torque is small. That is deliberate. A forceful escape wheel would hammer the pallet fork and push the balance with far more energy than it needs, destroying the regularity the whole mechanism depends on.
Second, the wheels are drilled with holes. The escape wheel must accelerate from rest to full speed inside the impulse window, a few milliseconds per beat. Moment of inertia scales with mass at a given radius, so removing material from the rim is the cheapest way to let the spring accelerate the wheel fast enough. Those holes are a latency budget, not decoration.
The train is a gearbox with a job: convert a slow, strong, unregulated torque into a fast, weak, precisely timed motion. It is not the clock.
The Escapement Is a Sampled Control Loop
The train is a loaded spring waiting to discharge. Left alone it would unwind in seconds and the rate would be whatever friction allowed. The escapement converts that continuous torque into discrete, metered releases. It is a sampled control system: the escape wheel advances one tooth per half-oscillation of the balance, and nothing moves in between.
Two parts do the work. The escape wheel carries club-shaped teeth, unlike any gear in the train. The pallet fork is a T-shaped lever with a ruby stone at each end. The stones alternately catch a tooth and release it. Synthetic ruby is used because it is hard, so it wears slowly, and it has a low coefficient of friction against steel, so little energy is lost at the contact. Both properties set the loop's deadband.
Each beat runs through three phases. In lock, a pallet stone holds an escape wheel tooth and the train is stopped. In impulse, the balance's roller jewel trips the fork, the stone slides off the tooth, and the escape wheel pushes the fork, which pushes the balance back. That push replaces the energy the balance lost to friction since the last beat. In drop, the other stone catches the next tooth and the train stops again.
package escapement
const secondsPerHour = 3600
// BeatRate returns vibrations per hour for a balance oscillating at
// balanceHz full cycles per second. Each full cycle contains two
// vibrations, and each vibration releases one escape wheel tooth.
func BeatRate(balanceHz float64) int {
return int(2 * balanceHz * secondsPerHour)
}
// ImpulseDuty is the fraction of a beat during which the escape wheel
// is actually transferring torque. liftAngle is the pallet lift (52
// degrees on a typical Swiss lever); amplitude is the balance swing.
func ImpulseDuty(liftAngleDeg, amplitudeDeg float64) float64 {
if amplitudeDeg == 0 {
return 0
}
return liftAngleDeg / (2 * amplitudeDeg)
}
// 4 Hz -> 28800 vph
// 52 deg lift, 300 deg amplitude -> duty ~0.087
A 4 Hz balance produces 28,800 vibrations per hour, eight beats per second. With a 52-degree lift and 300-degree amplitude, the train is under power for only about 8.7 percent of each beat. For the rest of the cycle the balance coasts and the escape wheel is locked.
That sampling rate is a design decision, not a spec sheet number. A 3 Hz movement (21,600 vph) samples the balance position less often per second and is more sensitive to positional error; a 5 Hz movement (36,000 vph) samples finer but wears pivots faster and drains the mainspring sooner. The 4 Hz standard is the point where rate stability, service life, and power reserve balance. Choose the sample rate to fit the disturbance you must reject, and pay for it in wear.
The Balance Wheel Is an Oscillator With a Quality Factor
The regulator is a mass on a spring. The balance wheel is a weighted ring; the hairspring is a flat spiral that supplies the restoring force. Left to swing freely, the pair is a harmonic oscillator with a natural frequency set by the spring's stiffness and the wheel's inertia. The escapement only replaces the energy lost to friction each beat, the same way a hand pushes a swing to keep it moving.
What sets accuracy is the quality factor, Q: how much energy the oscillator stores versus how much it loses per cycle. High Q means the rate is insensitive to disturbance, because a nudge barely shifts the phase. A quartz crystal has Q in the tens of thousands. A conventional balance and hairspring pair sits between 100 and 300.
package oscillator
import "math"
// QualityFactor for a damped harmonic oscillator: Q = 2*pi*f / (2*gamma),
// where gamma is the amplitude decay rate in 1/s.
func QualityFactor(freqHz, decayPerSec float64) float64 {
if decayPerSec <= 0 {
return math.Inf(1)
}
return 2 * math.Pi * freqHz / (2 * decayPerSec)
}
// AmplitudeDeg after t seconds with no escapement impulse, assuming the
// balance is released from a0 and decays as exp(-gamma*t).
func AmplitudeDeg(a0, decayPerSec, t float64) float64 {
return a0 * math.Exp(-decayPerSec*t)
}
// A 4 Hz balance released at 270 degrees with gamma = 0.5/s is down to
// 135 degrees in about 1.4 s. The escapement pushes every 125 ms.
Amplitude is the health signal a watchmaker reads first. A fully wound movement in good condition swings 270 to 300 degrees. Below 200 degrees the movement is losing energy to friction and needs service, not adjustment. That number is the direct readout of how much impulse the escapement is still delivering.
The architecture lesson here is that the balance never powers anything. It times; the train moves. The moment you couple the oscillator to the load, accuracy starts depending on the load. Zenith's Defy Lab takes the decoupling further by replacing the balance and hairspring with a single monolithic silicon flexure that oscillates without pivots, without sliding contact at the escapement, and without lubricant. It reports a quality factor more than ten times a conventional escapement and a theoretical accuracy near 0.3 seconds per day, while still counting nothing electronically.
The Rate Error Budget: Position, Temperature, Magnetism
A watch does not have one rate. It has a distribution of rates across the conditions it will actually see. COSC certifies a movement only if its average daily rate stays within −4/+6 seconds per day across five positions and three temperatures (8 °C, 23 °C, 38 °C). Each term in that window is a physical effect you can name and bound.
- Positional error: gravity loads the balance pivots differently dial-up than crown-down. Amplitude shifts, and with it the rate.
- Temperature: the hairspring's elastic modulus changes with heat. A hot spring is softer, the balance slows, and the rate drops. Nivarox alloys compensate; silicon hairsprings hold a near-zero thermoelastic coefficient from −10 °C to +40 °C.
- Magnetism: a steel hairspring magnetised above roughly 50 gauss has adjacent coils stick together, shortening its effective length and driving the rate fast. A heavy case runs 10 to 30 minutes per day fast. Silicon is diamagnetic to about 1,000 gauss.
- Isochronism error: as the mainspring unwinds, the torque reaching the escapement falls, the balance swing narrows, and the rate shifts.
package chrono
import "fmt"
// RateBudget holds per-source contributions to daily rate error in
// seconds per day. Positive means the watch gains.
type RateBudget struct {
Regulation float64 // fixed offset from the last service
Positional float64 // worst-case spread across five positions
Thermal float64 // -10C to +40C swing
Magnetism float64 // residual magnetisation of the hairspring
Isochronism float64 // drift as mainspring torque falls
}
func (b RateBudget) Total() float64 {
return b.Regulation + b.Positional + b.Thermal + b.Magnetism + b.Isochronism
}
// WithinCOSC reports whether the summed budget fits -4/+6 s/day.
func (b RateBudget) WithinCOSC() bool {
t := b.Total()
return t >= -4 && t <= 6
}
func Report(b RateBudget) string {
return fmt.Sprintf("total %.1f s/day (COSC -4/+6): ok=%v", b.Total(), b.WithinCOSC())
}
Two diagnostics separate the failure modes. A regulation error is consistent day to day: the watch gains the same amount every day. Magnetism is erratic and position-dependent, because gravity decides which coils stick. Beat error, the asymmetry between the tick and the tock, should stay under 0.6 milliseconds; above 2.0 the movement has a mechanical fault, not a rate adjustment problem.
One practical control-theory detail: a watchmaker regulates slightly fast, to about +2 to +5 seconds per day, because the rate slows as the power reserve drops. That is a deliberate setpoint bias against a known drift direction, cheaper than adding a mechanism to correct it.
What Software Engineers Should Take From the Movement
A two-century-old mechanism is worth reading for the same reason any good architecture is: the responsibilities are separated by failure mode. Energy delivery and rate regulation are different subsystems with different failure signatures, and the design keeps them from coupling.
- Continuous input, discrete output. The mainspring delivers continuous torque; the escapement turns it into a fixed-frequency event stream. If your system has a clock, it should have exactly one authority for it.
- Sample rate is a budget. 28,800 vph is not simply more accurate; it is a trade against pivot wear and power reserve. Pick the rate that rejects the disturbance you actually face.
- Bias the setpoint against known drift. Watchmakers regulate fast because the rate falls as the spring unwinds. If you know the direction a system drifts, bias the setpoint instead of bolting on a controller.
- Keep the oscillator off the load. The balance times but never moves anything. Coupling regulation to delivery is how you get a system whose accuracy depends on its load.
The separation is worth writing down as an interface boundary, because the boundary is what makes the two halves independently testable.
package movement
// PowerSource delivers torque. It knows nothing about time.
type PowerSource interface {
TorqueNm() float64
}
// Regulator decides when energy is released. It knows nothing about
// where the energy comes from, only the frequency it must hold.
type Regulator interface {
BeatHz() float64
ReleaseTeeth(n int) bool
}
// Movement is the composition. Neither side imports the other's
// details; the escape wheel is the only shared surface between them.
type Movement struct {
Power PowerSource
Governor Regulator
}
The gear train can be tested in isolation by asserting that its ratios produce the expected hand speeds. The escapement can be tested by asserting that its beat rate matches the balance frequency and that impulse duty stays inside the range that keeps the balance supplied. Neither test needs the other subsystem to exist.
That is the part worth stealing. A watch keeps time not because its gears are precise, but because one subsystem was given the single responsibility of deciding when energy moves, and nothing else was allowed to decide it.