mardi 18 août 2015

NTSC et Parkinson

Dans l'article précédent intitulé téléviseur conformiste je présentait un problème et sa solution concernant la génération d'un signal NTSC sur ATmega328P. Mais la solution présentée n'était pas optimum, il y avait un tremblement dans l'image. Aujourd'hui j'ai réglé ce problème et dans cet article j'explique la cause de ce tremblement et la solution que j'ai apportée.

Vidéo avant la correction du problème on voit le tremblement de l'image surtout apparent sur la ligne verticale à droite de l'écran.

Vidéo après correction du problème. Maintenant l'image est très stable. Il n'y a plus de Parkinson.

La gigue d'interruption

J'ai tapé dans google search: traduire: interrupt jitter et c'est la traduction que j'ai obtenu!

Le générateur de signal NTSC utilise une interruption pour modifier les paramètres du signal PWM qui génère la synchronisation mais aussi pour déterminer quand il est temps d'envoyer les pixels vidéo pour chaque ligne visible à l'écran. En ce qui concerne la synchronisation l'interrupt jitter n'affecte pas son fonctionnement puisque cette synchronisation utilise le compteur TCNT1 dont le fonctionnement est indépendant des interruptions. Par contre pour l'envoie des pixels vidéo il y a une influence. Il est primordial que l'envoie du signal vidéo débute toujours au même instant à chaque ligne ainsi qu'à chaque répétion de frame.

Le problème est que le temps de réponse de l'interruption est variable car il est influencé par la durée de l'exécution de l'instruction machine en cours au moment où se produit l'interruption. En effet le MCU doit attendre la fin de l'instruction en cours avant de répondre à l'interruption. Hors sur les AVR le temps d'exécution d'une instruction varie entre 1 et 4 cycles. La durée d'un pixel dans ce générateur vidéo est de 6 cycles machines. Donc une fluctuation de 4 cycles sur le départ de la ligne vidéo c'est 2/3 de pixel en largeur. C'est cette fluctuation qu'on perçoit dans l'image.

Comment régler ce problème

Comme je l'ai dit le fonctionnement du compteur TCNT1 est indépendant des interruptions on peut donc se fier à son fonctionnement pour avoir l'heure juste, si je peut dire. Dans cette application le compteur est cadencé à la même fréquence que le coeur du MCU. Donc lorsqu'il est temps de générer les pixels vidéo d'une ligne sa valeur est toujours la même à 4 cycles près. Le code assembleur que j'ai ajouté lit l'octet faible de TCNT1 qui est un compteur de 16 bits et ne garde que les 2 bits les plus faibles, donc cette valeur peut variée de 0 à 3 du au jitter. Je prends pour cible la valeur 3 et si le compte est inférieur à 3 on ajoute autant d'instruction NOP que nécessaire pour que lorsque le programme arrive au code qui génère le vidéo le compte de TCNT1 soit toujours le même. Puisque c'est TCNT1 qui détermine le début de la ligne vidéo, à compte égal de celui-ci on est à la même position sur la ligne vidéo.

Pour obtenir un timing précis j'ai du écrire le code en embedded assembly. Voici le code ajouté à la routine d'interruption vidéo TIMER1_COMPB_vect dans tvout.c.


 asm(// élimination de la gigue d'interuption (jitter)
 "ldi r30,lo8(jit)\n"
 "ldi r31,hi8(jit)\n"
 "clc\n"
 "ror r31\n"
 "ror r30\n"
 "lds r24,0x84\n"
 "andi r24,3\n"
 "add r30,r24\n"
 "adc r31,r1\n"
 "ijmp\n"
 "jit:"
 "nop\n"
 "nop\n"
 "nop\n"
 "nop\n"
 );
Ce code fonctionne de la façon suivante. l'adresse de l'étiquette jit (Just In Time) est chargée dans le registre d'index Z qui est en fait la concaténation des registres R31:R30 pour former un pointeur de 16 bits. Les 3 instructions suivantes divisent cette valeur par 2 parce que cette adresse est celle d'un octet mais le compteur ordinal du ATmega328P lui pointe sur des mots de 16 bits. Autrement dit si l'adresse de jit est 21590 en mémoire flash, pour que le PC pointe cette adresse il doit contenir 10795. Une fois le pointeur Z ajusté à la valeur désirée. On fait la lecture de l'octet faible de TCNT1 qui se trouve à l'adresse 0x84 et on met cette valeur dans le registre R24. L'instruction andi r24,3 à pour but de mettre tous les bits de R24 à zéro sauf les 2 plus faibles. On a maintenant dans R24 un nombre entre 0 et 3. On additionne ce nombre à la valeur dans le registre d'index Z. L'instruction ijmp prend la valeur dans Z et la transfert dans PC (le pointeur d'instruction). Si R24 contenait 0 l'instruction qui sera exécutée après le ijmp sera celle exactement à l'adresse jit. Si R24 contenait 1 l'instruction qui sera exécutée après le ijmp sera à l'adresse jit+2, etc. jit+2 parce que n'oubliez pas que le PC adresse des mots de 16 bits et non des octets et comme chaque instruction NOP occupe 2 octets on a le bon alignement.

Au bout du compte lorsque le PC arrive à l'instruction qui suis le dernier NOP, le compteur TNCT1 a toujours la même valeur puisqu'il est incrémenté au même rythme que le compteur ordinal (pointeur d'instruction).

routine TIMER1_COMPB_vect

Voici la routine d'interruption au complet. Je rappelle que ceci fait parti du projet open sourde CHIPcon v2.

jeudi 13 août 2015

Tétéviseur conformiste

J'ai conçu plusieurs projets générant un signal NTSC monochrome et les ai branchés sur 3 téléviseurs différents avec succès. Mais voilà que dernièrement j'ai du remplacer mon téléviseur par un de modèle RCA. Comme cette semaine j'ai repris le projet CHIPcon dans le but de remplacer le clavier hexadécimal par un joystick Atari 2600. Je branche CHIPcon v2 sur mon téléviseur RCA... pas de vidéo! Pourtant il fonctionne très bien sur mon petit moniteur de développement. Je n'ai pas été long à figurer d'où provenait le problème.

Standard NTSC

En fait la sortie NTSC que j'utilise n'est pas conforme au standard. Il s'agit d'une version simplifiée appellée balayage progressif sans alternance de champ (field). De plus la synchronisation verticale est aussi simplifiée et consiste à simplement rallonger l'impulsion de synchronisation pour les lignes 1 à 3.

Le standard NTSC lui définie une alternance de field pair et impair. Chaque field ayant chacun 262,5 lignes pour un total de 525 lignes par image puisque les lignes des champs pair et impaire sont inter-lacées. Ceci a pour but d'éviter le scintillement de l'image tout en réduisant la bande passante requise par le signal vidéo puisqu'il n'y a que 262,5 lignes de transmise par champ. Chaque champ n'est affiché que 30 fois par seconde en alternance pair/impair même si le balayage vertical est à la fréquence de 60 fois par seconde. Astucieux!

Dans la version que j'utilise il n'y a que 262 lignes répétées 60 fois par secondes sans inter-lacement.

Solution au problème

Heureusement la solution à ce problème était simple et consistait à une modification de la routine d'interruption vidéo ISR(TIMER1_COMPB_vect) située dans le fichier tvout.c Voici la version originale présentée dans CHIPcon et la version conforme à NTSC corrigée pour CHIPcon v2.

NTSC balayage progressif

NTSC conforme

Notez que ceci n'augmente pas la résolution verticale qui demeure à 2621 lignes puisque le même video_buffer est utilisé pour les 2 champs pair et impair.

conclusion

La morale de cette histoire est que si vous voulez que votre projet fonctionne sur tous les téléviseurs il vaut mieux qu'il se conforme au standard NTSC, car on ne sais jamais quand on aura affaire à un téléviseur pointilleux.


NOTES:

  1. Les 262 lignes ne sont pas toutes visibles, 21 de ces lignes font partie du vertical retrace. De plus sur les téléviseurs que j'ai essayé il y a environ 230 lignes visibles en balayage progressif. En respectant le standard et en utilisant l'entre-lacement on obtiendrait donc 460 lignes au coup d'un buffer vidéo 2 fois plus grand.
  2. m.à.j. 2015-08-18, j'avais oublié le break; pour le case 272:. De plus j'ai supprimé le test if(!even) qui était inutile car si even est vrai line_counter est mis à -1 avant d'atteindre 272.

jeudi 6 août 2015

trivia

Combien coûtait un disque dur de 26Mo en 1980?

réponse: 4995US$

Cette information me viens d'une publicité parue à la page 53 de BYTE édition du mois d'août 19801. La publicité qualifie ce prix de HARD BARGAIN. Toute une aubaine! Et ce sont des dollars de 1980. Si on tiens compte de l'inflation en me fiant à l'information trouvé ici, ça ferais en dollars d'aujourd'hui 14 350US$2.


notes

  1. Cette édition de BYTE consacré à FORTH peut-être téléchargée à partir de ce lien.
  2. Pour l'année 1980 j'ai utilisé le ratio <inflation ajusted $>/<nominal $> * 4995

mercredi 5 août 2015

Arduino sketch et C++

Cette Article est une réaction à l'article d'Elliot Williams paru sur Hackaday.com le 28 juillet 2015. La question est la suivante: est-ce qu'un sketch Arduino est un programme C++?

L'argument d'Elliot Williams est que derrière la scène, l'environnement Arduino est basé sur C++ et que la syntaxe des sketchs Arduino est la même que la syntaxe de C++. Un sketch Arduino est simplement pré-traité pour y apporter les éléments manquants pour en faire un programme C++ compilable par GCC.

Arduino reference

Jetons un coup d'oeil au manuel de référence du langage Arduino. En effet tous les éléments de la syntaxe sont les même que ceux du C++. Mais est-il question de classes, objets, etc? non.

Le langage Arduino est un sous-ensemble du langage C++. Un sous-ensemble d'un ensemble n'est pas l'ensemble lui-même.

Si on ajoute des éléments du C++ qui ne sont pas dans la référence du langage Arduino comme par exemple des classes d'objets et des instances d'objets, le script va compiler correctement donc c'est du C++ dit M. Williams.

Personnellement je considère que tout fichier qui a besoin d'être pré-traité avant d'être compiler avec gcc n'est pas du C++ à proprement parler même si les éléments manquant peuvent-être ajoutés manuellement dans le fichier sketch. par exemple les prototypes de fonctions ne sont pas requis dans la référence du langage Arduino mais ils le sont par C/C++. Arduino utilise donc un pré-traitement qui lui est propre et qui sert entre autre à ajouter ces prototypes de fonctions ainsi qu'à ajouter un #include pour la librairie Arduino.

exemple

Voici un exemple d'un sketch Arduino auquel j'ai ajouter une classe C++. le pré-traitement Arduino transforme le sketch en fichieer .cpp suivant:

On voit que le pré-traitement ajoute des directives #line ainsi que la directive #include "Arduino.h" ainsi que les prototypes void setup(); et void loop();

Pourquoi est-ce que j'insiste sur ces détails? A cause de la philosophie d'Arduino. Arduino a été créé pour simplifier l'utilisation des microcontrôleurs pour les rendre accessibles aux non spécialistes. C'est ce qui fait tout l'intérêt d'Arduino. Les spécialistes n'utilisent généralement pas Arduino, car ils n'en ont tout simplement bas besoin et pour eux l'IDE Arduino est d'une utilisation frustrante car cet environnement simplifié n'offre pas les outils auxquels ils sont habituer.

Donc lorsque certains comme M. Williams, veulent expliquer aux utilisateurs d'Arduino qu'en fait ceci c'est du C++ alors moi je dis qu'ils non pas compris l'objet du projet Arduino: garder ça le plus simple possible pour que les amateurs puisse utiliser la plateforme avec une difficulté d'apprentissage minimisée. Si éventuellement ces amateurs deviennent des professionnels ils n'auront plus besoin d'Arduino.

mardi 4 août 2015

comprendre les pointeurs

Lorsque j'ai appris le langage C il y a longtemps l'un des concepts qui m'a donné le plus de difficulté est celui concernant l'usage des variables pointeurs. Cet article est à l'usage de ceux qui débutent en C est qui auraient la même difficulté.

coup d’œil sur les registres interne d'un MCU

Prenons un MCU quelconque, par exemple un PIC12F1822. Si on regarde le modèle de programmation du cœur de ce MCU on voit ceci.

Les registres FSR0 et FSR1 sont ce qu'on appelle des registres d'index, c'est à dire des variables pointeurs. Supposons qu'on écris un programme en mpasm et qu'on veut initialiser la RAM commune à zéro que fait-on?


;sous-routine pour effacer ram commune 0x70-0x7f
clear_common_ram:
;initialistion du pointeur
    clrf FSR0H
    movlw H'70'
    movwf FSR0
    clrf WREG 
clear_loop:
    movwi, FSR0++ ; adressage indirect avec post-incrément
    btfss FSR0,7
    bra clr_loop
    return ; terminé car FSR0==0x80
On pourrais aussi faire ceci sur un PIC baseline:

;sous-routine pour effacer ram commune 0x70-0x7f
clear_common_ram:
    movlw H'70'
    movwf FSR0
clear_loop:
    clrf FSR0 ; adressage indirect
    incf FSR0,F ; incrémente le pointeur
    btfss FSR0,7
    bra clr_loop
    return ; terminé car FSR0==0x80
On a mis 0 dans le registre WREG et on transfert cette valeur dans toutes les localisations RAM entre 0x70 et 0x7F. Pour ce faire on se sert du registre d'index FSR0. FSR0 est une variable pointeur. FSR0 ne contient pas un entier mais une adresse et cette adresse indique où on veut mettre la valeur de WREG. l'instruction movwi FSR0++ effectue 2 opérations. D'abord elle transfert le contenu de WREG qui est zéro à l'adresse RAM indiquée par le contenu de FSR0 et deuxièmement incrémente de 1 la valeur de FSR0 pour pointé à l'adresse suivante. Le programme boucle sur clear_loop tant que le bit 7 de FSR0 est à 0 car si le bit 7 est à 1 c'est que FSR0 pointe sur une adresse après 0x7F.

la même chose en C

répétons le même programme en C


void clear_common_ram(){
    char *p;
    p=(char*)0x70;
    while(((int)p&0x80)==0){
        *p++=0; 
    }           
}

variable pointeur

Dans tout langage de programmation une variable est une adresse en mémoire dans laquelle une information est conservée. Dans l'exemple ci-haut p ne fait pas exception, c'est une adresse mémoire dans laquelle on va garder une information mais cette information n'est pas un char mais une adresse qui indique un autre endroit en mémoire ou est conservé une donnée de type char. Ça a peut-être l'air plus compliqué que la version mpasm mais c'est presque la même chose. Lorsqu'on déclare la variable p l'étoile * avant le p informe le compilateur que cette variable va contenir une autre adresse et non un char.

l'instruction *p++=0; indique qu'on veut modifier le contenu de l'adresse pointée par p et non le contenu de p lui-même. Ici l'étoile * n'a pas la même signification que lorsqu'on déclare la variable. Elle signifie plutôt un adressage indirect. Dans de nombreux assembleurs l'adressage indirect (par pointeur) est indiqué par le nom du registre qui sert de pointeur entre crochet comme ceci:


    LOAD R0,[R15]
Ce qui signifie met la valeur qui se trouve à l'adresse indiquée par le contenu de R15 dans R0. Donc ici R0 est une variable ordinaire tandis que R15 est une variable pointeur (registre d'indexation ou d'indirection).

Quand le programme compilé va s'exécuter, il va aller chercher la valeur de p et la mettre dans FSR0 ou FSR1. ensuite il va mettre WREG à zéro et effectuée une boucle très semblable à celle d'écrite ci-haut dans la version mpsasm.

J'ai fait un test dans MPLAPX en compilant avec XC8 free (pas d'optimisation) voici le résultat dans le disassembly listing


11:            void clear_common_ram(){
12:                char *p;
13:                p=(char*)0x70;
07ED  3070     MOVLW 0x70
07EE  00F0     MOVWF __pcstackCOMMON
07EF  3000     MOVLW 0x0
07F0  00F1     MOVWF 0x71
14:                while(((int)p&0x80)==0){
07F1  1BF0     BTFSC __pcstackCOMMON, 0x7
07F2  0008     RETURN
07FC  2FF1     GOTO 0x7F1
15:                    *p++=0;
07F3  0870     MOVF __pcstackCOMMON, W
07F4  0086     MOVWF FSR1
07F5  0871     MOVF 0x71, W
07F6  0087     MOVWF FSR1H
07F7  0181     CLRF INDF1
07F8  3001     MOVLW 0x1
07F9  07F0     ADDWF __pcstackCOMMON, F
07FA  3000     MOVLW 0x0
07FB  3DF1     ADDWFC 0x71, F
16:                }
17:            }
Ça a l'air plus compliqué sans optimisation! __pcstackCOMMON est une pseudo-pile créé par le compilateur. On peut s'en passer et simplifier ça. Il n'est pas nécessaire non plus de stocker le pointeur FSR1 sur la pile __pcstackCOMMON.

clear_common_ram:
; initialisation du pointeur p
; le compilateur a choisi FSR1 comme pointeur
; les FSR du PIC12F1822 sont de 16 bits
    movlw 0x70
    movwf FSR1L
    movlw 0
    movwf FSR1H
clear_loop:
    btfsc FSR1L,7
    return
    clrf INDF1
    ; fsr1 a 16 bits, incrément par addition de 1
    movlw 1
    addwf FSR1L
    movlw 0
    addwfc FSR1H
    goto clear_loop

Lorsqu'on déclare un tableau en C i.e. char tableau[10];, le compilateur va aussi utiliser un registre d'index pour atteindre les éléments de ce tableau, i.e. b=tableau[2]; se traduis en assembleur par:


;initialise le pointeur
   movlw LOW tableau
   movwf FSR0L
   movlw HIGH tableau
   movwf FSR0H
;va cherché la valeur dans le 3ième élément du tableau.
   moviw 2[FSR0]; WREG=indirection:[FSR0+2]
   movwf  b  ; assigne cette valeur à b

adresse d'une variable ordinaire

Si l'étoile avant le nom d'un pointeur sert à indiquer une indirection,on dit déréférencer, comment fait on pour obtenir l'adresse mémoire où est stocké le contenu d'une variable ordinaire:


int b=0, *p;

   p=&b;
   *p=34; // que contient b?
Le symbole & est utilisé pour indiquer l'adresse de la variable et non son contenu. donc dans cette exemple p contient l'adresse de la variable b. Et lorsqu'on déréférence p pour y assigner la valeur 34 et bien la valeur de la variable b devient 34 car p pointe vers b.

Que ferais-je sans toi

En effet comment ferait-on sans pointeurs pour adresser un tableau ou n'importe qu'elle étendu de mémoire? J'espère donc que cette présentation a clarifié la question des pointeurs.