dc6k.com

Devon's Collection of Assorted Projects

Software

Overview

The code for the nixie tube clock is written in AVR assembly, and assembled using Avra. Time advancement (and thus display update) is handled using a timer interrupt via Timer0, which has its clock source set to the 32768 Hz crystal. This interrupt fires every half second, which allows for blinking a colon(:) symbol if desired between the hours and minutes, although I didn't end up implementing the hardware for that. The display update subroutine is only invoked if the seconds register "overflows" (from 59 to 00) so the display only has to be updated once per minute.

SPI is used to shift data into the 74164 shift registers, after converting the hours and minutes from regular integers into binary coded decimal format. Conveniently, hours, minutes, and seconds are always one- or two-digit values, so the BCD version of the original value can still fit within a single byte (and thus a single register on the ATtiny). Due to the slow clock speed of the system and the lack of a latch on the 74164's, the display flashes seemingly random values during a display update. I actually really liked the effect it created and decided to leave it as is.

User input from the two push buttons is handled with debouncing by polling in a tight loop. A second counter is used to handle auto-repeat while the buttons are held down.

Code

avrclock2.asm

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
.nolist
.include "tn26def.inc"
.list

; Define registers
.def mpr=r16
.def callr=r17 ; Function argument value
.def tickr=r18
.def hh=r19 ; Clock registers
.def mm=r20
.def ss=r21
.def hh_db=r22 ; Debounce registers
.def mm_db=r23
.def hh_rep=r24 ; Button auto-repeat registers
.def mm_rep=r25

; Define constants
.equ debounce_ticks=70
.equ repeat_delay=255

; Configure interrupts
.org 0x0000 ; Reset
    rjmp init
.org OVF0addr ; Timer0 overflow interrupt
    rjmp half_second_tick
.org 0x000C
; Reset/init routine
init:
    ; Initialize stack pointer
    ldi mpr, RAMEND
    out SP, mpr
    
    ; Make sure time is zeroed out
    ldi hh, 0
    ldi mm, 0
    ldi ss, 0
    
    ; Initialize debounce timers and repeat delay timers
    ldi hh_db, debounce_ticks
    ldi mm_db, debounce_ticks
    ldi hh_rep, 1
    ldi mm_rep, 1
    
    ; Tick flipflop must start at zero
    ldi tickr, 0
    
    ; Configure SPI using USI module
    ; Set PB1(DO)/PB2(SCK) pins direction to output
    ldi mpr, (1<<PB1)|(1<<PB2)
    out DDRB, mpr
    ; Configure USI for two-wire mode; Note: use USICS1=1!! (See p86 in datasheet)
    ldi mpr, (0<<USIWM1)|(1<<USIWM0)|(1<<USICS1)|(0<<USICS0)|(1<<USICLK)
    out USICR, mpr
    
    ; Configure PA0/PA1 for input with pull-ups enabled
    ldi mpr, (0<<PA0)|(0<<PA1)
    out DDRA, mpr ; Input direction
    ldi mpr, (1<<PA0)|(1<<PA1)
    out PORTA, mpr ; Enable pullups

    ; Configure Timer0 to trigger an interrupt every half second
    ; Set prescaler to /64 (32768/64=512, or two overflows per second)
    ldi mpr, (1<<CS01)|(1<<CS00)
    out TCCR0, mpr
    ; Enable timer0 overflow interrupt
    ldi mpr, (1<<TOIE0)
    out TIMSK, mpr
    
    ; Initial display update
    rcall update_disp
    
    ; Enable interrupts (I-bit in status register)
    sei


;------------------------------------------------------------------------------
; Main button-handling code (non-interrupt)

; Main loop. Check for button presses here.
mainloop:
    sbic PINA, 0 ; PA0 is hours button; cleared (0) means pressed
    rcall hh_reset_debounce ; Button is NOT pressed; reset debounce timer
    sbic PINA, 1 ; PA1 is minutes button; cleared (0) means pressed
    rcall mm_reset_debounce ; Button is NOT pressed; reset debounce timer
    subi hh_db, 1
    brne mainloop2 ; Hours debounce timer hasn't reached zero -> skip to minutes-button check
    rcall hh_pressed  ; Run hours-button code when hh_db reaches zero
mainloop2:
    subi mm_db, 1
    brne mainloop ; Minutes debounce timer hasn't reached zero -> back to mainloop
    rcall mm_pressed  ; Run minutes-button code when mm_db reaches zero
    rjmp mainloop


; Subroutine to reset hours button debounce and repeat timers
hh_reset_debounce:
    ldi hh_db, debounce_ticks ; Reset hours timer
    ldi hh_rep, 1 ; Reset auto-repeat to 1
    ret
; Reset minutes button debounce and repeat timers
mm_reset_debounce:
    ldi mm_db, debounce_ticks
    ldi mm_rep, 1
    ret



; Hours button pressed. Debounce and handle
hh_pressed:
    subi hh_db, -1 ; hh_db+=1 to compensate for subtract-then-compare
    subi hh_rep, 1 ; Decrement button-repeat timer
    breq hh_pressed_action ; Advance hours when hh_rep hits zero
    ret
hh_pressed_action:
    ldi hh_rep, repeat_delay ; Reset button-repeat timer
    cli
    subi hh, -1 ; Advance hours by 1
    cpi hh, 24 ; Reset hours at 24
    brne hh_pressed_action_done
    ldi hh, 0
hh_pressed_action_done:
    rcall update_disp
    sei
    ret



; Minutes button pressed. Debounce and handle
mm_pressed:
    subi mm_db, -1 ; mm_db+=1 to compensate for subtract-then-compare
    subi mm_rep, 1 ; Decrement button-repeat timer
    breq mm_pressed_action ; Advance minutes when mm_rep hits zero
    ret
mm_pressed_action:
    ldi mm_rep, repeat_delay ; Reset button-repeat timer
    cli
    subi mm, -1 ; Advance minutes by 1
    cpi mm, 60 ; Reset minutes at 60
    brne mm_pressed_action_done
    ldi mm, 0
mm_pressed_action_done:
    rcall update_disp
    sei
    ret

;------------------------------------------------------------------------------
    
; Half-second tick interrupt handler (Can handle blinking ":" if desired)
half_second_tick:
    in mpr, SREG
    push mpr
    subi tickr, 128
    brne half_second_tick_done ; Only run code below half the time
    rcall time_adv_ss
    cpi ss, 0 ; Update display only if ss=0
    brne half_second_tick_done
    rcall update_disp
half_second_tick_done:
    pop mpr
    out SREG, mpr
    reti
    

; Advance time by one second
time_adv_ss:
    ; Add one second, check for carry
    subi ss, -1
    cpi ss, 60
    brne time_adv_done
    ldi ss, 0
    ; Add one minute, check for carry
    subi mm, -1
    cpi mm, 60
    brne time_adv_done
    ldi mm, 0
    ; Add one hour, check for overflow
    subi hh, -1
    cpi hh, 24
    brne time_adv_done
    ldi hh, 0
time_adv_done:
    ret


    
; Update display subroutine
update_disp:
    ; Convert minutes to BCD, output it
    mov callr, mm
    rcall bbcd
    rcall spi_transfer
    ; Convert hours to BCD, output it
    mov callr, hh
    rcall bbcd
    rcall spi_transfer

    ret


    
; Convert binary to BCD -- binary value in callr is changed to BCD representation
bbcd:
    ldi mpr, 0
bbcd_sub_ten_loop:  ; Check if callr is less than 10
    cpi callr, 10
    brlo bbcd_sub_ten_loop_done
    subi callr, 10 ; if not, subtract 10 until it is
    subi mpr, -1 ; ...and keep track of how many 10s we subtracted
    rjmp bbcd_sub_ten_loop
bbcd_sub_ten_loop_done:
    ; Now we have low nibble BCD value in callr, high value BCD in mpr
    ; Shift *callr* to the left, and then add the two nibbles together
    ;lsl mpr
    ;lsl mpr
    ;lsl mpr
    ;lsl mpr
    lsl callr
    lsl callr
    lsl callr
    lsl callr
	add callr, mpr
    ret

    
    
; USI-based SPI transfer routine. Sends byte in callr, returns response in callr
spi_transfer:
    ; Put USI config byte into mpr for software-clocked operation
    ldi mpr, (0<<USIWM1)|(1<<USIWM0)|(1<<USICS1)|(0<<USICS0)|(1<<USICLK)|(1<<USITC)
    ; Put byte into USI data register
    out USIDR, callr
spi_transfer_loop:
    out USICR, mpr
    sbis USISR, USIOIF ; Check if USI transfer counter has overflowed
    rjmp spi_transfer_loop
	ldi mpr, (1<<USIOIF) ; Clear USI counter overflow flag
	out USISR, mpr
    in callr, USIDR
    ret
Rendered using Pygments, the Python syntax highlighter.