jeudi 21 juillet 2016

AIS toujours

Évolution de la phase lors d'un message AIS (le 'Curata' de passage à Liège le 19 juillet 2016)
$ rtl_sdr -f 162025000 -s 249600 f.iq
$ rtl_dump -p < f.iq > phase
La phase est obtenue avec rtl_dump. L'allure générale est due à un décalage entre la fréquence d'émission et la fréquence de réception. On a, environ, 5700° sur 5994 échantillons, ce qui correspond à 660 Hz ((5700/360)*(249600/5994)); --à vérifier--

En fait, il faudrait s'intéresser à la stabilité de la fréquence de réception (et sa variation de clé en clé).

$ rtl_sdr -f 162024400 -s 249600 f.iq
rtl_fftmax permet de voir s'il y a quelque chose dans le fichier :
$ /rtl_fftmax -f 162024400 -s 249600 -n 1024 core
On voit que le fichier contient des messages des deux canaux AIS, en 161.975 MHz et 162.025 MHz.

Dans un autre enregistrement d'environ 88 secondes à la Citadelle de Liège centré sur 162.025 MHz, si on applique l'algorithme de Goertzel avec N=512 sur les 'I', on obtient :
Le canal 'A' est obtenu en cherchant la présence de -50 kHz; 'B' à 0 KHz. En regardant de près dans Gnuplot, on peut facilement repérer où sont les messages dans le fichier binaire 'iq'.

Si je ne prends que les IQ des blocs où le test de Goertzel à 25 kHz est positif dans un enregistrement d'une minute centré sur 162 MHz et que je trace la phase en veillant à ce qu'elle soit continue, j'obtiens le graphe :
Ce qui est très encourageant. Les zones montantes concernent le +25 kHz, celles descendantes, le -25 kHz. On a aussi, probablement des collisions entre les deux fréquences qu'il faudrait départager avec un filtre. À noter qu'une translation en fréquence peut se traduire par la multiplication par un sinus. Le IQ étant des e^jwt, si on les multiplie par des e^jdwt, on obtient des e^(j(w+dw)t)... Et donc, l'idée de 'corriger la pente' en ajoutant simplement des a*t à la phase ne semble pas complètement farfelue (bien que probablement un peut trop intuitive (?)).

Notons que le graphe, sur une minute, comprends plus de 200000 points et qu'à 26 points par bits, on arrive à environ 40 messages de 200 bits. Ce qui, si on parvient à les décoder sans erreur, ne serait pas mal du tout.

Voir aussi

lundi 27 juin 2016

Balise CC1101

Observer passivement les ondes avec une clé TNT (RTL-SDR), c'est bien mais ce serait bien aussi d'avoir une référence paramétrable disponible à tout moment. Une possibilité est d'utiliser ces modules pour Arduino avec un CC1101 de Texas Instruments, que l'on trouve à quelques euros sur eBay. Ils sont conçus pour émettre (et recevoir) dans la bande ISM 433 MHz. Plusieurs types de modulations FSK sont programmables, ainsi divers paramètres comme la puissance d'émission, le baud rate, la variation de fréquence,... Ainsi que différents formats et encodages de trame. La datasheet ne fait pas loin de 100 pages et il existe de nombreuses 'application notes' sur divers aspects de la mise en œuvre. On peut, par exemple, s'en servir pour comparer des antennes. Une autre application, c'est d'observer de près des signaux plus ou moins connus. Par exemple, à partir de la configuration utilisée par la librairie panstamp (générée par l'utilitaire de Texas Instrument 'SmartRF Studio'),
uint8_t panstamp_config[] =
        {
/*00*/  0x2E, 0x2E, 0x06, 0x07, 0xB5, 0x47, 0x3D, 0x06,
        0x05, 0xFF, 0x00, 0x08, 0x00, 0x10, 0xA7, 0x62,
/*10*/  0xCA, 0x83, 0x93, 0x22, 0xF8, 0x35, 0x07, 0x20, /* datarate (CA) = 38.4 kbauds*/
        0x18, 0x16, 0x6C, 0x43, 0x40, 0x91, 0x87, 0x6B,
/*20*/  0xFB, 0x56, 0x10, 0xE9, 0x2A, 0x00, 0x1F, 0x41,
        0x00, 0x59, 0x7F, 0x3F, 0x81, 0x35, 0x09
        };

/*10    0xC7, 0x83, 0x83, 0x22, 0xF8, 0x35, 0x07, 0x20, /* data_rate (C7) = 4800 bauds; 2FSK(83)*/
où R[0x10], MDMCFG4, est 0xCA (et R[0x11], MDMCFG3 = 0x83) qui correspond à 38383 symboles par seconde et R[0x12] est 0x93 (GFSK). Si on remplace ces valeurs par, respectivement, 0xC7 (4800 bauds) et 0x83 (2-FSK), on obtient un signal plus facile à observer/analyser avec une clé RTL-SDR. En zoomant sur la phase (arc tangente(I/Q); ou, serait-ce Q/I?) du signal reçu lors du préambule, on observe :
On observe que la phase décroît jusque A, croît entre A et B, décroît entre B et C pour ensuite recroître (les transitions autour de 0/360 provoquent un saut dont il ne faut pas tenir compte). La phase croît lorsque la fréquence est supérieure à la référence et décroît lorsque la fréquence est inférieure. En regardant de plus près, on note les coordonnées des points : A=(12585,130) B=(12634,150) et C=(12682,8). On compte donc 48 (ou 49) échantillons entre les points. Et, 230000 (échantillons/seconde) divisé par 48, cela donne bien 4792 symboles par seconde (~4800 bauds). En observant le gain (ou la perte) de phase entre deux points, on devrait retrouver l'écart de fréquence programmé pour le 2-FSK. A->B, +1460°; B->C, -1582°. L'asymétrie provient d'une erreur dans l'estimation de la fréquence de référence (ou, plus exactement, de quartz approximatifs; la fréquence choisie étant 433 MHz des deux côtés. Coupons la poire en deux et estimons que la phase progresse de 1500° 4800 fois par seconde, c'est-à-dire, 7200000° ou 20000 'tours', c.-à-d., 20 kHz. Et c'est exactement ce que l'on retrouve dans R[0x15], DEVIATN = 0x35. Une telle déviation est probablement excessive pour 4800 bauds; c'était prévu pour 38400 bauds. C'est aussi ce que l'on observe avec GNU Radio :
Le profil serait probablement plus doux en GFSK; en évitant les changements abrupts de fréquences. À noter que ces affichages FFT et Waterfall sont, un peu, des gadgets : un court message (du genre 8 caractères avant l'ajout du préambule et le CRC) est envoyé environ 7 fois par seconde (duty-cycle ~.25) et il ne s'affiche pas à chaque fois. J'ignore quelle est la proportion de signal non traité mais la traque aux messages courts et rares est un peu aléatoire avec ce genre d'outil. Si je mets 0x00 dans DEVIATN, j'obtiens la déviation minimale, 1587 Hz. À noter que c'est trop grand pour émuler un modem Bell 202. Par exemple, pour simuler un APRS venant d'ISS.
Si on regarde une trame complète en rapprochant la fréquence de réception de la fréquence d'émission (433 001 210 Hz plutôt que 433 000 000) et qu'on évite les sauts du côté de 0-360, cela donne le graphe :
On voit que la fréquence peut rester la même pendant un grand nombre de symboles (par exemple, quand on transmet des 0x00). Une possibilité est d'utiliser la fonction 'whitening' du chip qui fait un XOR avec un nombre pseudo-aléatoire. Une autre est d'utiliser un codage Manchester en mettant le bit 0x08 de R[0x12], MDMCFG2, à 1. On obtient alors le graphe :
...qui ressemble étrangement à celui de la grue du billet précédent. Sauf que dans le cas de la grue, les changement de fréquences sont 'arrondis', il doit s'agir de GFSK (?). On notera également que le codage Manchester engendre une diminution du débit utile.

La clé TNT utilisée ici est la petite : RT2832U avec un tuner R820T :


À suivre...

samedi 9 avril 2016

RTL-SDR SigInt

Lors du FOSDEM 2016, Felix Wunsch a présenté le SigInt comme une activité ludique et enrichissante... Équipé d'un de ces gadgets à moins de 10 euros sur eBay, une clé TNT avec un chip Realtek RTL2832U, je m'en vais faire un tour avec le LiveDVD GNU Radio sur la bande ISM 433 MHz :
Tiens!, il y a un joli signal assez puissant et bien stable... En fait, ce n'est pas tout le temps mais quand le signal est présent, il reste stable longtemps. Tout porte à croire qu'il s'agit de la télécommande de la grue du chantier en face... (les horaires et l'activité coïncident avec la présence du signal)
Comme on peut le voir sur l'affichage I/Q de la copie d'écran GNU Radio, ce n'est pas une modulation d'amplitude, la trace est un cercle (d'amplitude constante). En y regardant de plus près, en utilisant la commande
$ rtl_sdr -f 433630000 -s 256000 file
et d'un petit programme 'rtl_dump -p' pour sortir la phase en texte, on obtient avec Gnuplot quelque chose comme :
Le gain automatique (AGC) fait varier la puissance du signal qui sature un peu à l'extérieur mais bon. Ce qui est amusant, c'est que la phase semble 'tourner' (à magnitude constante). En fait, c'est dû au fait que la fréquence de réception choisie ne correspond pas exactement à la fréquence d'émission (en charabia : 'frequency offset' -> 'spinning constellation' en démodulation QAM). J'ai estimé la fréquence en regardant le 'waterfall'; ce n'est pas assez précis. Il existe des techniques pour retrouver la fréquence correcte mais on verra ça plus tard... Pour le moment, c'est bon assez pour voir si on peut en tirer quelque chose. Si l'on s'intéresse à l'évolution de la phase, on obtient bien quelque chose de structuré :
En zoomant,
on voit que le signal est très propre mais que les sauts autour de 0-360° rendent l'interprétation difficile. En assurant la continuité et en laissant croître la phase au delà de 360°, cela devient plus clair :
Cette dérive vers le haut est un peu gênante, on peut la supprimer artificiellement en soustrayant la pente (-i*alpha):
Et voilà une jolie trame bien propre (mais un peu asymétrique) qu'il ne reste plus qu'à analyser...

Si j'écris un petit programme qui mesure les montées et les descentes des données (montantes), j'obtiens quatre nuages de points bien séparés représentant les montées/descentes courtes/longues :
On notera également que l'on n'a pas n'importe quelle suite de transitions. On n'a pas de suite {courte descente+longue montée} ni de suite {courte montée+longue descente}. En fait, si on considère 4 positions de hauts et bas de courbe, on peut avoir les transitions 1->{2|4}, 2->1, 3->4 et 4->{3|1}. Seuls les départs de 1 et 4 permettent de coder 1 bits. Une petite transition est d'office suivie d'une petite transition qui ramène l'état à son point de départ. Une difficulté de ce petit programme est de commencer et finir aux bons endroits et éviter les parasites.

On peut déjà regarder si les trames se ressemblent en les mettant côte à côte :


Normalement, il faudrait filtrer pour purifier le signal, utiliser la fréquence correcte et ensuite synchroniser et corréler pour extraire les bits. Mais le signal est propre et semble facile à décoder avec un petit programme ad hoc. Mettons qu'on imprime '1' pour deux 'petites' transitions successives et '0' pour une grande transition. Les dix premières trames donnent :
111111000000000000000000010111010010000000011011011101100010101010101100000010010000000010000000010000000010000000010000000010000000010000111010001111100001011101110100010000000001111
0011000000000000000000010111010010000000011011011101100010101010110010000010010000000010000000010000000010000000010000000010000000011001001010001000100001011101110100010000000001111
111110001000000000000000000010111010010000000011011011101100010101010110100000010010000000010000000010000000010000000010000000010000000010001001101010111010001011101110100010000000001111
1110001000000000000000000010111010010000000011011011101100010101010111010000010010000000010000000010000000010000000010000000010000000011001100100111111010001011101110100010000000001111
111111011000000000000000000010111010010000000011011011101100010101010111100000010010000000010000000010000000010000000010000000010000000010001100011100000100001011101110100010000000001111
1111111011000000000000000000010111010010000000011011011101100010101010000010000010010000000010000000010000000010000000010000000010000000011010100100111001100001011101110100010000000001111
11111101011000000000000000000010111010010000000011011011101100010101010000100000010010000000010000000010000000010000000010000000010000000010010100011100110010001011101110100010000000001111
11111111110001000000000000000000010111010010000000011011011101100010101010001010000010010000000010000000010000000010000000010000000010000000011010001010001110010001011101110100010000000001111
01000000000000000000010111010010000000011011011101100010101010001100000010010000000010000000010000000010000000010000000010000000010010001101010001100001011101110100010000000001111
011000000000000000000010111010010000000011011011101100010101010010010000010010000000010000000010000000010000000010000000010000000011011111101010110100001011101110100010000000001111
C'est amusant, des similarités apparaissent mais, pour une raison inconnue, elles ne sont pas alignées. Si on les aligne, cela donne :
000000000000000000010111010010000000011011011101100010101010101100000010010000000010000000010000000010000000010000000010000000010000111010001111100001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010110010000010010000000010000000010000000010000000010000000010000000011001001010001000100001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010110100000010010000000010000000010000000010000000010000000010000000010001001101010111010001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010111010000010010000000010000000010000000010000000010000000010000000011001100100111111010001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010111100000010010000000010000000010000000010000000010000000010000000010001100011100000100001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010000010000010010000000010000000010000000010000000010000000010000000011010100100111001100001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010000100000010010000000010000000010000000010000000010000000010000000010010100011100110010001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010001010000010010000000010000000010000000010000000010000000010000000011010001010001110010001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010001100000010010000000010000000010000000010000000010000000010000000010010001101010001100001011101110100010000000001111
000000000000000000010111010010000000011011011101100010101010010010000010010000000010000000010000000010000000010000000010000000011011111101010110100001011101110100010000000001111
En hexadécimal, cela pourrait donner quelque chose comme (dc; 16o2i ...p; 'pourrait' parce qu'il faut encore aligner et voir si c'est le LSB ou le MSB qui est transmit en premier) :
02E900DBB155604804020100804021D1F0BBA200F
02E900DBB1559048040201008040325110BBA200F
02E900DBB155A048040201008040226AE8BBA200F
02E900DBB155D0480402010080403327E8BBA200F
02E900DBB155E048040201008040231C10BBA200F
02E900DBB1541048040201008040352730BBA200F
02E900DBB1542048040201008040251CC8BBA200F
02E900DBB15450480402010080403451C8BBA200F
02E900DBB1546048040201008040246A30BBA200F
02E900DBB154904804020100804037EAD0BBA200F
On trouve des choses constantes et d'autres qui ne le sont pas. Probablement qu'en analysant un grand nombre de trames dans un grand nombres de circonstances on parviendrait à décoder tout cela. Et, en comparant différentes grues, y découvrir une adresse, un modèle,... (?)

Les tests du billet suivant laissent à penser qu'il pourrait s'agir d'un encodage Manchester en GFSK à 9600 bauds (?; le CC1101 ne semble pas pouvoir du Manchester en MSK (qui 'arrondit' mieux l'évolution de la phase)).

(à suivre?)

mardi 8 décembre 2015

MLX90614

Un petit thermomètre infrarouge en I²C...

Juste 4 fils sur le clone Arduino nano : GND, VCC (3.3V), SCL(A5), SDA(A4)

Lecture assez ardue des datasheets ATmega328p (TWI aka I2C, section 21) et MLX90614 pour finalement pas grand chose : après avoir initialisé l'interface I²C, il 'suffit' d'envoyer <S><SLA+W><reg><S><SLA+R><data-in><data-in><data-in><P>. Cela se fait via le registre TWDR (data), activé par TWCR (control) en vérifiant le TWSR (status dont TW_STATUS est un subset). Avec <S> pour 'Start'; SLA pour 'SLave Address'; R/W, le sens de communication; et <P> pour 'stoP'. Toutes les activations n'utilisent pas les mêmes bits (mais TWINT & TWEN sont toujours présents); TWINT marque également la fin d'exécution de la commande (cela devrait se faire par interruption mais ici, on se contente de 'poller' le bit). Chaque phase présente un TWSR/TW_STATUS différent pour indiquer que l'opération s'est bien déroulée. Le composant renvoie 3 octets : deux octets pour la température (en 1/50-ièmes de degré Kelvin, LSB first) et un CRC non vérifié ici (PEC=Packet Error Code).

#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>
#include <util/twi.h>
#include <stdio.h>
#include <string.h>
/*
** UART - stdio
*/
int uart_getchar(FILE *stream)
    {
    while (!(UCSR0A & (1<<RXC0)))
        ;
    return(UDR0);
    }
int uart_putchar(char c, FILE *stream)
    {
    while (!(UCSR0A & (1<<UDRE0)))
        ;
    UDR0 = c;
    if (c == '\n')
        uart_putchar('\r', stream);
    }
void uart_init(void)
    {
    UBRR0H = 0x00;
    UBRR0L = 103;  /* 9600 bps | F_CPU/16/baud-1 */
    UCSR0C = (1<<USBS0) | (3<<UCSZ00); /* 8N1 */
    UCSR0B = (1<<RXEN0) | (1<<TXEN0);
    fdevopen(uart_putchar, uart_getchar); /* stdio init */
    }
/*-----------------------------------------------*/
/*
** TWI - I2C  (single master)
** SCL   A5   PC5
** SDA   A4   PC4
*/
int twi_stop();
void twi_init()
    {
    TWSR = 0;    /* prescaler = 0 */
    TWBR = (F_CPU / 100000UL - 16) / 2;    /* 0x48 -> 100 kHz */
    twi_stop();
    }
#define twi_wait()    { while ((TWCR & _BV(TWINT)) == 0) ; }
#define twi_go(x)    TWCR = _BV(TWINT)|_BV(TWEN)|x
int twi_start() /* send start condition */
    {
    twi_go(_BV(TWSTA));    /* start */
    twi_wait();
    if (TW_STATUS != TW_START && TW_STATUS != TW_REP_START)
        {
        fprintf(stderr, "**** I2C/start : unexpected status 0x%02x\n", TW_STATUS);
        return(-1);
        }
    return(0);
    }
int twi_stop()
    {
    twi_go(_BV(TWSTO));    /* stop */
    return(-1);    /* always */
    }
int twi_sla(uint8_t sla, uint8_t rw)
    {
    TWDR = (sla<<1) | rw;
    twi_go(0);
    twi_wait();
    if ((rw == TW_WRITE && TW_STATUS != TW_MT_SLA_ACK)
        || (rw == TW_READ && TW_STATUS != TW_MR_SLA_ACK))
        {
        fprintf(stderr, "**** I2C/slave address : unexpected status 0x%02x\n", TW_STATUS);
        return(-1);
        }
    return(0);
    }
int twi_wcmd(uint8_t data)
    {
    TWDR = data;    // reg
    twi_go(0);
    twi_wait();
    if (TW_STATUS != TW_MT_DATA_ACK)
        {
        fprintf(stderr, "**** twi_wcmd : unexpected status 0x%02x\n", TW_STATUS);
        return(-1);
        }
    return(0);
    }
int twi_rdata()
    {
    twi_go(_BV(TWEA));
    twi_wait();
    if (TW_STATUS != TW_MR_DATA_ACK)
        {
        fprintf(stderr, "**** twi_rdata : unexpected status 0x%02x\n", TW_STATUS);
        return(-1);
        }
    return(TWDR);
    }
pr_temp(long t)
    {
    char s;
    int a, b;

    t -= 27350; /* Kelvin * 100 -> Celsius * 100 */
    s = (t < 0) ? '-' : '+'; 
    if (t < 0)
        t = -t;
    a = t / 100; b = t % 100;
    printf("%c%d.%02d", s, a, b);
    }
#define MLX_ADD  0x5A    /* MLX90614 SLA (i2c default SLave Address) */
#define CMD_Ta   0x06    /* ambiant temperature */
#define CMD_Tobj 0x07    /* IR object temperature */

long twi_MLX_get100tc(uint8_t sla, uint8_t reg)
    {
    int    a, b;
    long    t;

    if (twi_start() < 0 || twi_sla(sla, TW_WRITE) < 0)
        return(twi_stop());
    if (twi_wcmd(reg) < 0)
        return(twi_stop());
    if (twi_start() < 0 || twi_sla(sla, TW_READ) < 0)
        return(twi_stop());
    if ((a = twi_rdata()) < 0)
        return(twi_stop());
    if ((b = twi_rdata()) < 0)
        return(twi_stop());
    twi_rdata();    /* PEC - don't care */
    twi_stop();
    t = (a + (b << 8));
    return(t*2);    /* Kelvin * 100 */
    }
/*-----------------------------------------------*/
int main(void)
    {
    uart_init();
    twi_init();
    for(;;)
        {
        printf("\nT ambiant : ");
        pr_temp(twi_MLX_get100tc(MLX_ADD, CMD_Ta));
        printf(", T obj : ");
        pr_temp(twi_MLX_get100tc(MLX_ADD, CMD_Tobj));
        _delay_ms(500L);
        }
    }


Mais, bien sûr, on pourrait utiliser la lib Wire de l'Arduino...

Pour la compilation, le téléchargement et le test :
$ avr-gcc  -mmcu=atmega328  -Os -o gy906.elf gy906.c
$ avrdude -c arduino -p atmega328P -P /dev/ttyUSB0 -b 57600 -U flash:w:gy906.elf
$ minicom -D /dev/ttyUSB0 -b 9600


jeudi 26 novembre 2015

Code minimal nRF24L01 pour un Arduino nano

On peut trouver des modules de communication avec des nRF24L01+ pour utiliser avec des Arduino aux alentours de un euro... Il s'agit de composants qui permettent de communiquer par ondes radios à 2.4 MHz en GFSK à 2 Mbits/seconde. Ils comprennent la partie émission et la partie réception mais il font l'un ou l'autre, ils fonctionnent en half-duplex. Il existe une ou plusieurs librairies Arduino mais ça n'empêche pas de chipoter soi-même...

Quelques fils à raccorder : 4 connexions SPI, 2 pour l'alimentation (3.3V!) et un pour une commande du chip (CE).

En pratique, cela donne :

...À réaliser deux fois.

Pour ce qui est du programme, quand on décide de se passer de l'IDE Arduino, de ses librairies et d'utiliser avr libc, il faut commencer par programmer l'UART et le relier à stdio. Ensuite, programmer l'interface SPI en fonction de ce que l'on trouve dans la datasheet du nRF24L01+ et de l'ATmega328p. Il faut configurer certaines pins en sortie : MOSI, /SS et CLK et activer/configurer le SPI. Comme l'horloge est inactive basse et qu'on échantillonne les données au flanc montant, on est en 'Mode 0'; on spécifie un diviseur pour l'horloge SPI et le fait qu'on est 'MASTER'. Reste à programmer la fonction de transfert... En fait, en SPI, chaque fois qu'il y a un bit qui sort, un bit rentre et les transferts comportent un certain nombre d'octets encadrés par la sélection de l'esclave; chaque octet étant géré par le hardware. Quand on met un octet dans le registre SPDR, le transfert démarre. La fin du transfert est signalée dans un bit du registre SPSR. Bref, on a une fonction spix(in <- out, n).

Reste à programmer le nRF24L01... La datasheet fait 74 pages mais si l'on s'en tient à une fonctionnalité de base : transmettre avec l'un et recevoir avec l'autre, il n'y a presque rien à faire. Le chip, initialisé au RESET, est quasi fonctionnel : la transmission se fait à 2 Mbits/s à une fréquence donnée (à puissance maximum), avec un CRC de 1 byte, des adresses (pré-initialisées) de 5 bytes, les paquets envoyés sont retransmis jusqu'à trois fois si un accusé de réception (acknowledgement) n'est pas reçu... Il faut juste préciser la taille des paquets que l'on entend recevoir. Quelques dizaines de lignes de programmation suffisent pour voir les systèmes interagir.

#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>
#include <stdio.h>
#include <string.h>
/*
** UART - stdio
*/
int uart_getchar(FILE *stream)
    {
    while (!(UCSR0A & (1<<RXC0)))
        ;
    return(UDR0);
    }
int uart_putchar(char c, FILE *stream)
    {
    while (!(UCSR0A & (1<<UDRE0)))
        ;
    UDR0 = c;
    }
void uart_init(void)
    {
    UBRR0H = 0x00;
    UBRR0L = 103;  /* 9600 bps | F_CPU/16/baud-1 */
    UCSR0C = (1<<USBS0) | (3<<UCSZ00); /* 8N1 */
    UCSR0B = (1<<RXEN0) | (1<<TXEN0);
    fdevopen(uart_putchar, uart_getchar); /* stdio init */
    }
void xdump(uint8_t *ucp, int len)
    {
    while (--len >= 0)
        printf("%02x ", *ucp++);
    printf("\n\r");
    }
char *getline(char *cp, int n) /* line input with  minimal editing */
    {
    int    i=0;
    char    c;

    for(;;)
        {
        c = fgetc(stdin);
        if (c=='\n' || c=='\r' || i==(n-1))
            {
            cp[i] = '\0';
            return(cp);
            }
        if (c=='\b' && i>0)
            {
            fputc(c, stdout);
            fputc(' ', stdout);
            i -= 1;
            }
        else cp[i++] = c;
        fputc(c, stdout);
        }
    }
/*-----------------------------------------------*/
/*
** SPI
** /SS /CS  D10 PB2
** MOSI     D11 PB3
** MISO     D12 PB4
** SCK      D13 PB5
*/
void spi_init()
    {
    DDRB  |= _BV(PB2)|_BV(PB3)|_BV(PB5);    /* /SS+MOSI+SCK out */
    SPCR   = _BV(SPE)|_BV(MSTR)|_BV(SPR0);  /* Enable SPI, Master, set clock rate fck/16 Mode-0 */
    PORTB |= _BV(PB2);                      /* /CS := 1 spi idle*/
    }
/*
** spix(...) clocks out *ucpout, getting *ucpin
*/
void spix(uint8_t *ucpin, uint8_t *ucpout, int n)  /* IN := spi(OUT) */
    {
    PORTB &= ~_BV(PB2);    /* /CS := 0 */
    while (--n >= 0)
        {
        SPDR = *ucpout++;
        while ((SPSR & (1<<SPIF)) == 0)
            ;
        *ucpin++ = SPDR;
        }
    PORTB |= _BV(PB2);    /* /CS := 1 */
    }
/*-----------------------------------------------*/
/*
** nRF24L01
**    CE = D8 = PB0
*/
/* commands */
#define R_REGISTER(reg) (0x00+reg)  /* read register 'reg' */
#define W_REGISTER(reg) (0x20+reg)  /* write register 'reg' */
#define R_RX_PAYLOAD    0x61        /* read received payload */
#define W_TX_PAYLOAD    0xA0        /* write payload to transmit */

uint8_t bufout[33];   /* Arduino -> nRF24L01 */
uint8_t bufin[33];    /* nRF24L01 -> Arduino */
void ce(int val)
    {
    if (val)
         PORTB |= _BV(PB0);
    else PORTB &= ~_BV(PB0);
    }
void pulse_ce()
    {
    ce(1);
    _delay_us(15L); /* minimum 10 */
    ce(0);
    }
void nrf24_init(int rx)
    {
    if (rx)
        {
        bufout[0] = W_REGISTER(0x11);
        bufout[1] = 32;
        spix(bufin, bufout, 2);        /* R11 = RX_PW_P0 = 32 payload length */
        bufout[0] = W_REGISTER(0x00);
        bufout[1] = (1<<1) + rx;
        spix(bufin, bufout, 2);     /* R00 = CONFIG = PWR_UP+PRIM_RX */
        _delay_ms(2L);            /* PWR_UP delay */
        ce(1);                /* enter listening */
        }
    else{ /* tx */
        bufout[0] = W_REGISTER(0x00);
        bufout[1] = (1<<1);
        spix(bufin, bufout, 2);        /* R00 = CONFIG = PWR_UP (tx default) */
        _delay_ms(2L);            /* PWR_UP delay */
        ce(0);
        }
    }
send(uint8_t *ucp, size_t n)
    {
    memset(bufout, 0x00, sizeof(bufout));
    bufout[0] = W_TX_PAYLOAD;        /* cmd = TX_PAYLOAD */
    n = (n > 32) ? 32 : n;
    memcpy(&bufout[1], ucp, n);
    spix(bufin, bufout, 33);    /* always send 32 bytes */
    pulse_ce();
    }
rcv(uint8_t *ucp, size_t n)
    {
    memset(bufout, 0xff, sizeof(bufout));
    bufout[0] = 0xFF;    /* cmd = NOP */
    do    {
        spix(bufin, bufout, 1);
        } while ((bufin[0] & 0x0e)  == 0x0e);    /* while nothing in */
    bufout[0] = R_RX_PAYLOAD;    /* cmd = R_RX_PAYLOAD */
    spix(bufin, bufout, 33);    /* always read 32 bytes */
    memcpy(ucp, bufin+1, n);
    }
/*-----------------------------------------------*/
int main(void)
    {
    char buf[32];

    spi_init();
    uart_init();
    printf("Test nRF24L01\n\r=============\n\r1. Receive\n\r2. Transmit\n\r> ");
    getline(buf, sizeof(buf));
    if (buf[0] == '1')
        { /* receive loop */
        nrf24_init(1);
        printf("Receiving...\n\r");
        for (;;)
            {
            rcv(buf, sizeof(buf));
            printf("%s\n\r", buf);
            }
        }
    else{ /* transmit loop */
        nrf24_init(0);
        for (;;)
            {
            printf("\n\r> ");
            getline(buf, sizeof(buf));
            send(buf, strlen(buf)+1);
            }
        }
    }
Il faut noter qu'un RESET de l'ATmega328p ne remet pas à zéro le nRF24L01 et que, pour bien faire, il faudrait ajouter un FLUSH_RX et un FLUSH_TX en démarrant pour remettre la transmission à zéro. Pour bien faire, il faudrait faire du contrôle de flux et traiter les erreurs. En fait, cela se passe surtout du côté de l'émetteur; du côté du récepteur, on ne reçoit rien si ce que l'on reçoit n'est pas un message correct (tout au plus peut-on s'apercevoir que la FIFO a débordé si on ne lit pas les messages assez vite). Il y a aussi de nombreuses autres fonctions du composant à explorer... (changement d'adresse, de puissance, sources multiples,...). Mais bon...

À noter aussi que _BV(b) (bit value) est équivalent à (1<<b) et que, pour bien faire, il faudrait utiliser une notation ou l'autre. Ce serait plus élégant... La seconde notation est plus puissante, elle fonctionne avec des champs de plusieurs bits. Par exemple (7<<3) n'est pas simplement représentable dans la première.

Pour ce qui est des commandes, quelque chose comme :
$ avr-gcc  -mmcu=atmega328  -Os -o nrf.elf nrf.c
$ avrdude -c arduino -p atmega328P -P /dev/ttyUSB0 -b 57600 -U flash:w:echo.elf
$ avrdude -c arduino -p atmega328P -P /dev/ttyUSB1 -b 57600 -U flash:w:echo.elf
$ minicom -D /dev/ttyUSB0 -b 9600  # et USB1 dans un autre terminal
Ensuite, on tape '1' dans un des terminaux minicom et '2' dans l'autre. Le premier passe en mode réception et reçoit tout ce que l'on tape dans l'autre (en veillant à garder des lignes de moins de 32 caractères).



Voir aussi



mardi 1 septembre 2015

52pi's 5 inch HDMI Touch Screen

Un écran 5 pouces HDMI à une trentaine d'euros pour mon Raspberry Pi, difficile de résister...

Il y avait un petit risque parce que la documentation est un peu lacunaire. Difficile de savoir s'il faut absolument un 'custom kernel' fourni par le constructeur ou si modifier /boot/config.txt suffit pour l'affichage. Pour la partie 'touch', je n'ai pas encore regardé (et je compte continuer à utiliser mon clavier Bluetooth IPazzPort). Après avoir un peu chipoté, sans succès, de manière générale en regardant la doc RPi. Je suis tombé sur ce forum où quelqu'un propose :
#increase HDMI signal strength (just a black screen if not set!)
config_hdmi_boost=4

#remove black borders
disable_overscan=1

#set specific CVT mode
hdmi_cvt 800 480 60 6 0 0 0

#set CVT as default
hdmi_group=2
hdmi_mode=87
Et, de fait, cela fonctionne maintenant.


Alimenté par le port USB (micro), il semble consommer 0.38 Ampère (0.23 quand le Pi est éteint; d'après mon 'charger doctor' (non qualibré)).

Voilà qui est beaucoup plus confortable que mon écran SPI 2.5 pouces 320x240 MZTX-PI-EXT, bien que le touchscreen reste résistif. C'est un peu plus encombrant avec les deux câbles en plus mais on récupère les GPIO. (Bien sûr, une tablette ou un smartphone...)

Lsusb donne :
0eef:0005 D-WAV Scientific Co., Ltd
Sur mon desktop Ubuntu, dmesg donne :
$ dmesg
[ 1965.392282] usb 1-1.4: new full-speed USB device number 4 using ehci-pci
[ 1965.486613] usb 1-1.4: New USB device found, idVendor=0eef, idProduct=0005
[ 1965.486618] usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 1965.486621] usb 1-1.4: Product: By ZH851
[ 1965.486623] usb 1-1.4: Manufacturer: RPI_TOUCH
[ 1965.486625] usb 1-1.4: SerialNumber: @\x18B547044417
[ 1965.488263] hid-generic 0003:0EEF:0005.0003: hiddev0,hidraw1: USB HID v1.10 Device [RPI_TOUCH By ZH851] on usb-0000:00:1a.0-1.4/input0
(mais ne permet pas encore de modifier la position du curseur ni de cliquer...; Cela donne néanmoins le sentiment qu'il devrait être possible de le faire fonctionner assez facilement...)

Pas encore testé mais la solution semble se trouver ici (en Python). En effet, /dev/hidraw1 donne des séquences de 22 caractères du type
170   1   1 115  14  73 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   1   1  90  14  70 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   0   0   0   0   0 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   1   1  93   2  70 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   0   0   0   0   0 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   1  14 200   2 105 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   1  14 194   2  98 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
170   1  14 200   2 113 187   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
où on devine un marqueur de début, un marqueur de fin, un indicateur de 'touché' et des xy sur deux caractères. En ne gardant que ces trois derniers, on lit des choses comme :
1 283 535
1 285 537
0   0   0
1 3790 3732
1 3795 3748
1 3795 3753
Les coordonnées sont comprises entre 0 et 4096. Il va donc falloir adapter le bidule... J'ai mis un court programme en 'C' sur https://github.com/xofc/hidraw2uinput. Aucun paramètre, tout est 'hardcodé' mais le programme n'est pas très long et devrait être facile à adapter et à installer. Sinon, il reste la solution en Python... (interprété, donc plus lourd et avec plus de dépendances (voir le script d'installation; le programme 'C' ne dépend de rien de particulier))

À noter que le Raspberry Pi possède un 'écran officiel' depuis le 08 septembre 2015.

Pour regarder de la vidéo, Xbmc (maintenant Kodi) est préférable à Vlc (qui manque de ressources sur un Pi-1).

5" HDMI LCD de Waveshare

On trouve un autre écran du même type qui s'enfiche sur le connecteur P1 des Raspberry (fabriqué par WaveShare). Il est fourni avec un adaptateur en 'U' pour le HDMI. Il est bien en face avec le Pi-2. Pour l'autre modèle que je possède (B rev 2?), il n'est pas en face mais cela fonctionne car le début du connecteur est compatible. Le connecteur HDMI est sur le grand côté, au dessus.
Sur certaines photos, il semble également avoir un connecteur micro-USB mais, en fait, il est factice (!) et la communication avec le 'touch' (XPT2046 ?) s'effectue via les GPIO de P1 (notamment SPI). J'ignore s'il est possible d'adapter le kernel, ils fournissent une image complète (que je ne compte pas utiliser). J'utiliserai la souris. Ou, peut-être regarder ici (mais je ne suis pas sûr qu'il y ait bien un XPT2046 et, en tout cas, les pins GPIO ne semblent pas correspondre... Mais cela vaudrait la peine de creuser la question et, éventuellement de faire une émulation de souris comme avec l'autre.

On peut déjà vérifier que P1-22 change d'état quand on touche l'écran avec WiringPi (apt-get install wiringpi) et la commande 'gpio readall' :
On voit également que les connexions annoncées par WaveShare correspondent effectivement à MISO, MOSI, SCLK et CE1. Le pas suivant est d'utiliser spincl(1) pour entrer en communication avec l'éventuel contrôleur XPT2046 (nécessite la libbcm2835).

En fait, spincl(1) est tellement simple qu'il est possible de tester la communication SPI directement en 'C', en quelques lignes. Quelque chose comme :
#include "bcm2835.h"                    

#define MAX_LEN 4
                                         
#define START   0x80    
#define XPOS    0x50
#define YPOS    0x10                    

int getit(int cmd)
    {
    char rbuf[MAX_LEN];
    char wbuf[MAX_LEN];
        
    memset(wbuf, 0, sizeof(rbuf));
    memset(rbuf, 0, sizeof(rbuf));
    wbuf[0] = cmd;
    bcm2835_spi_transfernb(wbuf, rbuf, sizeof(wbuf));
    return((rbuf[1]<<8)+rbuf[2]);
    }
int getxy(int *xp, int *yp)
    {
    bcm2835_spi_begin();
  
    bcm2835_spi_setDataMode(BCM2835_SPI_MODE0);
    bcm2835_spi_setClockDivider(BCM2835_SPI_CLOCK_DIVIDER_1024);
    bcm2835_spi_chipSelect(BCM2835_SPI_CS1);
    bcm2835_spi_setChipSelectPolarity(BCM2835_SPI_CS1, LOW);
        
    *xp = getit(START | XPOS); 
    *yp = getit(START | YPOS); 
        
    bcm2835_spi_end();
    return(1);
    }
donne des valeurs de XY dans l'intervalle 0..32768 quand l'écran est touché. Un petit programme qui envoie les données du XPT2046 dans /dev/uinput se trouve ici. À noter qu'un échange de 3 octets (et non 4) devrait suffire...

En fait, il est difficile de calibrer et ensuite de pointer exactement de petits 'contrôles' sur un écran 'résistif'. Il est probable qu'une émulation de souris (événements relatifs) soit plus efficace qu'une gestion avec des événements absolu. L'écran se comportant alors comme un 'touchpad'. Il n'y a plus qu'à...; le plus dur/incertain est fait.

À suivre...

dimanche 30 août 2015

Digistump de Digispark

Oups! j'en discute déjà sur enfantin...

Un petit bidule amusant : une petite carte Digistump avec juste un ATtiny85 qui communique en USB 1.1 (schéma). L'AVR fonctionne sur son oscillateur interne (pas de quartz) et gère l'USB par software (Littlewire). Un bootloader (micronucleus) permet de charger des applications (avec un utilitaire 'micronucleus'). On trouve des clones du bidule pour moins de deux euros sur eBay. Il faut noter que ce n'est pas tellement le prix qui est intéressant; on trouve des solutions USB 2.0 pour à peine plus cher. C'est l'idée... Digispark fournit un environnement 'Arduino' modifié (un 'blob' de 20 MB (?)) mais il y a moyen de s'en passer et d'utiliser les commandes de base. Ainsi, le Makefile :
blink.elf: blink.c
        avr-gcc  -std=c99 -Wall -Os -mmcu=attiny85 -o blink.elf blink.c

blink.hex: blink.elf
        avr-objcopy -j .text -j .data -O ihex blink.elf blink.hex

flash: blink.hex
        micronucleus --run blink.hex
permet de compiler et télécharger le programme suivant :
#include <avr/io.h>
#define F_CPU 16000000UL
#include <util/delay.h>

int main(void)
        {
        DDRB  |= _BV(PB1);

        for (;;)
                {
                _delay_ms(100);
                PORTB ^= _BV(PB1);
                }
        }
(à vérifier : que le CPU tourne bien à 16 MHz; voir ici pour l'idée de se passer de l'IDE Arduino)

Ce qui serait bien, c'est d'utiliser Little-Wire pour créer une application avec un 'moniteur' capable d'interpréter des commandes que l'on enverrait en utilisant minicom via /dev/ACM0 ... Ce n'est pas standard (USB/CDC n'est pas sensé fonctionner en USB 1.0) mais cela devrait fonctionner. Ce n'est pas trivial (Little-Wire est délicat à utiliser et la documentation USB/CDC-ACM n'est pas très lisible) mais cela devrait être possible. Les codes disponibles ne font que quelques centaines de lignes.

Ce qui serait bien aussi, ce serait d'explorer d'autres AVR implémentant la même solution... (en réduisant le nombre de composants au strict minimum (voir ici)

(à suivre...)