Nov 262012
 

Una lunga introduzione

Dopo aver visto qui come funziona il bus 1wire e la teoria su come utilizzare la porta seriale, ora vediamo l’implementazione pratica. Coerentemente al resto del blog ho utilizzato CodeBlocks con le wxwidgets ma in questo caso ci serve un’ulteriore aggiunta, infatti le wxwidgets non hanno il supporto nativo per l’interfaccia RS232. Per aggiungere questa funzionalità nel mio progetto avevo inizialmente usato le librerie ctb che danno appunto il supporto seriale alle per Linux che per Windows (non per Mac). Nel mio progetto avevo usato la versione 0.13 ora non più disponibile in quanto attualmente è distribuita la versione 0.16. Avevo perciò deciso di aggiornare il mio progetto per permettere a chiunque di scaricarsi l’ultima versione disponibile e procedere con questo progetto. Avevo già scritto una buona parte dell’articolo ma non sapevo che avrei avuto un’amara sorpresa. Con la nuova versione il progetto non funziona! Ho perso decine di ore a scrivere codice, testare, riprovare, analizzare i dati seriali e quant’altro, ma a parità di codice non otteniamo lo stesso comportamento sulla porta seriale. Non ho scovato dove si annida il vero problema, ma è come se il nuovo software perdesse alcuni dati nella lettura dalla porta il che impedisce il corretto funzionamento di tutto il software.

Ho perciò cercato un’alternativa e mi sono imbattuto nell famose librerie boost. Dopo un paio di giorni di prove ho deciso di scartale. A parte i numerosi passaggi da fare per integrarle in CodeBlock, ho avuto comunque notevoli problemi con errori di compilazione, files non trovati e quant’altro, senza contare che le trovo particolarmente complesse e pesanti visto che dopo la luuuunga compilazione occupano la bellezza di 2.5GB.

Mi sono imbattuto poi in queste librerie che sono molto leggere e supportano sia Linux che Windows. Il rovescio della medaglia è che sono scritte in C e non in C++, inoltre mancano di un comando per variare il baud-rate di una porta già aperta, funzione che è fondamentale per il nostro progetto. A questo punto dovevo scegliere se utilizzare queste ultime librerie modificandole, o tornare indietro ed utilizzare le ctb versione 0.13. Per fortuna avevo una copia di backup dei files originali per cui li ho resi disponibili per il download qui: Librerie ctb 0.13 (10 download ) .

Preparare la libreria ctb

Dopo aver scaricato le librerie decomprimete l’archivio dove preferite. Aprite un prompt dei comandi e puntate alla sottocartella build con un CD, nel mio caso sarà CD C:\STX\TECNIX\PROGRAMMAZIONE\Librerie\libwxctb-0.13\build. Per compilare le librerie usiamo, come di consueto, il comando mingw32-make usando l’opzione -f per specidifcare il file makefile.gcc. Lanciamo perciò il comando:

mingw32-make -f makefile.gcc DEBUG=0 che crea la versione release, e poi

mingw32-make -f makefile.gcc DEBUG=1 per la versione di debug

In meno di un minuto la compilazione sarà terminata e dobbiamo andare ad integrare le librerie con le wxwidgets, che nel mio esempio sono le 2.9.1. Prima di tutto copiamoci gli header, in particolare in wxWidgets-2.9.1\include\wx copiamo la directory ctb-0.13 che troviamo in include \libwxctb-0.13\include\wx. A questo punto potremmo decidere di spostare le librerie, ossia la directory lib, ma ciò non è indispensabile in quanto possiamo specificare il percorso nell’interfaccia di CodeBlocks. Basterà andare su Project->Buld Options e sia nelle sezioni Debug e Release, nel linker setting, aggiunguamo il percorso alla libreria come potete vedere nella porzione di screenshot qui vicino. Fate attenzione al nome della libreria, quella con la d aggiuntiva è la versione di debug, l’altra è la versione release (vedi immagine a lato).

Descrizione del progetto

Per questo articolo ho preparato una classe che raggruppa tutte le funzioni per l’interfacciamento con il bus 1wire:

class OWR
{
 public:
 wxSerialPort * SerialDevice;
 wxString DeviceName;
 char RS232_1W_Buff[50];           // buffer in ingresso della seriale
 unsigned short RS232_1W_Len;
 int Send2Serial(char*ccm,int len,int sleeptime=0);
 int Send2Serial(char car,int sleeptime=0);
 unsigned char Raw_RS232_Read(char * buff,int maxlen=50);
 unsigned char OWReset();
 void OWWriteBit(unsigned char bit_value);
 char OWReadBit();
 void OWWriteByte(unsigned char byte_value);
 unsigned char OWReadByte();
 void OWRomSelect(char * address);
 void OWWriteMultiByte(char * buffer,unsigned char len);
 unsigned char OWByteInvert(unsigned char val);
 void OWReadTemp();
 unsigned char ROM[10][8]; // memoria per memorizzare 10 rom
 float temp[10];          // memoria per 10 temperature: vedi prossimo articolo
 unsigned char DeviceCount;
 char RomSearch();
 wxTextCtrl* Txt;
 char Calculate_crc(unsigned char*, int);
 OWR();
 ~OWR();
};

Ora andremo a vedere le funzioni una per una, ma prima vi descivo rapidamente le variabili di questa classe. All’inizio abbiamo wxSerialPort * SerialDevice che è un puntatore alla classe ctb che gestisce la porta seriale. Per comodità manteniamo il puntatore direttamente dentro la nostra classe OWR (OneWiRe). DeviceName tiene in memoria il nome della porta seriale da aprire, infatti in questa versione del progetto è gestito solo il nome delle porte seriali Windows ma la classe è già predisposta per una piccola aggiunta per Linux. RS232_1W_Buff e RS232_1W_Len vengono utilizzate per gestire le letture dalla porta seriale. Per ultimo vi segnalo wxTextCtrl* Txt che permette di tenere in memoria un puntatore ad un wxTextCtrl per permettere alla nostra classa di stampare a monitor le informazioni che vogliamo fa apparire.

Funzioni generiche

int Send2Serial(char*ccm,int len,int sleeptime=0) e int Send2Serial(char car,int sleeptime=0)

Queste due funzioni permettono di inviare dati sulla porta seriale: la prima permette di specificare un buffer di caratteri e la lunghezza da inviare con un terzo parametro opzionale che permette di attendere quel numero di millisecondi dopo la scrittura sulla porta. La seconda funzione invia un singolo carattere sulla porta seriale e anch’essa permette opzionalmente di stabilire un ritardo di un tot di millisecondi prima di uscire dalla funzione.

int OWR::Send2Serial(char*ccm,int len,int sleeptime)
{
 int scritti=SerialDevice->Writev(ccm,len,1000);
 if(sleeptime) wxMilliSleep(sleeptime);
 return(scritti);
}

int OWR::Send2Serial(char car,int sleeptime)
{
 char *addcar=&car;
 int scritti=SerialDevice->Writev(addcar,1,1000);
 if(sleeptime) wxMilliSleep(sleeptime);
 return(scritti);
}

Come vedete entrambe le funzioni al loro interno sfruttano la funzione Writev delle librerie ctb per inviare i caratteri alla porta seriale. Un’altra funzione generica è la :

Raw_RS232_Read(char * buff,int maxlen)

unsigned char OWR::Raw_RS232_Read(char * buff,int maxlen)
{
 unsigned short dwBytesRead = 0;
 dwBytesRead = SerialDevice->Read(buff,maxlen-1);
 return(dwBytesRead);
}

Questa funzione sfrutta la funzione Read della classe ctb, copia i bytes letti nel buffer specificato e ritorna il numero di bytes letti. Si faccia attenzione che se il buffer contiene già dei dati, questi vengono sovrascritti e non accodati.

Funzioni 1-Wire

Ora vediamo le funzioni specificatamente scritte per il bus 1-wire. Questa parte è in definitiva la parte più importante di questo articolo e del precedente in quanto è il risultato di tutto quel che abbiamo discusso in queste lunghe pagine.

OWReset()

Vi ricordate il reset del bus 1-Wire? Questa funzione fà esattamente quanto abbiamo spiegato nel primo articolo: Prima viene settata una velocità di 9600 baud, poi viene inviato 0xF0 e si attende la risposta da parte dei dispositivi, dopodichè la velocità è reimpostata a 115200 baud per le operazioni successive. Vi faccio notare che nell’attesa della risposta ho inserito un breve ciclo di lettura / attesa in quanto saltuariamente mi capitava di non ricevere la risposta nei tempi dovuti e così facendo è migliorata l’affidabilità del software.

unsigned char OWR::OWReset()
{
  RS232_1W_Len=0;
  SerialDevice->SetBaudRate((wxBaud)9600);
  Send2Serial(0xF0,1); // reset
  for(int i=0;iSetBaudRate((wxBaud)115200);
  return((unsigned char)RS232_1W_Buff);
}

La restituzione del primo carattere letto in risposta non è necessario, l’ho utilizzato unicamente per motivi di debug. Ora vediamo le funzioni per leggere e scrivere i singoli bit che sono:

char OWReadBit() e void OWRWriteBit(unsigned char bit_value)

Anche in questo caso non facciamo che applicare quanto visto nel precedente articolo per cui nella scrittura del bit inviamo 0xFF per i bit 1 e 0x00 per i bit 0. Per quanto riguarda la lettura, ricordiamo che 0xFF corrisponde al bit 1 mentre valori diversi corrispondono a bit 0.

void OWR::OWWriteBit(unsigned char bit_value)
{
  Send2Serial((bit_value ? 0xFF : 0x00),0);
  wxMilliSleep(1);
  RS232_1W_Len=Raw_RS232_Read(RS232_1W_Buff);
}
//--------------------------------------------------------------------------
 char OWR::OWReadBit()
{
  Send2Serial(0xFF); // invio comando lettura
  wxMilliSleep(2);
  RS232_1W_Len=Raw_RS232_Read(RS232_1W_Buff); // comando lettura
  return(RS232_1W_Buff[0] &0x01);
}

Per leggere interi bytes non facciamo altro che ripetere 8 volte la funzione OWReadBit. A dirla tutta non abbiamo mai visto quando utilizzare questa funzione, infatti ci è utile quando dobbiamo andare a leggere aree di memoria contenute nei sensori, cosa che ad esempio nei  DS18B20 ci serve per leggere il valore di temperaura, e non solo.

unsigned char OWReadByte()

unsigned char OWR::OWReadByte()
{
  unsigned char byte=0;
  for(int i=0;i<8;i++) if(OWReadBit()) byte=byte+(1<<i);
  return(byte);
}

Adesso vediamo una funzione che non opera direttamente sul BUS ma che serve a calcolare il CRC per verificare l’eventualità di alcuni errori di trasmissione. Ho avuto non poche difficoltà a comprendere fino in fondo come viene calcolato questo valore, volevo infatti scrivere la mia routine ma alla fine ho deciso di prenderne una già pronta. Anche in questo caso ho avuto numerosi problemi in quanto diverse funzioni mi davano risultati altrettanto diversi. L’unica che ho trovato funzionare è quella che vi propongo qui sotto. Io mi sono limitato a fare un  copia e incolla con piccole modifiche al codice che trovate qua. Non mi dilungherò ulteriormente in quanto servirebbe un intero articolo per l’argomento, l’unica cosa che vorrei sottolineare è che il polinomio genreatore corretto per il bus 1wire è 0x19.

char OWR::Calculate_crc(unsigned char * data_in, int number_of_bytes_to_read)
{
  //#define CRC8POLY    0x18              //0X18 = X^8+X^5+X^4+X^0

    uint8_t     crc;
    uint16_t loop_count;
    uint8_t  bit_counter;
    uint8_t  data;
    uint8_t  feedback_bit;

    crc = 0x00; // init

    for (loop_count = 0; loop_count != number_of_bytes_to_read; loop_count++)
    {
        data = data_in[loop_count];

        bit_counter = 8;
        do {
            feedback_bit = (crc ^ data) & 0x01;
            if ( feedback_bit == 0x01 ) crc = crc ^ 0x19; //CRC8POLY;
            crc = (crc >> 1) & 0x7F;
            if ( feedback_bit == 0x01 ) crc = crc | 0x80;

            data = data >> 1;
            bit_counter--;
        } while (bit_counter > 0);
    }

    return crc;
}

Ora la funzione più importante del progetto, quella sche scansiona il bus 1wire alla ricerca dei dispositivi

char RomSearch()

Questa funzione è descritta nella guida ufficiale, nel commento che trovate in fondo a questa pagina. Il codice perciò non l’ho scritto io ma mi sono limitato ad adattarlo per utilizzare le funzioni che vi ho descritto sopra, per prevenire alcuni errori originariamente non previsti, verificare il CRC di ogni indirizzo, scrivere a monitor alcune informazioni e memorizzare gli indirizzi ROM nella struttura OWR da noi scritta: attenzione che la memoria è impostata per 10 dispositivi e non è stato inserito un controllo per verificare il superamento di queso limite.


char OWR::RomSearch()
{
   int cnt;
   char errorcounter=0;
  cnt = 0;


unsigned char L_dwBytesRead;

  char mybits[65];
  char address[8];
  int path,next,pos;                          // decision markers
  int count;                                  // bit count
  int bit,chk;
partenza:
  char crc=0;                              // bit values
  path=0;                                     // initial path to follow
  DeviceCount=0;

  do
  {                                         // each ROM search pass
    L_dwBytesRead=OWReset();          //initialize the bus with a reset pulse
    if(L_dwBytesRead==0)
    {
      Txt->AppendText("RomSearch ritorna -1 per errore reset\n");
      return(-1);
    }
    OWWriteByte(0xF0); // Comando ricerca dispositivi

    next=0;                                 // next path to follow
    pos=1;                                  // path bit pointer
    count=0;                                // count the bits
    do
    {                                       // each bit of the ROM value
      bit=OWReadBit();//OWB->OWReadBit();                //read two bits, 'bit' and 'chk', from the 1-wire bus
      chk=OWReadBit();//OWB->OWReadBit();
      if(!bit && !chk)
      {                   // collision, both are zero
        if(pos&path) bit=1;             // if we've been here before
        else next=(path&(pos-1))|pos;   // else, new branch for next
        pos<<=1;
      }
      OWWriteBit(bit);//(write 'bit' to the 1-wire bus)

      mybits[count]=bit;
      SetBitN((unsigned char *)address,count,bit);
      count++;
    }
    while(count<64);

    crc=Calculate_crc((unsigned char*)&address[0],7);
    if(crc!=address[7]) // c'è un errore del CRC per cui la letura non è stata corretta
    {
      Txt->AppendText("CRC error\n");
      Txt->AppendText("-> "+Str2AnsiHex((char*)&address[0],8)+"\n");
      errorcounter++;
      if(errorcounter==3) break; // dopo 5 errori molto probabilmente hai un problema sulla linea seriale
      goto partenza;
    }

    for(int i=0;i<8;i++) ROM[DeviceCount][i]=address[7-i];//baddress[i]=address[7-i];
    errorcounter=0;
    Txt->AppendText("-> "+Str2AnsiHex((char*)&ROM[DeviceCount][0],8)+"\n");
    path=next;
    DeviceCount++; // non controlla il superamento dei limiti di memoria!!!!
}while(path);

}

Di fatto abbiamo ormai completato il progetto. Dobbiamo solamente scrivere una funzione che apre la porta seriale ed effettua la scansione del bus. Nello specifico ho creato usato la pressione di un pulsante per lanciare l’applicazione: dapprima il software chiede il numero della porta seriale, avvia la scansione ed utilizza un TextCtrl per scrivere i numeri delle ROM ed eventualmente gli errori occorsi. La funzione credo sia autoesplicativa, l’unica cosa che potrebbe confondere è come viene calcolato il nome del device seriale da aprire (wxT(“\\\\.\\com”)+dialog->GetValue()), questa forma, poco usuale, permette di aprire porte seriali che vadano oltre la 9 (dal 10 in su).

Ecco a voi la funzione:

void wxOneWireDialog::OnButton1Click(wxCommandEvent& event)
{
  OWbus = new OWR;

  if(!OWbus) return;

  OWbus->Txt=TextCtrl1;
  wxString comport;
  TextCtrl1->AppendText("Start\n");
  wxTextEntryDialog* dialog = new wxTextEntryDialog(this,wxT("Please insert serial port connection number"),wxT("Please inser the value...."));
  if ( dialog->ShowModal() != wxID_OK ) return;
  OWbus->DeviceName=wxT("\\\\.\\com")+dialog->GetValue();
  dialog->Destroy();

  OWbus->SerialDevice = new wxSerialPort;
  if(!OWbus->SerialDevice) return;

  if(OWbus->SerialDevice->Open(OWbus->DeviceName.mb_str()) < 0) TextCtrl1->AppendText("Attenzione porta seriale NON aperta\n");
  else TextCtrl1->AppendText("Ok porta seriale aperta.\n");
  OWbus->SerialDevice->SetBaudRate((wxBaud)9600);
  TextCtrl1->AppendText("Comincio ricerca dispositivi 1Wire\n");

  int result=OWbus->RomSearch();

  if(result==-1) TextCtrl1->AppendText("Dispositivi 1Wire non rilevati\n");
  else
  {
    TextCtrl1->AppendText("\nScansione terminata..\n");
    for(int i=0;iDeviceCount;i++) TextCtrl1->AppendText("Rilevato 0x"+Str2AnsiHex((char*)&OWbus->ROM[i][0],8)+"\n");
  }

}

Ed ecco, nell’immagine qui a lato,  il risultato finale. Faccio notare anche la presenza del pulsante “temp” per la lettura della temperatura dei sensori ds18B20, ma questa parte la affrontiamo nel prossimo articolo visto che anche questa volta ci siamo dilungati abbondantemente. Infatti nel prossimo articolo vediamo come leggere la temperatura dai sensori DS18B20, com’è strutturata la loro memoria e come possiamo sfruttare il lavoro fatto sinora per interfacciarci a quesi sensori tramite porta seriale. Spero che abbiate apprezzato questi articoli anche perchè non esiste nulal di così approfondito, in particolare in lingua italiana.

Prima di lasciarvi volevo solamente fare una nota sulla funzione Str2AnsiHex che non trovate in quanto descritto prima perchè è un funzione di una mia libreria e che utilizzo per la stampa in esadecimale:

wxString Str2AnsiHex(char * cm,unsigned short len,bool removespace)
{
    wxString AS=_("");

    for (unsigned short a=0;a<len;a++)
    {
      AS=AS+(Char2AnsiHex(cm[a]));
      if(!removespace) AS=AS+wxT(" ");
    }

    return(AS);
}

Spero che i listati appaiano correttamente visto che postare codic c sul web è sempre un incubo.

  No Responses to “RS232 con bus 1wire: dalla teoria alla pratica”