Systemy wbudowane w praktyce

tech. Michał Gabor

Celem naszej zabawy jest

licznik

copyright by dwz under CC-BY 2.0

BOM, czyli co kupić na obiad

ATmega 8A, czyli:

• High-performance, Low-power Atmel AVR 8-bit Microcontroller
• Advanced RISC Architecture
– 130 Powerful Instructions – Most Single-clock Cycle Execution
– 32 x 8 General Purpose Working Registers
– Fully Static Operation
– Up to 16 MIPS Throughput at 16MHz
– On-chip 2-cycle Multiplier
• High Endurance Non-volatile Memory segments
– 8KBytes of In-System Self-programmable Flash program memory
– 512Bytes EEPROM
– 1KByte Internal SRAM
– Write/Erase Cycles: 10,000 Flash/100,000 EEPROM
– Data retention: 20 years at 85°C/100 years at 25°C
– Optional Boot Code Section with Independent Lock Bits
• In-System Programming by On-chip Boot Program
• True Read-While-Write Operation
– Programming Lock for Software Security
• Peripheral Features
– Two 8-bit Timer/Counters with Separate Prescaler, one Compare Mode
– One 16-bit Timer/Counter with Separate Prescaler, Compare Mode, and Capture Mode
– Real Time Counter with Separate Oscillator
– Three PWM Channels
– 8-channel ADC in TQFP and QFN/MLF package

ATmega 8A, czyli:

• Eight Channels 10-bit Accuracy
– 6-channel ADC in PDIP package
• Six Channels 10-bit Accuracy
– Byte-oriented Two-wire Serial Interface
– Programmable Serial USART
– Master/Slave SPI Serial Interface
– Programmable Watchdog Timer with Separate On-chip Oscillator
– On-chip Analog Comparator
• Special Microcontroller Features
– Power-on Reset and Programmable Brown-out Detection
– Internal Calibrated RC Oscillator
– External and Internal Interrupt Sources
– Five Sleep Modes: Idle, ADC Noise Reduction, Power-save, Power-down, and Standby
• I/O and Packages
– 23 Programmable I/O Lines
– 28-lead PDIP, 32-lead TQFP, and 32-pad QFN/MLF
• Operating Voltages
– 2.7 - 5.5V
• Speed Grades
– 0 - 16MHz
• Power Consumption at 4Mhz, 3V, 25 C
– Active: 3.6mA
– Idle Mode: 1.0mA
– Power-down Mode: 0.5μ

Jak to wszystko połączyć?

Rezystor do diody

Przycisk

Wyświetlacz

Podłączenie do mikrokontrolera

      segmenty wyświetlacza
   
      przyciski
   
      cyfry (bazy tranzystorów)
   
      programowanie

Kodzimy

Pusty projekt

Tworzymy main.c

int main()
{
	while(1); //nie chcemy, by program się skończył
}

Tworzymy Makefile

CC=/usr/bin/avr-gcc
CFLAGS=-g -Os -Wall -mcall-prologues -mmcu=atmega8a

program : prog.hex
	sudo avrdude -p atmega8a -c usbasp -U flash:w:prog.hex

%.obj : main.o
	$(CC) $(CFLAGS) $< -o $@

%.hex : %.obj
	/usr/bin/avr-objcopy -R .eeprom -O ihex $< $@

Jak działa make?

Jeśli pliku nie ma, lub jest starszy, to jest uruchamiana reguła na jego utworzenie.

Kolejność

  1. main.c
  2. main.o
  3. prog.obj
  4. prog.hex
  5. program - nie jest tworzony, więc ta reguła wykona się zawsze

W praktyce

$ sudo pacman -S avr-libc avrdude #tylko raz, bo to jest instalacja środowiska
$ vim main.c
$ make

Rejestr

Rejestry portów GPIO

General Purpose Input Output

  1. DDRx - ustalenie kierunku działania portu
  2. PORTx - ustawienie podciągania/wartości
  3. PINx - odczytanie napięcia na pinach
DDRx PORTx znaczenie
0 0 wejście - stan wysokiej impedancji (domyślne)
0 1 wejście z włączonym podciąganiem
1 0 wyjście - stan niski
1 1 wyjście - stan wysoki

Hello world!

Ustawmy porty w odpowiedni stan i nadajmy im wartości

Pamiętajmy o zapalaniu zerem!

Cyfry to piny PC3-PC0

Segmenty to piny PD6-PD0

#include <avr/io.h> //by korzystać z rejestrów

int main()
{
	DDRC  = 0b00001111; //wyjścia cyfr
	PORTC = 0b00001010; //druga i czwarta cyfra
	DDRD  = 0b01111111; //wyjścia segmentów
	PORTD = 0b00000010; //cyfra 6
	while(1);
}

Skąd mamy wartość na 6?

zapalamy zerem

pin 6 5 4 3 2 1 0
segment G F E D C B A
wartość 0 0 0 0 0 1 0

stąd mamy:

PORTD = 0b00000010; //cyfra 6

Chcemy różne cyfry!

Ale czy to możliwe?

Odpowiedź to multipleksowanie

Licznik

Licznik może liczyć impulsy z:

Licznik może liczyć niektóre impulsy z zegara i kwarcu

Licznik - tryby

Przy przepełnieniu

Co wybrać?

Chcemy zapalać segment za segmentem

Chcemy uruchamiać jakiś kod kilkaset razy na sekundę

Licznik - wybór trybu i Timera

Wybieramy impulsy z zegara, CTC i przerwanie

Załóżmy, że chcemy 500Hz: 250 x 8 x 500 = 1000000

Częstotliwość 500Hz z 1MHz osiągniemy, zliczając 250 impulsów z preskalerem 1/8

Timery

Użyjmy Timera 1

Ustawienie preskalera

za datasheetem firmy ATMEL

Zapalamy bit CS11

Ustawienie CTC

za datasheetem firmy ATMEL

Zapalamy bit WGM12

Ustawienie TCCR1B - makro _BV() i dodawanie bitów

Chcąc ustawić bit WGM12 nie możemy napisać:

TCCR1B = WGM12; //źle

bo WGM12 to numer bitu (WGM12 = 3 = 0b00000011 - zapalimy pierwszy i zerowy bit)

Dodatkowo chcemy ustawić kilka bitów - użyjmy dodawania bitowego:

TCCR1B = _BV(WGM12) | _BV(CS11);

Kod

#include <avr/io.h>
#include <avr/interrupt.h> //korzystamy z przerwań

//tu zmienne globalne

int main()
{
	DDRC  = 0b00001111;
	PORTC = 0b00000111; //zapal pierwszą cyfrę
	DDRD  = 0b01111111;
	PORTD = 0b00000010;
	
	TCCR1B = _BV(WGM12) | _BV(CS11); //CTC 1/8
	TIMSK  = _BV(OCIE1A); //włącz przerwanie Timera 1
	OCR1A  = 250; //odlicz 250 impulsów
	
	sei(); //włącz przerwania
	
	while(1);
}

ISR(TIMER1_COMPA_vect)
{
	//przerwanie
}

Przerwanie

Wyświetl następną cyfrę

ISR(TIMER1_COMPA_vect)
{
	switch(PORTC)
	{
		case 0b00000111: //pierwsza
			PORTC = 0b00001011; //druga
			break;
		case 0b00001011: //druga
			PORTC = 0b00001101; //trzecia
			break;
		case 0b00001101: //trzecia
			PORTC = 0b00001110; //czwarta
			break;
		default:
			PORTC = 0b00000111; //pierwsza
			break;
	}
}

Wyświetlmy inną liczbę

Tworzymy zmienne globalne

#include <avr/io.h>
#include <avr/interrupt.h>

//tu zmienne globalne
int cyfry[] = {0x40, 0x79, 0x24, 0x30, 0x19, 0x12, 0x02, 0x78, 0x00, 0x10, 0x06, 0x2F};
int liczba[4] = {1, 3, 3, 7};

int main() //i dalej program...

Tworzymy dwie tablice:

Przerwanie

Wyświetlamy następną cyfrę

ISR(TIMER1_COMPA_vect)
{	
	switch(PORTC)
	{
		case 0b00000111: //gdy pierwsza
			PORTC = 0b00001111; //wygaś wszystko
			PORTD = cyfry[liczba[1]];
			//wyświetl drugą (od lewej) cyfrę liczby
			//1, bo liczymy od zera
			PORTC = 0b00001011;
			break;
		//...
		//w innych tak samo
	}
}

Druga cyfra = 1

liczba[1] = jaka cyfra jest druga, np. 3 w 1337

cyfry[liczba[1]] = kształt cyfry, np. zapal segmenty A, C, E i F

A co się stanie, gdy częstotliwość przerwania zmniejszymy do ...?

2Hz

OCR1A = 62500;

10Hz

OCR1A = 12500;

50Hz

OCR1A =  2500;

200Hz

OCR1A =   625;

Mieliśmy robić licznik

Brakuje nam obsługi przycisku. Będziemy wykrywać zbocze malejące, tj. gdy przycisk przed chwilą był puszczony, a teraz jest naciśnięty

ISR(TIMER1_COMPA_vect)
{
	switch(PORTC)
	{ /* ... */ }
	
	if(lewy == 0 && bit_is_clear(PINB, 1))
	{
		lewy = 1; //możemy też zaincludować stdbool.h
		liczba[0] = 0;
		liczba[1] = 0;
		liczba[2] = 0;
		liczba[3] = 6; //wyświetl 0006
	}
	
	if(bit_is_set(PINB, 1))
	{
		lewy = 0;
	}
}

Dodanie obsługi przycisku

Dodajmy zmienne globalne

int cyfry[] = {0x40, 0x79, 0x24, 0x30, 0x19, 0x12, 0x02, 0x78, 0x00, 0x10, 0x06, 0x2F};
int liczba[4] = {1, 3, 3, 7};

int lewy  = 0;
int prawy = 0;

Musimy też włączyć podciąganie

int main()
{
	PORTB = 0b00000011; //podciąganie na przyciskach
	//...
	//dalszy kod

Prawy przycisk

analogicznie

if(prawy == 0 && bit_is_clear(PINB, 0))
{
	prawy = 1;
	liczba[0] = 1;
	liczba[1] = 3;
	liczba[2] = 3;
	liczba[3] = 7; //wyświetl 1337
}

if(bit_is_set(PINB, 0))
{
	prawy = 0;
}

Reset

Resetowanie licznika jest już prawie gotowe. Wystarczy ustawiać same zera

if(lewy == 0 && bit_is_clear(PINB, 1))
	{
		lewy = 1;
		liczba[0] = 0;
		liczba[1] = 0;
		liczba[2] = 0;
		liczba[3] = 0;
	}

Zmieńmy też domyślną wartość licznika

int liczba[4] = {0, 0, 0, 0};

Inkrementacja

if(++liczba[n] > 9)
{
	liczba[n] = 0;
	//zwiększ dla n-1
}
+1
19
+1
20

Inkrementacja

if(prawy == 0 && bit_is_clear(PINB, 0))
{
	prawy = 1;
	if(++liczba[3] > 9)
	{
		liczba[3] = 0;
		if(++liczba[2] > 9)
		{
			liczba[2] = 0;
			if(++liczba[1] > 9)
			{
				liczba[1] = 0;
				if(++liczba[0] > 9)
				{
					liczba[0] = 0;
					//niech się zawinie do 0000
				}
			}
		}
	}
}

Koniec, ale

Program jest skończony, jednak warto takie rzeczy pisać lepiej

#define PRZYC C
#define LEWY 1

// Makra upraszczające dostęp do portów
#define GLUE(a, b)     a##b
#define PORT(x)        GLUE(PORT, x)
#define PIN(x)         GLUE(PIN, x)
#define DDR(x)         GLUE(DDR, x)

//i potem
DDR(PRZYC) &= ~(_BV(LEWY));
PORT(PRZYC) |= _BV(LEWY);

zamiast

DDRC = 0b00000000;
PORTC = 0b00000010;

Koniec

copyleft under M6PL by matma6 http://matma6.net

/

#