PWM 4 canaux en C sur Attiny2313

Juste un petit bout de code en passant. Pour ceux qui voudraient utiliser les 4 canaux de PWM des Amtel AVR Attiny2313, voici la configuration :
  DDRB |= (1<<PB2);               // make OC0A an output
  DDRD |= (1<<PD5);               // make OC0B an output
  DDRB |= (1<<PB3);               // make OC1A an output
  DDRB |= (1<<PB4);               // make OC1B an output

  TCCR0B = 0;                     // stop timer 0
  TCCR0A = (1<<WGM00)|(1<<WGM01); // select fast pwm mode 3
  TCCR0A |= (1<<COM0A1);          // Clear OC0A on Compare Match when up-counting.
                                  // Set OC0A on Compare Match when down-counting.
  TCCR0A |= (1<<COM0B1);          // Clear OC0B on Compare Match when up-counting.
                                  // Set OC0B on Compare Match when down-counting.
  OCR0A = 0x00;                   // duty cycle
  OCR0B = 0x00;                   // duty cycle
  TCCR0B |= (1<<CS00);            // no prescaling, timer on

  TCCR1B = 0;                     // stop timer 1
  TCCR1A = (1<<WGM12)|(1<<WGM10); // Fast PWM, 8-bit mode 5
  TCCR1A |= (1<<COM1A1);          // Clear OC1A on Compare Match when up-counting.
                                  // Set OC1A on Compare Match when down-counting.
  TCCR1A |= (1<<COM1B1);          // Clear OC1B on Compare Match when up-counting.
                                  // Set OC1B on Compare Match when down-counting.
  OCR1A = 0x00;                   // duty cycle
  OCR1B = 0x00;                   // duty cycle
  TCCR1B |= (1<<CS10);            // no prescaling, timer on
Il ne reste, ensuite qu’à changer la valeur de OCR0A, OCR0B, OCR1A ou OCR1B pour modifier le rapport de phase.

Base de travail pour différents projets à base d'AVR en C

Je travail sur différentes choses autour des microcontrôleurs Atmel AVR à l’heure actuelle. Je n’en dirai pas trop pour le moment, mais cela touche à la PWM et aux leds (comme souvent) ainsi qu’aux ports séries/USB. Dans le tas, il y a une signalisation lumineuse pour notre salle de réunion (dans la GLMFcave), le projet coccilight et un projet secret (tant qu’il sera pas terminé) qui manque encore d’éléments de décoration (bricoler un truc laid plein d’epoxy et de soudure c’est facile, arriver à quelque chose de présentable c’est une autre affaire).

Quoi qu’il en soit, j’ai dernièrement joué un peu avec un de mes vieux codes en assembleur Atmel que j’ai réimplémenté en C pour un AVR Attiny13 : la pulsation d’une led en PWM utilisant des valeurs sinusoïdales stockées dans la Flash du microcontrôleur.

Le stockage à proprement parler prend cette forme :
const uint8_t dasin[] PROGMEM = {
255,255,255,255,255,254,254,253,252,252,251,250,249,248,247,246,
245,243,242,240,239,237,236,234,232,230,228,226,224,222,220,218,
216,213,211,209,206,204,201,199,196,193,191,188,185,182,179,176,
174,171,168,165,162,159,156,152,149,146,143,140,137,134,131,128,
124,121,118,115,112,109,106,103,99,96,93,90,87,84,81,79,
76,73,70,67,64,62,59,56,54,51,49,46,44,42,39,37,
35,33,31,29,27,25,23,21,19,18,16,15,13,12,10,9,
8,7,6,5,4,3,3,2,1,1,1,1,0,0,0,0,
0,0,0,0,1,1,1,2,3,3,4,5,6,7,8,9,
10,12,13,15,16,18,19,21,23,25,27,29,31,33,35,37,
39,42,44,46,49,51,54,56,59,62,64,67,70,73,76,79,
81,84,87,90,93,96,99,103,106,109,112,115,118,121,124,127,
131,134,137,140,143,146,149,152,156,159,162,165,168,171,174,176,
179,182,185,188,191,193,196,199,201,204,206,209,211,213,216,218,
220,222,224,226,228,230,232,234,236,237,239,240,242,243,245,246,
247,248,249,250,251,252,252,253,254,254,255,255,255,255,255,255
};
Après quelques define préalables et autres choses indispensables en début de code…
#define F_CPU 2000000UL

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/delay.h>

#define PWMDDR     DDRB
#define PWMOUT     PB0
#define MCUCSR     MCUSR
#define DELAY      5

uint8_t mcucsr __attribute__((section(« .noinit »)));
…on peut s’attaquer à la fonction main :
int main (void) {
volatile uint8_t j;

TCCR0A = _BV(WGM00) | _BV(WGM01) | _BV(COM0A1) | _BV(COM0A0);
TCCR0B = _BV(CS00);

OCR0A = 0;

PWMDDR |= _BV(PWMOUT);

while (1) {
for(j=0;j<255;j++) {
OCR0A = pgm_read_byte(dasin+j);
_delay_ms(DELAY);
}
}
}
Voilà, c’est tout. La table des valeurs est le résultat d’un calcul détaillé dans un précédent billet. Rien d’extraordinaire dans tout cela, certes, mais comme j’ai un peu creusé dans le code d’autres personnes et sur Google, donc je fais tourner…

PWM logicielle sur Atmel AVR Attiny2313

Travaillant sur un projet de présentoire lumineux, j’ai eu besoin de disposer de plus que les quelques canaux de PWM que n’en proposent généralement les microcontrôleurs. En particulier les petits AVR comme l’Attiny2313. La seule solution restante était donc de se passer de ces fonctionnalités et d’implémenter la PWM en logiciel de manière à pouvoir piloter un minimum de 6 leds via les port E/S standards.

Le principe de fonctionnement est simple. En réglant l’un des timers de manière à obtenir le maximum de tops d’horloge possible, la plus haute fréquence donc, on peut implémenter, sous la forme d’un compteur et d’une poignée de comparaisons une PWM acceptable. Le début du code est tout ce qu’il y a de plus classique :
.device attiny2313

.def T1 = r1
.def T2 = r2
.def temp = r16
.def truc = r17
.def cpt = r18
.def savestatus = r20
.def zlow = r21
.def zhigh = r22
.def pwm1 = r19
.def pwm2 = r23
.def pwm3 = r24
.def pwm4 = r25
.def pwm5 = r26
.def pwm6 = r27

.org 0x0000
rjmp RESET
.org 0x000D
rjmp TIM0A_CMP
.org 0x0013
Vient ensuite la routine d’interruption correspondant à la comparaison de timer. Ici on trouve cpt, le compteur. Il est incrémenté à chaque interruption puis sa valeur est comparée aux variables réglant le PWM pour chaque sortie. Tant que le compteur est en dessus de la valeur d’une variable la sortie correspondante est active. Si le compteur est au delà, la sortie est à la masse. Notez qu’il faut penser à sauvegarder le registre d’état car les comparaisons successives risquent de perturber les drapeaux et donc de nuire au bon fonctionnement du reste du code :
TIM0A_CMP:
in savestatus,SREG
inc cpt

cp cpt,pwm1
brlo lowrouge
cbi PORTB,PB4
rjmp vert
lowrouge:
sbi PORTB,PB4
vert:
cp cpt,pwm2
brlo lowvert
cbi PORTB,PB3
rjmp bleu
lowvert:
sbi PORTB,PB3
bleu:
cp cpt,pwm3
brlo lowbleu
cbi PORTB,PB2
rjmp uv
lowbleu:
sbi PORTB,PB2
uv:
cp cpt,pwm4
brlo lowuv
cbi PORTB,PB1
rjmp blanc
lowuv:
sbi PORTB,PB1
blanc:
cp cpt,pwm5
brlo lowblanc
cbi PORTB,PB0
rjmp jaune
lowblanc:
sbi PORTB,PB0
jaune:
cp cpt,pwm6
brlo lowjaune
cbi PORTB,PB5
rjmp fin
lowjaune:
sbi PORTB,PB5
sbi PORTB,PB5
fin:
out SREG,savestatus
reti
On définit également deux petites routines de retardement :
mini:
clr T1
hop:
dec T1
brne hop
dec temp
brne hop
ret

micro:
dec temp
brne micro
ret
Enfin, on arrive au reste où il faut, dans l’ordre, définir la pile, utiliser l’octet de calibration propre à chaque AVR, placer les ports en sortie, définir l’effacement du timer en comparaison, le prescaling, le niveau de référence pour la comparaison et le déclenchement d’une interruption en comparaison :
RESET:
ldi temp, 0x70
out SPL, temp

ldi temp,0x52
out OSCCAL,temp
ldi temp,0xf0
rcall mini

sbi DDRB,PB0
sbi DDRB,PB1
sbi DDRB,PB2
sbi DDRB,PB3
sbi DDRB,PB4
sbi DDRB,PB5

ldi temp,(1<<WGM01)
out TCCR0A,temp

ldi temp,(1<<CS00)
out TCCR0B,temp

ldi temp,0x02
out OCR0A,temp

ldi temp,(1<<OCIE0A)
out TIMSK,temp

sei
Arrive ensuite la boucle principale où l’on pioche dans la mémoire flash les valeurs des variables pwm*. Cette boucle sera interrompue par les interruptions du timer. Cela manque de précision mais est largement suffisant pour faire pulser des leds proprement :
loop: ldi ZH, high(tbpwm1*2)
ldi ZL, low(tbpwm1*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm1,r0

ldi ZH, high(tbpwm2*2)
ldi ZL, low(tbpwm2*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm2,r0

ldi ZH, high(tbpwm3*2)
ldi ZL, low(tbpwm3*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm3,r0

ldi ZH, high(tbpwm4*2)
ldi ZL, low(tbpwm4*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm4,r0

ldi ZH, high(tbpwm5*2)
ldi ZL, low(tbpwm5*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm5,r0

ldi ZH, high(tbpwm6*2)
ldi ZL, low(tbpwm6*2)
add ZL, zlow
adc ZH, zhigh
lpm
mov pwm6,r0

inc zlow
inc zlow

ldi temp,0x50
rcall micro
rcall mini

rjmp LOOP
Enfin, pour terminer, nous avons les valeurs stockées. Ici 6 sinus identiques légèrement décalés les uns par rapport aux autres :
tbpwm1:
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125

tbpwm2:
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10

tbpwm3:
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40

tbpwm4:
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179

tbpwm5:
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246

tbpwm6:
.db 218,220,222,224,226,228,230,232,234,235,237,239,240,241,243,244
.db 245,246,248,249,250,250,251,252,253,253,254,254,254,254,254,254
.db 254,254,254,254,254,254,254,253,253,252,251,250,250,249,248,246
.db 245,244,243,241,240,239,237,235,234,232,230,228,226,224,222,220
.db 218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179
.db 177,174,171,168,165,162,159,156,153,150,147,144,140,137,134,131
.db 128,125,122,119,116,112,109,106,103,100,97,94,91,88,85,82
.db 79,77,74,71,68,65,63,60,57,55,52,50,47,45,43,40
.db 38,36,34,32,30,28,26,24,22,21,19,17,16,15,13,12
.db 11,10,8,7,6,6,5,4,4,4,4,4,4,4,4,4
.db 4,4,4,4,4,4,4,4,4,4,5,6,6,7,8,10
.db 11,12,13,15,16,17,19,21,22,24,26,28,30,32,34,36
.db 38,40,43,45,47,50,52,55,57,60,63,65,68,71,74,77
.db 79,82,85,88,91,94,97,100,103,106,109,112,116,119,122,125
.db 128,131,134,137,140,144,147,150,153,156,159,162,165,168,171,174
.db 177,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216
Bien entendu, ce code n’est qu’une première ébauche. Il est possible de condenser le code de l’interruption et de la boucle principale pour économiser du temps et de la mémoire. De plus il faut prendre en considération que ce code occupe déjà quelques 85% de la mémoire flash d’un Attiny2313. En voulant utiliser plus de motifs ou plus de sorties, on se heurte à un problème d’espace. Il me faudra donc trouver un autre support de stockage, comme une carte MMC, pour pousser plus loin.

Attiny15, PWM, led qui pulse, Sinus et GNU bc

Le microcontrôleur AVR Attiny15 offre une fonctionnalité pour faire de la PWM (Pulse-width modulation) ou de modulation de largeur d’impulsions en bon français. Ceci permet, par exemple, de faire varier l’intensité lumineuse d’une Led ou la vitesse d’un moteur. Pour cela, on utilise dans le code de l’Atiny15 une valeur entre 0 et 255. Pour obtenir une fluctuation d’intensité qui donne l’impression que la Led pulse (comme sur les Mac en veille) il faut utiliser une série de valeurs sinusoïdale. C’est là que GNU bc intervient et se révèle sous sa vrai nature : bien plus qu’une calculette en ligne de commande, un véritable langage.

Nous avons besoin de 256 valeurs hexadécimales entre 0 et 255 qui soient répartie à la façon d’un sinus. Hors de question d’utiliser un simple compteur de 255 à 0 puis de 0 à 255. Nous n’obtiendrons pas une pulsation ainsi. Il nous fait un sinus. Ca, bc, nous le donne de bon coeur avec la fonction s(x). Il nous faut ensuite décaler le sinus sur x de pi/2 (1) et sur y de 1 pour ne pas avoir de valeurs négatives. Nous calculons donc :
((s(3.14159*(0.5+((2/256)*a)))+1)/2)*256)
Aaaaah ! Du calme, découpons :

a : c’est notre valeur de base entre 1 et 256
Nous avons besoins de deux sommets du sinus. Le premier est à pi/2 et le suivant à pi fois 2.5. A ces deux points sur x nous avons 1 sur y. Nous avons donc 2 fois pi sur x entre les deux sommets. 2 que nous découpons en 256 tranches. Le premier point est pi fois 0.5 plus 2/256 fois 1. Le dernier est pi fois 0.5 plus 2/256 fois 256. Donc : 3.14159*(0.5+((2/256)*a))
Nous n’oublions pas d’ajouter 1 au résultat pour « remonter » le sinus variant alors entre 0 et 1 en non plus -1 et 1.
Notre sinus fait 2 « de haut » on « ratatine » en divisant par 2

Enfin pour obtenir un sinus entre 0 et 255 on multiplie par 256 (l’arrondis se chargera du reste)

L’arrondis, justement. Il n’y a pas d’arrondis dans bc (enfin pas exactement). Nous pouvons jouer sur la variable scale permettant de définir le nombre de décimales significatives. Si nous utilisons scale=0 notre 3.14159 devient 3 et le calcul est complètement faussé. Donc….

Première démonstration de la puissance de bc. On créé simplement la fonction qui nous manque :
define i(x) {
auto s
s = scale
scale = 0
x /= 1
scale = s
return (x)
}
Fonction très simple prenant en argument une valeur et retournant la même divisée par 1. On change au passage la valeur de scale ce qui élimine les décimales. Résultat, après avoir enregistré cette fonction dans un fichier toto.bc :
$ echo « i(10/3) » | bc -l toto.bc
3
Bingo ! Et là vous vous dites, « il ne reste plus qu’à mettre une bonne couche de shell et » :
for ((i=1;i<=256;i+=1));
do echo « obase=16;i(((s(3.14159*(0.5+((2/256)*$i)))+1)/2)*256) » |
bc -l toto.bc;
done
Rappelons que obase permet de spécifier la base en sortie (16 = hexa).

Et bien non ! Si bc permet à l’utilisateur de créer des fonctions il doit bien être capable de supporter des boucles. Nous ajoutons donc, dans notre toto.bc :
obase=16

auto a
for(a=1;a<=256;a=a+1) {
i(((s(3.14159*(0.5+((2/256)*a)))+1)/2)*256)
}
quit
Et voilà. Il ne nous reste plus qu’à faire :
$ bc -l toto.bc
FF
FF
FF
FF
FF
FE
FE
FD
FC
FC
FB
FA
[…]
FB
FC
FC
FD
FE
FE
FF
FF
FF
FF
FF
FF
Voilà notre beau sinus pour notre Attiny15.

Note sur la PWM : la modulation de largeur d’impulsions est une technique pour faire varier l’intensité (Led) ou la vitesse (moteur) d’un composant qui ne peut pas être contrôlé en variant la tension ou le courant. Dans le cas d’une Led, elle a besoin de 10mA pour fonctionner (selon le modèle) et fait apparaître une tension à ses bornes (+/- 2 Volts selon le modèle). Pour faire varier l’intensité lumineuse, il faut la faire clignoter très rapidement, notre oeil fera le reste. Avec la PWM on fait clignoter la Led à une fréquence fixe et on joue sur le rapport cyclique. 100% d’intensité ce sont des « clignotements » toujours allumés. 0%, toujours éteint. 50% un « clignotement » sur deux est allumé. 10% c’est un sur 10, etc. La Led ne clignote jamais plus ou moins vite, on passe simplement des « tranches » à « éteint ». Avec un Attiny15 on règle le rapport cyclique avec une valeur entre 0 et 255 dans le registre OCR1A. Ce sont donc les valeurs de ce registre que nous avons calculé avec bc.