Už je to tak 2 roky, kdy jsem si řekl, že s assemblerem končím :-) Fakt, je to sice úžasný způsob programování jednočipů, ale jelikož bych nerad datlováním delších programů strávil půlku života, tak když jsem se rozhodoval co dál, Céčko byla jasná volba :-)))
Po prvotním zděšení z avr-gcc a jeho MakeFile jsem na chvíli zakotvil u komerčního CodeVison, které bylo na první klik funkční a předvařené kontrukce a konfigurace v začítcích opravdu dost pomohly. Poté jsem se ale vrátil k avr-gcc a to hlavně z toho důvodu, že je free a bez nějakých zbytečných omezení.
Na této stránce bych chtěl postupně umísťovat různé postřehy a drobnosti, které mě potkají. Není mým cílem zde uvádět nějaké advanced záležitosti, ale opravdu jednoduché věci, které mohou začátečníkům pomoci se v C pro AVR zorientovat.
Všechno je odladěno a odzkoušeno na WinAVR (GCC + AVRLibc). Na webu AVRLibc je taky dosti dobrá dokumentace, především co se týče eeprom, přerušení a všeho možného u AVR architektury a C.
Jednoduché čtení teploty z čidla DS1820.
Tento kousek kódu je schopen komunikovat s čidlem DS1820 na one-wire sběrnici a přečíst z něj teplotu. Omezení je, vzhledem k jednoduchosti, ve schopnosti komunikovat pouze s jedním zařízením na sběrnici. Ve většině případů to stačí a implementace plné podpory one-wire je zbytečná.
#include <avr/io.h>
#define F_CPU 4000000UL // 4 MHz - takt MCU
#include <util/delay.h>
#include "myds.h"
#define PD6 0x40 // na PD6 je připojeno čidlo
#define TX DDRD |= PD6;
#define RX DDRD &= ~PD6;
#define RXPIN PIND&PD6
// provede reset a test prezence ds-18b20 na sbernici
unsigned char ow_detect_presence(void) {
unsigned char out=1; // vychozi navratova hodnota
RX; // vychozi stav sbernice
_delay_us(1000); // pro ustaleni
TX; // bus low
_delay_us(480); // cas pro prikaz reset
RX; // uvolneni sbernice
_delay_us(70); // cekani na potvrzeni teplomerem
if(RXPIN) out=0; // pokud detekovana log.1, tak teplomer na sbernici neni
_delay_us(410); // pauza pred dalsi komunikaci
return out; // vrati stav 1=teplomer nalezen, 0=teplomer nenalezen
}
// posle na sbernici log.1
void ow_write_one(void) {
TX; // bus low
_delay_us(6); // pauza definujici log.1
RX; // uvolneni sbernice
_delay_us(64); // pauza pred dalsi komunikaci
}
// posle na sbernici log.0
void ow_write_zero(void) {
TX; // bus low
_delay_us(60); // pauza definujici log.0
RX; // uvolneni sbernice
_delay_us(10); // pauza pred dalsi komunikaci
}
// precte jeden bit ze sbernice
unsigned char ow_read_bit(void) {
unsigned char out=0; // vychozi navratova hodnota bitu
TX; // bus low
_delay_us(6); // pauza pro stav cteni
RX; // uvolneni sbernice
_delay_us(9); // pauza pro reakci teplomeru
if(RXPIN) out=1; // test stavu sbernice, vlastni cteni
_delay_us(55); // pauza pred dalsi komunikaci
return out; // prectena hodnota, 1 nebo 0
}
// odeslne na sbernici jeden byte. Odesila se prvni LSB
void ow_write_byte(unsigned char tosend) {
int n=8;
while(n--) {
if(tosend&1) ow_write_one(); else ow_write_zero();
tosend >>= 1;
}
}
// prijde ze sbernice jeden byte. Prijima jako prvni LSB.
unsigned char ow_read_byte(void) {
int n=8, out=0;
while(n--) {
out >>= 1; // bitovy posuv doprava
if(ow_read_bit()) out |= 0x80; // nastaveni nejvyssiho bitu na 1
}
return out;
}
// nacte teplotu z teplomeru a vrati ji ve formatu 1000+t*10
// priklad: 23.5°C = 1235, -10.5°C = 895
// tento format lze snadneji zpracovavat nez nejake floaty (zerou moc pameti)
unsigned int zmer(void) {
unsigned char data_lo, data_hi, desetiny;
signed char teplota;
unsigned int tmp = 0;
cli();
ow_detect_presence();
ow_write_byte(0x0CC);
ow_write_byte(0x44);
_delay_ms(300);
ow_detect_presence();
ow_write_byte(0x0CC);
ow_write_byte(0x0BE);
data_lo=ow_read_byte(); // 1. byte scratchpadu teplomeru = spodni byte teploty
data_hi=ow_read_byte(); // 2. byte scratchpadu teplomeru = horni byte teploty
teplota = (data_lo & 0x0F0) >> 4 | (data_hi & 0x0F) << 4 ; // signed teplota
tmp = 10 * teplota + 1000;
desetiny = (data_lo & 0x0F) * 0.625;
if(tmp<1000) tmp -= desetiny; else tmp += desetiny;
if((tmp<700)||(tmp>2200)) tmp=0;
sei();
return tmp;
}
int main(void) {
while(1) {
funkce_co_nekam_posle_vysledek( zmer() );
}
}
Vlastní zpožďovadlo s využitím timeru a přerušení
#include <avr/io.h>
#include <avr/interrupt.h>
// povoleni a zakazani preruseni pro generovani casove prodlevy
#define TIMER1_OVF_ENABLE TIMSK|=1<<TOIE1
#define TIMER1_OVF_DISABLE TIMSK&=~(1<<TOIE1)
// globalni promenna pouzita v preruseni
volatile unsigned int counter = 0;
// preruseni kazdou 1ms na generovani zpozdeni
// TCNT1=65535-(F_CPU/1000)
// Pro 4MHz: 65535-(4000000/1000)=65535-4000=61535=F05F
ISR(TIMER1_OVF_vect) {
TCNT1H=0x0F0;
TCNT1L=0x5F;
if (counter) {
counter--;
asm("wdr");
}
}
// cekaci smycka po kroku 1ms
void delay(unsigned int cas) {
counter=cas;
TIMER1_OVF_ENABLE;
while(counter) asm("nop");
TIMER1_OVF_DISABLE;
}
int main {
TCCR1A=0x00;
TCCR1B=0x01; // timer1 bez preddelicky, vstup systemovy takt
sei(); // povoleni preruseni
while(1) {
delay(1000);
}
}
EEPROM aneb ukládáme na 100 let
Pracovat z EEPROM nelze tak jednoduše, jako tomu bylo například v CodeVision, ale musí se využívat funkce k tomu určené.
#include <avr/io.h>
#include <avr/eeprom.h>
#define EEPROM_SECTION __attribute__ ((section (".eeprom")))
// globalni promenne v EEPROM, například tyto
uint8_t ee_pole[3] EEPROM_SECTION = {'1','2','3'}; // unsigned char pole
uint16_t ee_bigpole[200] EEPROM_SECTION = {0x6581,0x3283}; // unsigned int pole
uint16_t ee_cislo EEPROM_SECTION = 15; // unsigned int
uint8_t ee_kolo EEPROM_SECTION; // unsigned char
// globalni promenne v RAM na ukazku prace s EEPROM
unsigned int a;
unsigned char x,y;
int main(void) {
// "běžná" práce s EEPROM je pak například tato
a=eeprom_read_word(&ee_bigpole[1]);
x=2;
eeprom_write_byte(&ee_pole[x],0x2F);
y=x+eeprom_read_byte(&ee_kolo);
y=200;
while(y--){
eeprom_write_word(&ee_bigpole[y],eeprom_read_word(&ee_cislo););
}
}
Konstanty ve FLASHce
K čemu je to dobré? V případě, že máte velké množství konstant, které ve vašem programu potřebujete, tak je celkem nesmyslné je cpát do RAM, které je vždycky málo. Daleko vhodnější je nechat tyto konstatnty ve flash paměti a v případě potřeby si pro ně jednoduše sáhnout. Příkladem můžou být texty a hlášky, které jednočip vypisuje na displej a nebo posílá přes RS232 do světa. Opět dokumentace. Taková jednoduchá ukázka je zde:
#include <avr/io.h>
#include <avr/pgmspace.h>
char HELLO[] PROGMEM = "Ahoj svete, ja jsem konstanta";
char CHYBA[] PROGMEM = "Chyba, uzivatel je nepritomen";
char ZIVOT[] PROGMEM = "Je pondeli rano a to nemam rad :-)";
// vypsani znakoveho retezce na terminal z flash pameti
void write(char *sss){
char k;
while ((k=pgm_read_byte(sss++))>0) {
funkce_co_posle_znak_nekam(k);
}
lfcr();
}
int main(void) {
write(HELLO);
write(CHYBA);
write(ZIVOT);
}
Na počátku byla LED a jednočip
Blikání LED je sice doopravdy dost velká blbost, ale někdy se hodí, aby obsluha věděla, že procesor žije. A to nejlépe tak, aby blikání nezatěžovalo a nekomplikovalo vlastní program. Pokud je k dispozici nějaký časovač, je blikání snadné
#include <avr/io.h>
#include <avr/interrupt.h>
// stavova ledka
#define LED 0x04 // PD2
#define LED_SLOW TCCR0=0x05 // nastaveni TCCR0 na delicku 1024 pro pomale blikani led
#define LED_FAST TCCR0=0x04 // nastaveni TCCR0 na delicku 256 pro rychle blikani led
#define LED_SUPERFAST TCCR0=0x02 // nastaveni TCCR0 na delicku 8 pro super rychle blikani led
// bitova delicka
volatile unsigned char ledscaler = 0x08;
// preruseni kde se prevraci stav vystupu s LEDkou
ISR(TIMER0_OVF_vect) {
if(!(ledscaler>>=1)) {
if(!(PORTD&LED)) PORTD|=LED; else PORTD&=~LED;
ledscaler=0x10;
}
}
int main(void) {
PORTD=0x0;
DDRD=0x0FF; // PortD jako vystup
TIMSK|=1<<TOIE0; // povoleni preruseni timeru0 na preteceni
sei(); // globani povoleni preruseni
LED_SLOW;
//LED_FAST;
//LED_SUPERFAST;
}
Spáchal Walda 2008 - Datum: