344 lines
9.6 KiB
NASM
344 lines
9.6 KiB
NASM
|
; This is a copy of my seg7multiplex main program, translated for zasm.
|
||
|
; The output of zasm was verified against avra's.
|
||
|
|
||
|
; 7-segments multiplexer for an ATtiny45
|
||
|
;
|
||
|
; Register usage
|
||
|
; R0: Digit on AFF1 (rightmost, QH on the SR)
|
||
|
; R1: Digit on AFF2 (QG on the SR)
|
||
|
; R2: Digit on AFF3 (QF on the SR)
|
||
|
; R3: Digit on AFF4 (leftmost, QE on the SR)
|
||
|
; R5: always zero
|
||
|
; R6: generic tmp value
|
||
|
; R16: generic tmp value
|
||
|
; R18: value to send to the SR. cleared at every SENDSR call
|
||
|
; in input mode, holds the input buffer
|
||
|
; R30: (low Z) current digit being refreshed. cycles from 0 to 3
|
||
|
;
|
||
|
; Flags on GPIOs
|
||
|
; GPIOR0 - bit 0: Whether we need to refresh the display
|
||
|
; GPIOR0 - bit 1: Set when INT_INT0 has received a new bit
|
||
|
; GPIOR0 - bit 2: The value of the new bit received
|
||
|
; GPIOR0 - bit 4: input mode enabled
|
||
|
|
||
|
; Notes on register usage
|
||
|
; R0 - R3: 4 low bits are for digit, 5th bit is for dot. other bits are unused.
|
||
|
;
|
||
|
; Notes on AFF1-4
|
||
|
; They are reversed (depending on how you see things...). They read right to
|
||
|
; left. That means that AFF1 is least significant, AFF4 is most.
|
||
|
;
|
||
|
; Input mode counter
|
||
|
; When in input mode, TIMER0_OVF, instead of setting the refresh flag, increases
|
||
|
; the counter. When it reaches 3, we timeout and consider input invalid.
|
||
|
;
|
||
|
; Input procedure
|
||
|
;
|
||
|
; Input starts at INT_INT0. What it does there is very simple: is sets up a flag
|
||
|
; telling it received something and conditionally sets another flag with the
|
||
|
; value of the received bit.
|
||
|
;
|
||
|
; While we do that, we have the input loop eagerly checking for that flag. When
|
||
|
; it triggers, it records the bit in R18. The way it does so is that it inits
|
||
|
; R18 at 1 (not 0), then for every bit, it left shifts R18, then adds the new
|
||
|
; bit. When the 6th bit of R18 is set, it means we have every bit we need, we
|
||
|
; can flush it into Z.
|
||
|
|
||
|
; Z points directly to R3, then R2, then R1, then R0. Because display refresh
|
||
|
; is disabled during input, it won't result in weird displays, and because
|
||
|
; partial numbers result in error display, then partial result won't lead to
|
||
|
; weird displays, just error displays.
|
||
|
;
|
||
|
; When input mode begins, we change Z to point to R3 (the first digit we
|
||
|
; receive) and we decrease the Z pointer after every digit we receive. When we
|
||
|
; receive the last bit of the last digit and that we see that R30 is 0, we know
|
||
|
; that the next (and last) digit is the checksum.
|
||
|
|
||
|
.inc "avr.h"
|
||
|
.inc "tn254585.h"
|
||
|
.inc "tn45.h"
|
||
|
|
||
|
; pins
|
||
|
.equ RCLK 0 ; on PORTB
|
||
|
.equ SRCLK 3 ; on PORTB
|
||
|
.equ SER_DP 4 ; on PORTB
|
||
|
.equ INSER 1 ; on PORTB
|
||
|
|
||
|
; Let's begin!
|
||
|
|
||
|
.org 0x0000
|
||
|
RJMP MAIN
|
||
|
RJMP INT_INT0
|
||
|
RETI ; PCINT0
|
||
|
RETI ; TIMER1_COMPA
|
||
|
RETI ; TIMER1_OVF
|
||
|
RJMP INT_TIMER0_OVF
|
||
|
|
||
|
MAIN:
|
||
|
LDI R16, RAMEND&0xff
|
||
|
OUT SPL, R16
|
||
|
LDI R16, RAMEND}8
|
||
|
OUT SPH, R16
|
||
|
|
||
|
SBI DDRB, RCLK
|
||
|
SBI DDRB, SRCLK
|
||
|
SBI DDRB, SER_DP
|
||
|
|
||
|
; we generally keep SER_DP high to avoid lighting DP
|
||
|
SBI PORTB, SER_DP
|
||
|
|
||
|
; target delay: 600us. At 1Mhz, that's 75 ticks with a 1/8 prescaler.
|
||
|
LDI R16, 0x02 ; CS01, 1/8 prescaler
|
||
|
OUT TCCR0B, R16
|
||
|
LDI R16, 0xb5 ; TOP - 75 ticks
|
||
|
OUT TCNT0, R16
|
||
|
|
||
|
; Enable TIMER0_OVF
|
||
|
IN R16, TIMSK
|
||
|
ORI R16, 0x02 ; TOIE0
|
||
|
OUT TIMSK, R16
|
||
|
|
||
|
; Generate interrupt on rising edge of INT0
|
||
|
IN R16, MCUCR
|
||
|
ORI R16, 0b00000011 ; ISC00 + ISC01
|
||
|
OUT MCUCR, R16
|
||
|
IN R16, GIMSK
|
||
|
ORI R16, 0b01000000 ; INT0
|
||
|
OUT GIMSK, R16
|
||
|
|
||
|
; we never use indirect addresses above 0xff through Z and never use
|
||
|
; R31 in other situations. We can set it once and forget about it.
|
||
|
CLR R31 ; high Z
|
||
|
|
||
|
; put 4321 in R2-5
|
||
|
CLR R30 ; low Z
|
||
|
LDI R16, 0x04
|
||
|
ST Z+, R16 ; 4
|
||
|
DEC R16
|
||
|
ST Z+, R16 ; 3
|
||
|
DEC R16
|
||
|
ST Z+, R16 ; 2
|
||
|
DEC R16
|
||
|
ORI R16, 0b00010000 ; DP
|
||
|
ST Z, R16 ; 1
|
||
|
CLR R30 ; replace Z to 0
|
||
|
|
||
|
SEI
|
||
|
|
||
|
LOOP:
|
||
|
RCALL INPT_CHK ; verify that we shouldn't enter input mode
|
||
|
SBIC GPIOR0, 0 ; refesh flag cleared? skip next
|
||
|
RCALL RDISP
|
||
|
RJMP LOOP
|
||
|
|
||
|
; ***** DISPLAY *****
|
||
|
|
||
|
; refresh display with current number
|
||
|
RDISP:
|
||
|
; First things first: setup the timer for the next time
|
||
|
LDI R16, 0xb5 ; TOP - 75 ticks
|
||
|
OUT TCNT0, R16
|
||
|
CBI GPIOR0, 0 ; Also, clear the refresh flag
|
||
|
|
||
|
; Let's begin with the display selector. We select one display at once
|
||
|
; (not ready for multi-display refresh operations yet). Let's decode our
|
||
|
; binary value from R30 into R16.
|
||
|
MOV R6, R30
|
||
|
INC R6 ; we need values 1-4, not 0-3
|
||
|
LDI R16, 0x01
|
||
|
RDISP1:
|
||
|
DEC R6
|
||
|
BREQ RDISP2 ; == 0? we're finished
|
||
|
LSL R16
|
||
|
RJMP RDISP1
|
||
|
|
||
|
; select a digit to display
|
||
|
; we do so in a clever way: our registers just happen to be in SRAM
|
||
|
; locations 0x00, 0x01, 0x02 and 0x03. Handy eh!
|
||
|
RDISP2:
|
||
|
LD R18, Z+ ; Indirect load of Z into R18 then increment
|
||
|
CPI R30, 4
|
||
|
BRCS RDISP3 ; lower than 4 ? don't reset
|
||
|
CLR R30 ; not lower than 4? reset
|
||
|
|
||
|
; in the next step, we're going to join R18 and R16 together, but
|
||
|
; before we do, we have one thing to process: R18's 5th bit. If it's
|
||
|
; high, it means that DP is highlighted. We have to store this
|
||
|
; information in R6 and use it later. Also, we have to clear the higher
|
||
|
; bits of R18.
|
||
|
RDISP3:
|
||
|
SBRC R18, 4 ; 5th bit cleared? skip next
|
||
|
INC R6 ; if set, then set R6 as well
|
||
|
ANDI R18, 0xf ; clear higher bits
|
||
|
|
||
|
; Now we have our display selector in R16 and our digit to display in
|
||
|
; R18. We want it all in R18.
|
||
|
SWAP R18 ; digit goes in high "nibble"
|
||
|
OR R18, R16
|
||
|
|
||
|
; While we send value to the shift register, SER_DP will change.
|
||
|
; Because we want to avoid falsely lighting DP, we need to disable
|
||
|
; output (disable OE) while that happens. This is why we set RCLK,
|
||
|
; which is wired to OE too, HIGH (OE disabled) at the beginning of
|
||
|
; the SR operation.
|
||
|
;
|
||
|
; Because RCLK was low before, this triggers a "buffer clock" on
|
||
|
; the SR, but it doesn't matter because the value that was there
|
||
|
; before has just been invalidated.
|
||
|
SBI PORTB, RCLK ; high
|
||
|
RCALL SENDSR
|
||
|
; Flush out the buffer with RCLK
|
||
|
CBI PORTB, RCLK ; OE enabled, but SR buffer isn't flushed
|
||
|
NOP
|
||
|
SBI PORTB, RCLK ; SR buffer flushed, OE disabled
|
||
|
NOP
|
||
|
CBI PORTB, RCLK ; OE enabled
|
||
|
|
||
|
; We're finished! Oh no wait, one last thing: should we highlight DP?
|
||
|
; If we should, then we should keep SER_DP low rather than high for this
|
||
|
; SR round.
|
||
|
SBI PORTB, SER_DP ; SER_DP generally kept high
|
||
|
SBRC R6, 0 ; R6 is cleared? skip DP set
|
||
|
CBI PORTB, SER_DP ; SER_DP low highlight DP
|
||
|
|
||
|
RET ; finished for real this time!
|
||
|
|
||
|
; send R18 to shift register.
|
||
|
; We send highest bits first so that QH is the MSB and QA is the LSB
|
||
|
; low bits (QD - QA) control display's power
|
||
|
; high bits (QH - QE) select the glyph
|
||
|
SENDSR:
|
||
|
LDI R16, 8 ; we will loop 8 times
|
||
|
CBI PORTB, SER_DP ; low
|
||
|
SBRC R18, 7 ; if latest bit isn't cleared, set SER_DP high
|
||
|
SBI PORTB, SER_DP ; high
|
||
|
RCALL TOGCP
|
||
|
LSL R18 ; shift our data left
|
||
|
DEC R16
|
||
|
BRNE SENDSR+2 ; not zero yet? loop! (+2 to avoid reset)
|
||
|
RET
|
||
|
|
||
|
; toggle SRCLK, waiting 1us between pin changes
|
||
|
TOGCP:
|
||
|
CBI PORTB, SRCLK ; low
|
||
|
NOP ; At 1Mhz, this is enough for 1us
|
||
|
SBI PORTB, SRCLK ; high
|
||
|
RET
|
||
|
|
||
|
; ***** INPUT MODE *****
|
||
|
|
||
|
; check whether we should enter input mode and enter it if needed
|
||
|
INPT_CHK:
|
||
|
SBIS GPIOR0, 1 ; did we just trigger INT_INT0?
|
||
|
RET ; no? return
|
||
|
; yes? continue in input mode
|
||
|
|
||
|
; Initialize input mode and start the loop
|
||
|
INPT_BEGIN:
|
||
|
SBI GPIOR0, 4 ; enable input mode
|
||
|
CBI GPIOR0, 1 ; The first trigger was an empty one
|
||
|
|
||
|
; At 1/8 prescaler, a "full" counter overflow is 2048us. That sounds
|
||
|
; about right for an input timeout. So we co the easy route and simply
|
||
|
; clear TCNT0 whenever we want to reset the timer
|
||
|
OUT TCNT0, R5 ; R5 == 0
|
||
|
CBI GPIOR0, 0 ; clear refresh flag in case it was just set
|
||
|
LDI R30, 0x04 ; make Z point on R3+1 (we use pre-decrement)
|
||
|
LDI R18, 0x01 ; initialize input buffer
|
||
|
|
||
|
; loop in input mode. When in input mode, we don't refresh the display, we use
|
||
|
; all our processing power to process input.
|
||
|
INPT_LOOP:
|
||
|
RCALL INPT_READ
|
||
|
|
||
|
; Check whether we've reached timeout
|
||
|
SBIC GPIOR0, 0 ; refesh flag cleared? skip next
|
||
|
RCALL INPT_TIMEOUT
|
||
|
|
||
|
SBIC GPIOR0, 4 ; input mode cleared? skip next, to INPT_END
|
||
|
RJMP INPT_LOOP ; not cleared? loop
|
||
|
|
||
|
INPT_END:
|
||
|
; We received all our date or reached timeout. let's go back in normal
|
||
|
; mode.
|
||
|
CLR R30 ; Ensure Z isn't out of bounds
|
||
|
SBI GPIOR0, 0 ; set refresh flag so we start refreshing now
|
||
|
RET
|
||
|
|
||
|
; Read, if needed, the last received bit
|
||
|
INPT_READ:
|
||
|
SBIS GPIOR0, 1
|
||
|
RET ; flag cleared? nothing to do
|
||
|
|
||
|
; Flag is set, we have to read
|
||
|
CBI GPIOR0, 1 ; unset flag
|
||
|
LSL R18
|
||
|
SBIC GPIOR0, 2 ; data flag cleared? skip next
|
||
|
INC R18
|
||
|
|
||
|
; Now, let's check if we have our 5 digits
|
||
|
SBRC R18, 5 ; 6th bit cleared? nothing to do
|
||
|
RCALL INPT_PUSH
|
||
|
|
||
|
OUT TCNT0, R5 ; clear timeout counter
|
||
|
|
||
|
RET
|
||
|
|
||
|
; Push the digit currently in R18 in Z and reset R18.
|
||
|
INPT_PUSH:
|
||
|
ANDI R18, 0b00011111 ; Remove 6th bit flag
|
||
|
|
||
|
TST R30 ; is R30 zero?
|
||
|
BREQ INPT_CHECKSUM ; yes? it means we're at checksum phase.
|
||
|
|
||
|
; Otherwise, its a regular digit push
|
||
|
ST -Z, R18
|
||
|
LDI R18, 0x01
|
||
|
RET
|
||
|
|
||
|
INPT_CHECKSUM:
|
||
|
CBI GPIOR0, 4 ; clear input mode, whether we error or not
|
||
|
MOV R16, R0
|
||
|
ADD R16, R1
|
||
|
ADD R16, R2
|
||
|
ADD R16, R3
|
||
|
; only consider the first 5 bits of the checksum since we can't receive
|
||
|
; more. Otherwise, we couldn't possibly validate a value like 9999
|
||
|
ANDI R16, 0b00011111
|
||
|
CP R16, R18
|
||
|
BRNE INPT_ERROR
|
||
|
RET
|
||
|
|
||
|
INPT_TIMEOUT:
|
||
|
CBI GPIOR0, 4 ; timeout reached, clear input flag
|
||
|
; continue to INPT_ERROR
|
||
|
|
||
|
INPT_ERROR:
|
||
|
LDI R16, 0x0c ; some weird digit
|
||
|
MOV R0, R16
|
||
|
MOV R1, R16
|
||
|
MOV R2, R16
|
||
|
MOV R3, R16
|
||
|
RET
|
||
|
|
||
|
; ***** INTERRUPTS *****
|
||
|
|
||
|
; Record received bit
|
||
|
; The main loop has to be fast enough to process that bit before we receive the
|
||
|
; next one!
|
||
|
; no SREG fiddling because no SREG-modifying instruction
|
||
|
INT_INT0:
|
||
|
CBI GPIOR0, 2 ; clear received data
|
||
|
SBIC PINB, INSER ; INSER clear? skip next
|
||
|
SBI GPIOR0, 2 ; INSER set? record this
|
||
|
SBI GPIOR0, 1 ; indicate that we've received a bit
|
||
|
RETI
|
||
|
|
||
|
; Set refresh flag whenever timer0 overflows
|
||
|
; no SREG fiddling because no SREG-modifying instruction
|
||
|
INT_TIMER0_OVF:
|
||
|
SBI GPIOR0, 0
|
||
|
RETI
|
||
|
|
||
|
|