TAMS / Java / Hades / applets: contents | previous | next | ||||
Hades Applets contents visual index introduction std_logic_1164 gatelevel circuits delay models flipflops adders and arithm... counters LFSR and selftest memories programmable logic state-machine editor misc. demos I/O and displays DCF-77 clock relays (switch-le... CMOS circuits (sw... RTLIB logic RTLIB registers Prima processor D*CORE MicroJava Pic16 cosimulation PIC16C84 dem... fast PIC16C8... interrupt-dr... on-chip timer EEPROM access EEPROM access RS-232 trans... software RS-232 software RS-... chronograph MIDI footswi... MIDI footswi... MIDI footswi... MIDI organ p... MIDI organ p... ultrasonic r... ultrasonic r... "Phrasendres... "mastermind"... Mips R3000 cosimu... Intel MCS4 (i4004) image processing ... [Sch04] Codeumsetzer [Sch04] Addierer [Sch04] Flipflops [Sch04] Schaltwerke [Sch04] RALU, Min... [Fer05] State-Mac... [Fer05] PIC16F84/... [Fer05] Miscellan... [Fer05] Femtojava FreeTTS | Chronograph/stopwatch controller
Circuit Description
This applet demonstrates a PIC16-based controller for a quartz chronograph
(like the Swatch I used for years, before the plastic casing broke).
Press the input switches to control the stopwatch interactively;
open the PIC user-interface to watch the program execution,
setting breakpoints, etc.
(I recommend to uncheck the 'update display' checkbox and to click
the data and program memories to enforce repainting. This saves
a lot of CPU cycles and makes the animation of the clock go smoothly).
Like many quartz-controlled chronographs, the Swatch has one main dial and three smaller dials:
The main clockwork is standard and uses a single motor that drives the seconds arm, with the minutes and hours connected by gears with the standard 1:60 and 1:12 ratios. Setting the time is achieved mechanically via rotating the crown (this is not implemented in the simulation model, and the main clock will start at 12:00:00 every time you (re)start the simulation.) The stopwatch is controlled by two switches:
Three different motors are used for the stopwatch. This can be seen on the 'real' clock when resetting the stopwatch or during resuming from an intermediate timing, because then all three stopwatch arms move simultaneusly and independently from each other. As explained above, one additional motor is used for the main clock (hours, minutes, seconds). Obviously, our software program must read the two input switches and control the four drive motors. For simplicity, we assume that stepping motors are used, where a single 0-1-0 transition advances the corresponding gear by one step. We also assume that the initial state of the motors is known, so that no additional position encoders or switches are required. The following assembly code is used for the watch:
; TITLE "Swatch chronograph controller" ; SUBTITLE "FNH 24.10.2005" ; Processor 16F84 ; Radix DEC ; EXPAND ; include "p16c84.inc" ; This program implements a controller for a quartz-controlled stopwatch ; like my old 'Swatch Chrono' (from around 1997) based on the PIC16F84. ; The program is intended as a medium complexity (and nicely animated) ; demonstration application for cosimulation in the Hades simulation ; framework. It also shows how to use the PIC timers and interrupt handling. ; ; For details about Hades visit our applet collection at ; http://tams.informatik.uni-hamburg.de/applets/hades/webdemos/toc.html ; (the demo is in the 72-pic/60-swatch subdirectory). ; ; The clock is assumed to contain four separate dials: ; 1. main dial with three arms: hours, minutes, stopwatch seconds ; 2. upper-left small dial: stopwatch minutes ; 3. upper-right small dial: stopwatch tenths of seconds ; 4. lower-centered small dial: stopwatch seconds ; The simulation model of the clock uses blue color for the main clock, ; and green for the stopwatch. ; ; We also assume that the clock uses four separate stepper-type motors; ; one to drive the seconds of the main clock (with mechanical gears to ; drive minutes and hours), and three motors to drive the three dials ; of the stopwatch. A single 0-1-0 pulse on the inputs of the clock ; simulation model steps the corresponding clock are by one position. ; ; The program uses four main states: ; a) stopwatch stopped ; b) stopwatch running and displaying (no intermediate time taken) ; c) stopwatch running, intermediate time taken, displaying intermediate time ; d) stopwatch stopped, but displaying intermediate time ; ; The on-chip timer and prescaler are programmed to result in periodic ; timer interrupts at a rate of 1/80th of a second. After the one-time ; program initialization, all calculations are performed from within ; the timer interrupt handler. ; ; The algorithm used in this program assumes that we can drive the ; stepping motors with a maximum frequency of about 100 Hz; also, we want ; to drive at most one motor at a time to reduce maximum currents. ; As a result of those restrictions, we cannot easily use wait loops ; to generate the motor pulses without either overrunning the motors ; or risking to lose interrupts. ; ; Instead, we divide the basic 1/10 second cycle of the stopwatch into ; 8 subcycles and distribute the updating of the stopwatch states and ; motor positions over the eight subcycles as follows: ; ; t 0.0 0.1 0.2 seconds ; cycle [ 0 ] [ 1 ] [ 2 ; subcycle [ 0 1 2 3 4 5 6 7 ] [ 0 1 2 3 4 5 6 7 ] [ 0 1 2 ... ; ; clock update [ C ] [ C ] [ C ; main motor [ M ] [ ] [ ; ; state update [ u ] [ u ] [ u ; tenth motor [ t t ] [ t t ] [ t ; secs motor [ s s ] [ s s ] [ s ; mins motor [ m m ] [ m m ] [ m ; ; This timing strategy ensures that at most one motor is driven at a time, ; and that the motors are driven with a frequency of 20 Hz at most. It also ; means that a state update can take up to 59steps/20Hz or 3 seconds ; - slighly slower that my Swatch, which manages this in a little over one ; second. On the other hand, this makes it easier to see the animation in ; the Hades editor, which is usually limited to about 10..20 repaints per ; seconds (unless you have a very fast computer). ; ; Given the fixed prescaler ratios of (1:2, ... 1:64, 1:128, 1:256), ; we choose a prescaler value of 1:4 and let the on-chip timer count ; continuously (0..255). This leads to an instruction cycle time of ; 1/(80*256*4) seconds = 1/81920 seconds, or an instruction cycle length ; of 1.220703125E-5 seconds. ; Note that the actual input clock frequency is 327.680 KHz, as the PIC ; needs four clock cycles per instruction. ; ; Between interrupts, the processor should wait in the sleep state ; to save power, but this doesn't work on the PIC16F84 (because the ; on-chip timer is stopped in sleep-mode). ; A real-world design could use an external toggle-flipflop based ; asynchronous counter, and trigger an PIC portB wakeup interrupt ; when the external counter overflows. ; ; Revision history: ; ; 24.10.05 - subcycle-based version ; 22.10.05 - first timer-interrupt based version ; 21.10.05 - first working state-machine ; 20.10.05 - new program ; ; (C) 2005, F.N.H., hendrich@informatik.uni-hamburg.de ; _ResetVector equ 0x00 _IntVector equ 0x04 ; ; memory locations: states, time, stopwatch time, stopwatch intermediate time, ; current motor positions ; SUBCYCLE equ 0x0f STATE equ 0x10 KEYS equ 0x11 OLDKEYS equ 0x12 DAYS equ 0x13 HOURS equ 0x14 MINS equ 0x15 SECS equ 0x16 CENTS equ 0x17 STOP_MINS equ 0x18 STOP_SECS equ 0x19 STOP_TENTHS equ 0x1a INTER_MINS equ 0x1d INTER_SECS equ 0x1e INTER_TENTHS equ 0x1f ; ; state-machine stuff. We use a one-hot encoding allow fast testing ; for states via the btfsc instruction: ; STOPPED equ 0x01 RUNNING equ 0x02 RUNNING_INTERMEDIATE equ 0x04 STOPPED_INTERMEDIATE equ 0x08 MASK_STOPPED equ 0 MASK_RUNNING equ 1 MASK_RUNNING_INTERM equ 2 MASK_STOPPED_INTERM equ 3 ; ; the current actual and target positions of the motors ; MOTOR_MINS equ 0x20 MOTOR_SECS equ 0x21 MOTOR_TENTHS equ 0x22 TARGET_MINS equ 0x25 TARGET_SECS equ 0x26 TARGET_TENTHS equ 0x27 ; ; pin assignments on port A ; PIN_START_STOP equ 0 PIN_RESET_PAUSE equ 1 ; ; pin assignments on port B ; PIN_MAIN_MOTOR equ 7 PIN_MINS_MOTOR equ 6 PIN_SECS_MOTOR equ 5 PIN_TENTHS_MOTOR equ 4 ; *************************************************************************** ; *************************************************************************** ; *************************************************************************** ; ; code starts here: this does not return ; ORG _ResetVector goto Start ; ; main interrupt vector ; ORG _IntVector goto InterruptHandler ; ; main initialization routine ; Start: movlw 0x00 movwf INTCON ; disable all interrupts movwf SUBCYCLE ; reset subcycle counter call ResetTime call ResetStopwatchTime call ResetIntermediateTime call ResetMotorValues call ResetTargetValues call InitStateMachine call InitPorts call InitTimerAndInterrupts DemoLoop: nop clrwdt ; sleep goto DemoLoop ; ; endless loop to catch errors ; Error: goto Error ; ; reset the main clock to dd/hh:mm:ss.ccc 00/00:00:00.000 ; ResetTime: movlw 0x00 movwf DAYS ; init main clock variables movwf HOURS ; movwf SECS ; movwf CENTS ; return ; ; reset the stop-watch clock to mm:ss.t 00:00.0 ; ResetStopwatchTime: movlw 0x00 movwf STOP_MINS ; init stopwatch to 0:00:00 movwf STOP_SECS ; movwf STOP_TENTHS ; return ; ; reset the stop-watch intermediate time to mm:ss:t 00:00.0 ; ResetIntermediateTime: movlw 0x00 movwf INTER_MINS ; init intermediate time movwf INTER_SECS ; movwf INTER_TENTHS ; return ; ; reset the actual motor positions to mm:ss:t 00:00.0 ; ResetMotorValues: movlw 0x00 movwf MOTOR_MINS ; init (actual) motor positions movwf MOTOR_SECS movwf MOTOR_TENTHS return ; ; reset the target motor positions to mm:ss:t 00:00.0 ; ResetTargetValues: movlw 0x00 movwf TARGET_MINS ; init target motor positions movwf TARGET_SECS movwf TARGET_TENTHS return ; ; initialize the state-machine stuff including keyboard debouncing ; InitStateMachine: movlw STOPPED movwf STATE movlw 0x00 movwf KEYS movwf OLDKEYS return ; ; initialize port PA0 and PA1 as inputs, PB7..PB4 as outputs ; InitPorts: bsf STATUS, RP0 ; access port direction regs (1=input) movlw 0x1F ; all five bits are inputs movwf TRISA ; on port A ; movlw 0x0F ; upper four bits are outputs movlw 0x00 ; all eight bits are outputs movwf TRISB ; on port B bcf STATUS, RP0 ; access data regs movlw 0x00 ; movwf PORTB ; motor controls driven low (inactive) return ; ; initialize the timer and prescaler, then enable timer interrupts ; InitTimerAndInterrupts: bsf STATUS, RP0 ; bank 1 ; movlw B'10000101' ; rtcc inc = tcylc/64 = tclk/(4*64) movlw B'10000001' ; rtcc inc = tcylc/2 = tclk/(4*2) movwf OPTION_REG ; bcf STATUS, RP0 ; bank 0 clrf TMR0 ; reset timer (and prescaler!) movlw B'10100000' ; enable timer interrupt and GIE movwf INTCON ; return; ; ; read the two input switches and debounce the input values. ; The result is a bit-mask in the KEYS register: ; 0x00 - no keypress ; 0x01 - keypress of the start-stop switch ; 0x01 - keypress of the reset-pause switch ; If both switches are pressed, only a reset-pause is reported ; ReadSwitches: movf KEYS,0 ; save keypress state from previous movwf OLDKEYS ; iteration to OLDKEYS movf PORTA,0 ; read port A andlw 0x03 ; mask lower two bits (the switches) movwf KEYS ; and save to KEYS btfsc KEYS,PIN_START_STOP ; start-stop pressed right now? goto StartStopPressed ; btfsc KEYS,PIN_RESET_PAUSE ; reset-pause pressed right now? goto ResetPausePressed return ; ; handle a keypress on the start-stop switch: ; 1) If the key was already pressed during the previous iteration, ; we return immediately. ; 2) In STOPPED, we start the stopwatch and change to RUNNING ; 3) In RUNNING or PAUSED or INTERMEDIATE we change to STOPPED ; StartStopPressed: ; start-stop pressed right now; btfsc OLDKEYS,PIN_START_STOP ; but also on previous iteration? return ; yes, ignore. ; bsf PORTB,PIN_SECS_MOTOR ; for debugging the debounce logic ; bcf PORTB,PIN_SECS_MOTOR ; return btfsc STATE,MASK_STOPPED ; state==STOPPED? goto _SSP_Running ; yes, start the stopwatch btfsc STATE,MASK_RUNNING ; state==RUNNING? goto _SSP_Stopped btfsc STATE,MASK_RUNNING_INTERM; state==RUNNING_INTERMEDIATE? goto _SSP_Stopped_Intermediate goto _SSP_Running_Intermediate _SSP_Running: ; start the stopwatch movlw RUNNING ; next state is RUNNING movwf STATE call CopyStopwatchTimeToTargetTime ; call MoveMotorsToTargetPositions return _SSP_Stopped: ; stop the stopwatch movlw STOPPED ; next state is STOPPED movwf STATE call CopyStopwatchTimeToTargetTime ; call MoveMotorsToTargetPositions return _SSP_Stopped_Intermediate: ; from running_intermediate to stopped, movlw STOPPED_INTERMEDIATE ; but keep showing intermediate time movwf STATE call CopyIntermediateTimeToTargetTime ; call MoveMotorsToTargetPositions return _SSP_Running_Intermediate: ; from stopped_intermediate: movlw RUNNING_INTERMEDIATE ; running again, but still showing the movwf STATE ; intermediate time. call CopyIntermediateTimeToTargetTime ; call MoveMotorsToTargetPositions return ; ; handle a keypress of the reset-pause switch (aka 'intermediate time' switch). ; ResetPausePressed: btfsc OLDKEYS,PIN_RESET_PAUSE ; debouncing: if pressed on previous return ; iteration return immediately ; bsf PORTB,PIN_MINS_MOTOR ; for debugging ; bcf PORTB,PIN_MINS_MOTOR ; return btfsc STATE,MASK_STOPPED ; state==STOPPED? goto _RPP_Reset ; yes, reset the stopwatch btfsc STATE,MASK_RUNNING ; state==RUNNING? goto _RPP_TakeIntermediateTime btfsc STATE,MASK_RUNNING_INTERM; state==RUNNING_INTERMEDIATE? goto _RPP_ResumeRunning goto _RPP_Stop ; state == STOPPED_INTERMEDIATE _RPP_Reset: ; we're already stopped, now reset call ResetStopwatchTime ; the stopwatch time. call CopyStopwatchTimeToTargetTime ; call MoveMotorsToTargetPositions return _RPP_TakeIntermediateTime: ; we're running, now take an movlw RUNNING_INTERMEDIATE ; intermediate time and display it movwf STATE call TakeIntermediateTime call CopyIntermediateTimeToTargetTime ; call MoveMotorsToTargetPositions return _RPP_ResumeRunning: ; resume showing the stopwatch time movlw RUNNING movwf STATE call CopyStopwatchTimeToTargetTime ; call MoveMotorsToTargetPositions return _RPP_Stop: ; move to stopped state movlw STOPPED movwf STATE call CopyStopwatchTimeToTargetTime ; call MoveMotorsToTargetPositions return ; ; increment the current (main) clock, and generate a motor-pulse every second ; IncrementTime: movf CENTS,0 ; increment cents value by 10 addlw 10 ; (use 1 for real 1/100s of seconds) movwf CENTS ; sublw 100 ; (cents==100)? btfss STATUS,Z ; return ; no. _IncrSecs: ; yes, need to increment seconds movlw 0x00 ; movwf CENTS ; reset cents value incf SECS,1 ; increments seconds movf SECS,0 ; sublw 60 ; (secs==60)? btfss STATUS,Z ; return ; no. _IncrMins: ; yes, need to increment minutes movlw 0x00 ; movwf SECS ; reset seconds incf MINS,1 ; increment minutes movf MINS,0 ; sublw 60 ; (mins==60)? btfss STATUS,Z ; return ; no. _IncrHours: ; yes, need to increment hours movlw 0x00 ; movwf MINS ; reset minutes incf HOURS,1 ; increment minutes movf HOURS,0 ; sublw 24 ; (hours==24)? btfss STATUS,Z ; return ; no. _IncrDays: ; yes, need to increment days movlw 0x00 ; movwf HOURS ; reset hours incf DAYS,1 ; increment days movf DAYS,0 ; sublw 32 ; (days==32)? FIXME: respect days/month btfss STATUS,Z ; return movlw 0x00 ; reset days movwf DAYS return ; ; increment stopwatch time mm:ss.t (by one tenth second) ; IncrementStopwatch: incf STOP_TENTHS,1 ; increment stopwatch tenths of secs movf STOP_TENTHS,0 sublw 10 ; (stop_tenths==10)? btfss STATUS,Z ; return ; no. _IncrStopSecs: ; yes, increment stopwatch secs movlw 0x00 ; movwf STOP_TENTHS ; incf STOP_SECS,1 ; movf STOP_SECS,0 ; sublw 60 ; (stop_secs==60)? btfss STATUS,Z ; return ; no. _IncrStopMins: ; yes, increment stopwatch mins movlw 0x00 ; movwf STOP_SECS ; reset seconds incf STOP_MINS,1 ; increment minutes movf STOP_MINS,0 ; sublw 60 ; (stop_mins==60)? btfss STATUS,Z ; return ; no. _ResetStopMins: movlw 0x00 ; movwf STOP_MINS ; return ; ; check the current stopwatch state. If STOPPED, do nothing, ; otherwise increment the stopwatch time (by one tenth of a second). ; CheckIncrementStopwatch: btfsc STATE,MASK_STOPPED ; state==STOPPED? return ; yes: do nothing call IncrementStopwatch ; no: increment the stopwatch time return ; ; take intermediate time (from current stopwatch time) ; TakeIntermediateTime: movf STOP_TENTHS,0 movwf INTER_TENTHS movf STOP_SECS,0 movwf INTER_SECS movf STOP_MINS,0 movwf INTER_MINS return ; ; copy stopwatch time to motor-target-time register ; CopyStopwatchTimeToTargetTime: movf STOP_TENTHS,0 movwf TARGET_TENTHS movf STOP_SECS,0 movwf TARGET_SECS movf STOP_MINS,0 movwf TARGET_MINS return ; ; copy intermediate time to motor-target-time register ; CopyIntermediateTimeToTargetTime: movf INTER_TENTHS,0 movwf TARGET_TENTHS movf INTER_SECS,0 movwf TARGET_SECS movf INTER_MINS,0 movwf TARGET_MINS return ; ; check current state to decide whether to show the current stopwatch time ; or the intermediate time on the small dials. ; CheckSetTargetPositions: btfsc STATE,MASK_RUNNING_INTERM goto CopyIntermediateTimeToTargetTime btfsc STATE,MASK_STOPPED_INTERM goto CopyIntermediateTimeToTargetTime goto CopyStopwatchTimeToTargetTime ; ; generate a pulse for the main clock motor (once every second). ; We insert a few nop instructions to ensure a minimum pulse-length. ; MainMotorPulse: bsf PORTB,PIN_MAIN_MOTOR nop nop nop bcf PORTB,PIN_MAIN_MOTOR return ; ; handle subcycle zero (where a cycle consists of eight subcycles of 1/80 secs). ; The algorithm used by this program reserves subcycle zero to increment ; the main clock time; given the timer interrupt rate of 1/80 seconds, ; this method is called every 0.1 seconds, and we have to generate ; one main motor step every ten calls. ; HandleSubcycle0: call IncrementTime ; time += 0.1 secs movf CENTS,0 ; if cents==0 subwf 0 ; btfsc STATUS,Z ; generate one motor pulse call MainMotorPulse ; return ; ; handle subcycle one (where a cycle consists of eight subcycles of 1/80 secs). ; The algorithm used by this program reserves subcycle one to increment the ; stopwatch time (if running), to check the input switches, and to handle ; the corresponding stopwatch state changes. ; No motor pulses are generated in subcycle one; any motor updates are ; deferred to subcycles 2..7. ; HandleSubcycle1: call CheckIncrementStopwatch call ReadSwitches call CheckSetTargetPositions return ; ; generate one-step for the stopwatch tenths-of-seconds-motor when necessary ; UpdateTenthsMotor: movf TARGET_TENTHS,0 ; W = target_tenths subwf MOTOR_TENTHS,0 ; W = (motor_tenths - target_tenths) btfsc STATUS,Z ; zero difference? return ; yes: return bsf PORTB,PIN_TENTHS_MOTOR ; one motor step nop nop nop bcf PORTB,PIN_TENTHS_MOTOR incf MOTOR_TENTHS,1 ; update motor position movf MOTOR_TENTHS,0 ; check for overflow sublw 10 ; btfsc STATUS,Z ; if (motor_tenths==10): clrf MOTOR_TENTHS return ; ; generate one-step for the stopwatch seconds-motor when necessary ; UpdateSecsMotor: movf TARGET_SECS,0 ; W = target_secs subwf MOTOR_SECS,0 ; W = (motor_secs - target_secs) btfsc STATUS,Z ; zero difference? return ; yes: return bsf PORTB,PIN_SECS_MOTOR ; one motor step nop nop nop bcf PORTB,PIN_SECS_MOTOR incf MOTOR_SECS,1 ; update motor position movf MOTOR_SECS,0 ; check for overflow sublw 60 ; btfsc STATUS,Z ; if (motor_secs==60): clrf MOTOR_SECS ; then motor_secs=0 return ; ; generate one-step for the stopwatch minutes-motor when necessary ; UpdateMinsMotor: movf TARGET_MINS,0 ; W = target_mins subwf MOTOR_MINS,0 ; W = (motor_mins - target_mins) btfsc STATUS,Z ; zero difference? return ; yes: return bsf PORTB,PIN_MINS_MOTOR ; one motor step nop nop nop bcf PORTB,PIN_MINS_MOTOR incf MOTOR_MINS,1 ; update motor position movf MOTOR_MINS,0 ; check for overflow sublw 60 ; btfsc STATUS,Z ; if (motor_mins==60): clrf MOTOR_MINS return ; ; main interrupt handler starts here. ; ; We first increment the subcycle counter variable (0..7) and then ; dispatch to the action corresponding to the subcycles. ; The extra bsf/bcf instructions are inserted for debugging; just ; add a signal probe to PORT B.3 and use the waveform viewer to ; watch the time spent inside the interrupt handler. ; InterruptHandler: clrf INTCON ; clear all interrupts bsf PORTB,3 ; for debugging and cycle counting incf SUBCYCLE ; increment subcycle movlw b'00000111' ; lower 3 bits mask andwf SUBCYCLE,1 ; subcycle AND 00000111 movf SUBCYCLE,0 ; subcycle==0? sublw 0x0 ; btfsc STATUS,Z ; call HandleSubcycle0 ; yes, increment main clock movf SUBCYCLE,0 ; subcycle==1? sublw 0x1 ; btfsc STATUS,Z ; call HandleSubcycle1 ; yes, update stopwatch movf SUBCYCLE,0 ; subcycle==2? sublw 0x2 ; btfsc STATUS,Z ; call UpdateTenthsMotor ; yes, tenths-of-seconds motor update movf SUBCYCLE,0 ; subcycle==3? sublw 0x3 ; btfsc STATUS,Z ; call UpdateSecsMotor ; yes, seconds motor update movf SUBCYCLE,0 ; subcycle==4? sublw 0x4 ; btfsc STATUS,Z ; call UpdateMinsMotor ; yes, minutes motor update movf SUBCYCLE,0 ; subcycle==5? sublw 0x5 ; btfsc STATUS,Z ; call UpdateTenthsMotor ; yes, tenths-of-seconds motor update movf SUBCYCLE,0 ; subcycle==6? sublw 0x6 ; btfsc STATUS,Z ; call UpdateSecsMotor ; yes, seconds motor update movf SUBCYCLE,0 ; subcycle==7? sublw 0x7 ; btfsc STATUS,Z ; call UpdateMinsMotor ; yes, minutes motor update bcf PORTB,3 ; for debugging on B.3 bsf INTCON,T0IE ; enable timer interrupt RTIE again retfie ; return (and enable GIE again) END
| |||
Print version | Run this demo in the Hades editor (via Java WebStart) | ||||
Usage | FAQ | About | License | Feedback | Tutorial (PDF) | Referenzkarte (PDF, in German) | ||||
Impressum | http://tams.informatik.uni-hamburg.de/applets/hades/webdemos/72-pic/60-swatch/swatch.html |