Informatyka 
 
Częstościomierz - program
Narcyz
Częstościomierz cyfrowy to nie tylko odpowiednio połączone obwody elektroniczne, ale także program. Do tego program na tyle prosty, że warto go na spokojnie przeanalizować. Możemy go także potraktować jako punkt wyjścia do oprogramowania innych funkcjonalności. Niestety, program będzie w assemblerze, języku uznawanym za trudny bo niskopoziomowy. Ale jak inaczej zrozumieć działalnie procesora?

Na początek warto opisać zastosowanie poszczególnych rejestrów, oraz wejść i wyjść. To ważne, bo bez tego trudno się będzie zorientować co do czego służy. Taki komentarz przyda się przy analizie kodu a i ewentualne modyfikacje nie powinny być potem problemem.

;-------------------------------------------------------------------------------

		.include "m8adef.inc"	; Atmega8

;-------------------------------------------------------------------------------

; ------------------------------------
; register usage:
; ----------------
;   r16, z, - common purpose registers, to be used for current calculation and 
;		  passing parameters to subroutines. Usually changed after return 
;   r17-18	- common use registers
;   r0-5	- display content
;   r6		- currently displayed digit
;   r7-9	- number to dispaly

; ------------------------------------
; pinout:
; ----------------
; PB0 - 
; PB1 - display 4
; PB2 - time gate output
; PB3 - programmer
; PB4 - programmer
; PB5 - programmer
; PB6 - xtal
; PB7 - xtal
; PC0 - display A
; PC1 - display B
; PC2 - display C
; PC3 - display D
; PC4 - display E
; PC5 - display F
; PC6 - reset - resistor 10k to Vcc
; PD0 - display 3
; PD1 - display 2
; PD2 - display 1
; PD3 - display 0
; PD4 - pulse input
; PD5 - display 5
; PD6 - display G
; PD7 - display H

;-------------------------------------------------------------------------------
W pamięci RAM –czyli pamięci przeznaczonej na dane, rezerwujemy miejsce na stos wywołań podprogramów. Stos będzie też używany do zapamiętywania stanu rejestrów. To ważne, bo w przerwaniach nie powinniśmy zmodyfikować żadnych rejestrów które mogą być używane.
		.dseg
		.equ	stack_size = 128
stack:		.byte	stack_size
Kolejnym elementem programu jest wektor przerwań. Ta tablica skoków określa instrukcje które są wykonane w momencie w którym zostaje wywołane odpowiednie przerwanie. Oczywiście jedyną sensowną instrukcją jest instrukcja skoku w miejsce w którym odpowiedni podprogram jest zapisane. Skoki takie są konieczne tylko dla tych przerwań, które są używane. W naszym przypadku – używamy tylko trzech przerwań pochodzących od wewnętrznych zegarów
;-------------------------------------------------------------------------------

		.cseg
		.org	0

		; interupt vector
		rjmp	i_reset
		reti		; rjmpi_ext_int0
		reti		; rjmpi_ext_int1
		reti		; rjmpi_tim2_comp
		rjmp	out_next_digit
		reti		; rjmpi_tim1_capt
		reti		; rjmpi_tim1_compa
		rjmp	timer_int
		reti		; rjmpi_tim1_ovf
		rjmp	counter_int
		reti		; rjmpi_spi_stc
		reti		; rjmpi_usart_rxc
		reti		; rjmpi_usart_udre
		reti		; rjmpi_usart_txc
		reti		; rjmpi_adc
		reti		; rjmpi_ee_rdy
		reti		; rjmpi_ana_comp
		reti		; rjmpi_twsi
		reti		; rjmpi_spm_rdy

;-------------------------------------------------------------------------------
Główna część programu składa się z inicjalizacji stosu, licznika oraz wyświetlacza. Po zakończeniu inicjalizacji – program wpada w nieskończoną pętlę – wszystko co program będzie robił – będzie robił w przerwaniach.
i_reset:	; Initialize whole program. Setup all internal periferials
		;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		cli

		; init stack
		ldi	zh, high(stack+stack_size-1)
		ldi	zl, low(stack+stack_size-1)
		out	SPH, zh
		out	SPL, zl

		rcall	init_display
		rcall	init_counter
		rcall	init_timer

		sei

loop:		; Main loop of processor - just doing nothing because everything
		; is handled in interuptes
		;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		nop
		nop
		nop

l1:		dec	r18
		brne	l1
		dec	r17
		brne	l1

		rjmp	loop

;-------------------------------------------------------------------------------
Teraz pora na przygotowanie wyświetlania znaków na wyświetlaczach siedmiosegmentowych. Ze względu na małą liczbę wyprowadzeń procesora, katody wszystkich wyświetlaczy są połączone razem i przyłączone do ośmiu wyprowadzeń. Anody – są sterowane przez tranzystory włączane przez sześć innych wyjść procesora. W jednym momencie świecą segmenty tylko jednego wyświetlacza, a program dba o to by przełączać aktywny wyświetlacz co 1ms. Zajmuje się tym podprogram wyzwalany przerwaniem związanym z przepełnieniem licznika T2.

Inicjując wyświetlacz musimy przełączyć odpowiednie porty tak by pracowały jako wyjścia, włączyć przerwania i ustawić licznik T2 tak by pracował z 1/64 częstotliwości zegara procesora.

Podczas wyświetlania niestety musimy wykonać parę dodatkowych operacji. Oprócz sprawdzenia który znak powinien być obecnie wyświetlany, i oprócz włączenia i wyłączenia sygnału na odpowiedniej anodzie, sam znak musimy przesłać na wyjścia które są częściami dwóch różnych portów. Niestety wyjścia procesora mają dodatkowe punkcje których używamy i nie dysponujemy całym żadnym całym portem.

;-------------------------------------------------------------------------------

init_display:	; changes r16
	
		ldi	r16, DDRC
		ori	r16, $3f
		out 	DDRC, r16

		ldi	r16, DDRD
		ori	r16, $c0
		out	DDRD, r16

		sbi	DDRD, 3
		sbi	DDRD, 2
		sbi	DDRD, 1
		sbi	DDRD, 0
		sbi	DDRB, 1
		sbi	DDRD, 5

		sbi	PORTD, 3
		sbi	PORTD, 2
		sbi	PORTD, 1
		sbi	PORTD, 0
		sbi	PORTB, 1
		sbi	PORTD, 5

		ldi	r16, $04
		out	TCCR2, r16
		in	r16, TIMSK
		andi	r16, $3f
		ori	r16, $40
		out	TIMSK, r16

		ret

out_next_digit:	; changes noting - interupt
	
		push r16
		in	r16, SREG
		push	r16
		push	r17
		push	r18

		mov	r16, r6
		cpi	r16, 0
		breq	out_1_digit
		cpi	r16, 1
		breq	out_2_digit
		cpi	r16, 2
		breq	out_3_digit
		cpi	r16, 3
		breq	out_4_digit
		cpi	r16, 4
		breq	out_5_digit
				
out_6_digit:	sbi	PORTB, 1
		mov	r16, r5
		rcall	out_digit
		cbi	PORTD, 5
		ldi	r16, $ff
		mov	r6, r16
		rjmp	out_end

out_5_digit:	sbi	PORTD, 0
		mov	r16, r4
		rcall	out_digit
		cbi	PORTB, 1
		rjmp	out_end

out_4_digit:	sbi	PORTD, 1
		mov	r16, r3
		rcall	out_digit
		cbi	PORTD, 0
		rjmp	out_end

out_3_digit:	sbi	PORTD, 2
		mov	r16, r2
		rcall	out_digit
		cbi	PORTD, 1
		rjmp	out_end

out_2_digit:	sbi	PORTD, 3
		mov	r16, r1
		rcall	out_digit
		cbi	PORTD, 2
		rjmp	out_end

out_1_digit:	sbi	PORTD, 5
		mov	r16, r0
		rcall	out_digit
		cbi	PORTD, 3

out_end:	inc	r6		

		pop	r18
		pop	r17
		pop	r16
		out	SREG, r16
		pop	r16
		reti

out_digit:	; input r16 - bit image; active: 0
		; changes r17,r18

		in	r17, PORTC
		andi	r17, $c0
		mov	r18, r16
		andi	r18, $3f
		or	r18, r17
		out	PORTC, r18

		in	r17, PORTD
		andi	r17, $3f
		mov	r18, r16
		andi	r18, $c0
		or	r18, r17
		out	PORTD, r18

		ret

;-------------------------------------------------------------------------------
Wyświetlanie wymaga wartości przygotowanych dla wyświetlacza siedmiosegmentowego. Dlatego warto przygotować procedurę która zamieni numer znaku na odpowiednie zapalone i zgaszone segmenty wyświetlacza
char_views:	.db	0b00010010, 0b11010111, 0b00011100, 0b01010100 ; 0123
		.db	0b11010001, 0b01110000, 0b00110000, 0b11010110 ; 4567
		.db	0b00010000, 0b01010000, 0b11101111, 0b11111101 ; 89.-
		.db	0b00111000, 0b10111101, 0b00110101, 0b11111111 ; Ero
		.db	0b00111011, 0b10010001                         ; LH

get_char_view:	; changes z(r30,r31)
		; input r16 - char number
		; output r16 - bit image; active:0

		ldi	zl, low(char_views * 2)
		ldi	zh, high(char_views * 2)
		add	zl, r16
		ldi	r16, 0
		adc	zh, r16
		lpm	r16, z
		ret
Teraz trzeba przygotować procedurę która zmienili liczbę na ciąg cyfr. Nie jest to wbrew pozorom prosta operacja, bo procesor z jakim mamy do czynienia nie pozwala na wykonywanie dzielenia, a ponadto będziemy mieli do czynienia ze stosunkowo długimi liczbami. Jeśli chcemy wyświetlać liczby sześciocyfrowe – do ich zapisy będziemy używali 20 bitów, a więc aż trzy rejestry.

Operacja konwersji jest skomplikowana jeśli chodzi o ilość kodu, ale tak naprawdę jest bardzo prosta. Zamiast dzielić, będziemy sprawdzali czy liczba jest większa od pewnej stałej, i jeśli jest, to ową stałą będziemy odejmowali. Jeśli wykonamy ta operację dla stałej wynoszącej odpowiednio 800000, 400000, 200000, 10000, to uzyskamy cztery bity cyfry oznaczającej setki tysięcy. Musimy tylko taką liczbę rozłożyć na bajty, i sprawa wydaje się prosta. Jeśli wynik będzie mały – wystarczy operować dwoma bajtami, a dla dwóch najmniej znaczących liczb – będziemy operowali pojedynczym bajtem.

Ta procedura jest skuteczna, pod warunkiem że wejściowa liczba jest mniejsza niż milion. Dlatego warto sprawdzić to na początki i jeśli nie jest – wyświetlić informację o błędzie

;-------------------------------------------------------------------------------

convert_number:	; converts number from r7,8,9 to digits from r0,1,2,3,4,5
		; changes r16-23, z(r30,31)

		mov	r21, r7
		mov	r22, r8
		mov	r23, r9
		ldi	r18, 64
		ldi	r19, 66
		ldi	r20, 15
		rcall	compare_3
		brlo	convert_3

show_error:	; changes r16,z(r30,r31)

		ldi	r16, $0f
		rcall	get_char_view
		mov	r5, r16
		ldi	r16, $0c
		rcall	get_char_view
		mov	r4, r16
		ldi	r16, $0d
		rcall	get_char_view
		mov	r3, r16
		ldi	r16, $0d
		rcall	get_char_view
		mov	r2, r16
		ldi	r16, $0e
		rcall	get_char_view
		mov	r1, r16
		ldi	r16, $0d
		rcall	get_char_view
		mov	r0, r16

		ret

convert_3:	ldi	r16, 0		; digit 6
		ldi	r17, 8
		ldi	r18, 0
		ldi	r19, 53
		ldi	r20, 12
		rcall	check_bit_3
		ldi	r17, 4
		ldi	r18, 128
		ldi	r19, 26
		ldi	r20, 6
		rcall	check_bit_3
		ldi	r17, 2
		ldi	r18, 64
		ldi	r19, 13
		ldi	r20, 3
		rcall	check_bit_3
		ldi	r17, 1
		ldi	r18, 160
		ldi	r19, 134
		ldi	r20, 1
		rcall	check_bit_3
		cpi	r16, 0
		brne	convert_a
		ldi	r16, $0f
convert_a:	rcall	get_char_view
		mov	r5, r16

		ldi	r16, 0		; digit 5
		ldi	r17, 8
		ldi	r18, 128
		ldi	r19, 56
		ldi	r20, 1
		rcall	check_bit_3
		ldi	r17, 4
		ldi	r18, 64
		ldi	r19, 156
		ldi	r20, 0
		rcall	check_bit_3
		ldi	r17, 2
		ldi	r18, 32
		ldi	r19, 78
		rcall	check_bit_2
		ldi	r17, 1
		ldi	r18, 16
		ldi	r19, 39
		rcall	check_bit_2
		rcall	get_char_view
		rcall	add_dot
		mov	r4, r16

		ldi	r16, 0		; digit 4
		ldi	r17, 8
		ldi	r18, 64
		ldi	r19, 31
		rcall	check_bit_2
		ldi	r17, 4
		ldi	r18, 160
		ldi	r19, 15
		rcall	check_bit_2
		ldi	r17, 2
		ldi	r18, 208
		ldi	r19, 7
		rcall	check_bit_2
		ldi	r17, 1
		ldi	r18, 232
		ldi	r19, 3
		rcall	check_bit_2
		rcall	get_char_view
		mov	r3, r16

		ldi	r16, 0		; digit 3
		ldi	r17, 8
		ldi	r18, 32
		ldi	r19, 3
		rcall	check_bit_2
		ldi	r17, 4
		ldi	r18, 144
		ldi	r19, 1
		rcall	check_bit_2
		ldi	r17, 2
		ldi	r18, 200
		ldi	r19, 0
		rcall	check_bit_2
		ldi	r17, 1
		ldi	r18, 100
		rcall	check_bit_1
		rcall	get_char_view
		mov	r2, r16

		ldi	r16, 0		; digit 2
		ldi	r17, 8
		ldi	r18, 80
		rcall	check_bit_1
		ldi	r17, 4
		ldi	r18, 40
		rcall	check_bit_1
		ldi	r17, 2
		ldi	r18, 20
		rcall	check_bit_1
		ldi	r17, 1
		ldi	r18, 10
		rcall	check_bit_1
		rcall	get_char_view
		rcall	add_dot
		mov	r1, r16

		mov	r16, r21
		rcall	get_char_view
		mov	r0, r16
				
		ret

add_dot:	mov	r17, r16
		ldi	r16, 10
		rcall	get_char_view
		and	r16, r17
		ret

check_bit_3:	; compares r18,19,20 with r21,22,23 and if not less - 
		; subtracts r16.. from r19... and adds r17 to r16

		rcall	compare_3
		brlo	cmp_end
		sub	r21, r18
		sbc	r22, r19
		sbc	r23, r20
		add	r16, r17
		ret

check_bit_2:	; compares r18,19 with r21,22 and if not less - 
		; subtracts r16.. from r19... and adds r17 to r16

		rcall	compare_2
		brlo	cmp_end
		sub	r21, r18
		sbc	r22, r19
		add	r16, r17
		ret

check_bit_1:	; compares r18 with r21 and if not less - 
		; subtracts r16.. from r19... and adds r17 to r16

		cp	r21, r18
		brlo	cmp_end
		sub	r21, r18
		add	r16, r17
		ret

compare_3:	; compares r18,19,20 with r21,22,23

		cp	r23, r20
		brne	cmp_end
compare_2:	cp	r22, r19
		brne	cmp_end
		cp	r21, r18
cmp_end:		ret

;-------------------------------------------------------------------------------
Zamo zliczanie impulsów zrealizujemy na liczniku T0, którego wejście jest dostępne jako alternatywna funkcja jednego z portów. Licznik ten potrafi liczyć do 256, ale przy przepełnieniu może zgłaszać przepełnienie jako przerwanie. W tym przerwaniu wystarczy zwiększać rejestry które potraktujemy jako kolejne bajty licznika. Musimy tylko pamiętać, że w przerwaniu nie wolno nam zmienić stanu procesora, a operacje zwiększania wartości rejestru – modyfikują znaczniki stanu. Dlatego musimy je na początku zapamiętać na stosie.
init_counter:	; initialises external counter realized on timer/counter T0
		; storing upper bytes in r10,11
		; changes r16

		cbi	DDRD, 4
		cbi	PORTD, 4

		ldi	r16, $06
		out	TCCR0, r16
		in	r16, TIMSK
		ori	r16, $01
		out	TIMSK, r16

counter_reset:	; changes r16, resets all bytes of count

		ldi	r16, 0
		out	TCNT0, r16
		mov	r10, r16
		mov	r11, r16
		ret

counter_int:	; nothing changes - interupt

		push	r16
		in	r16, SREG
		push	r16
		inc	r10
		brne	cint_01
		inc	r11
cint_01:	pop	r16
		out	SREG, r16
		pop	r16
		reti
Odczyt licznika i zamiana jego wartości na liczbę jest tylko złożeniem procedur które są gotowe. Pamiętajmy tylko, że pierwszy bajt – jest w rejestrze licznika.
counter_read:	; changes r16-23, z(r30,31)
		; sets r7..9 acording to counted value
		; sets r0..5 to display apropriate digits

		in	r16, TCNT0
		mov	r17, r10
		or	r17, r11
		brne	cr_01
		cpi	r16, 200
		brsh	cr_01

		ldi	r16, 0
		mov	r7, r16
		mov	r8, r16
		mov	r9, r16
		rjmp	convert_number

cr_01:		mov	r7, r16
		mov	r8, r10
		mov	r9, r11
		rjmp	convert_number
Teraz wystarczy odliczenie odpowiedniego czasu na liczniku T1. Zastosowano tu tryb generowania fali PWM, przy czym dla nas istotny jest czas trwania stanu wysokiego – użytego do bramkowania sygnału wejściowego. Przy zmianie stanu – zgłaszane przerwanie czyta stan licznika, przekazuje go to wyświetlania, oraz kasuje licznik przygotowując go do kolejnego cyklu zliczania.
;-------------------------------------------------------------------------------

init_timer:	; changes r16

		ldi	r16, $23
		out	TCCR1A, r16
		ldi	r16, $1d
		out 	TCCR1B, r16

		ldi	r16, 9
		push	r16
		out	OCR1BH, r16
		ldi	r16, 196
		out	OCR1BL, r16
		pop	r16
		inc	r16
		inc	r16
		out	OCR1AH, r16
		ldi	r16, 0
		out	OCR1AL, r16
				
		ldi	r16, $ff
		out	TCNT1H, r16
		out	TCNT1H, r16

		sbi	DDRB, 2

		in	r16, TIMSK
		ori	r16, $08
		out	TIMSK, r16

		ret

timer_int:	; nothing changes - interupt

		push	r16
		in	r16, SREG
		push	r16

		push	r17
		push	r18
		push	r19
		push	r20
		push	r21
		push	r22
		push	r23
				
		rcall	counter_read
		rcall	counter_reset

		pop	r23
		pop	r22
		pop	r21
		pop	r20
		pop	r19
		pop	r18
		pop	r17

		pop	r16
		out	SREG, r16
		pop	r16
		reti

;-------------------------------------------------------------------------------

 
Opinie
 
Facebook
 
  
2193 wyświetlenia

numer 8/2017
2017-08-03

Od redakcji
Dydaktyka
Felieton
Film
Informatyka
Matematyka
Polityka
Rozmaitości
Sztuka życia

nowyOlimp.net na Twitterze

nowy Olimp - internetowe czasopismo naukowe dla młodzieży.
Kolegium redakcyjne: gaja@nowyolimp.net; hefajstos@nowyolimp.net