Jak na softwarové PWM pomocí timeru a por. registru OCR?

Zdravím, už je to nějakou dobu co jsem sem psal a sháněl nějaký návod na 4 kanálové PWM vytvořené softwarově. To co mi pan kolega
Showlee, kterému za to ještě jednou děkuju, už ovšem nestačí. PWM funguje na frekvenci 244Hz a po připojení na výkonové LED je vidět jemné “poblikávání”. Proto jsem se rozhodl program upravit, zatím bohužel neúspěšně.
Myšlenka však zůstává stejná, čitač, který načítá vzhůru a pomocí porovnávacího registru se porovnává aktuální hodnota s nastavenou a při shodě se vyvolá přerušení ve kterém se provede nastavení kanálu do log.1. Kanály musí být ovšem seřazeny postupně ,aby mohlo během načítání čitače dojít k nastavení všech 4 kanálů(proto pole [hodnota,pozice]). Při přetečení čítače se všechny kanály shodí zpět na 0 a začne se znovu.To vše na frekvenci cca 1kHz, a OC proto, aby PWM generovalo co nejmenší počet přerušení.
Bohužel realita je krutá a tak jak jsem si myslel ,že by to mohlo fungovat, tak to zatím nefunguje. Zde přikládám kód(boublesort jsem vyřadil a hodnoty si nastavil vzestupně už dopředu):

#define F_CPU 16000000UL

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "uart.h"

//.................SW-PWM.........................
#define REG_TCNT0	0 // frekvence PWM

#define BITPOS_0 	0	  
#define BITPOS_1 	1
#define BITPOS_2 	2
#define BITPOS_3 	3
#define PORT_1 	PORTB

#define SET_BIT(BYTE,BIT)(BYTE|=(1<<BIT))
#define CLEAR_BIT(BYTE,BIT)(BYTE&=~(1<<BIT))
#define CHECK_BIT(BYTE,BIT)(BYTE&=(1<<BIT))

// Deklarace globalnich promennych
//volatile unsigned char Kanal1 = 0,Kanal2 = 0,Kanal3 = 0,Kanal4 = 0;
volatile unsigned char Kanal[4][2]={ {0,1} , {10,2} , {230,3} , {254,4} };
volatile unsigned char pozice=0;

ISR(TIMER0_COMP_vect)//přerušení pri shode registru
{
	// jestlize promenna [cislo][x] je 1 nastav kanal1
	do{
	
	SET_BIT(PORT_1,(Kanal[pozice][1])); //nastaví příslušný bit do 1
	pozice++;							//posun na další kanál
	
	}while(Kanal[pozice-1][0]==Kanal[pozice][0]);	//opakuje nastaveni pokud se hodnoty rovnají
	OCR0=Kanal[pozice][0];							//nastaví další kanál
	if(pozice==3){pozice=0;}						//nulování 
	
}

ISR(TIMER0_OVF_vect)//přerušení pri přetečení
{
CLEAR_BIT(PORT_1,BITPOS_0); // nuluje piny 0
CLEAR_BIT(PORT_1,BITPOS_1); // nuluje piny 1
CLEAR_BIT(PORT_1,BITPOS_2); // nuluje piny 2
CLEAR_BIT(PORT_1,BITPOS_3); // nuluje piny 3

OCR0=Kanal[0][0];
}

void bubbleSort(void)
{
int i, j;
unsigned char temp,temp2; 

  for (i = (4 - 1); i > 0; i--)
  {
    for (j = 1; j <= i; j++)
    {
      if (Kanal[j-1][0] > Kanal[j][0])
      {
        temp = Kanal[j-1][0];
		temp2 = Kanal[j-1][1];
        Kanal[j-1][0] = Kanal[j][0];
		Kanal[j-1][1] = Kanal[j][1];
        Kanal[j][0] = temp;
		Kanal[j][1] = temp2;
      }
    }
  }
}

//HLAVNI PROGRAM
int main (void){ 

	sei(); 			//povolení globalniho přerušení

PORTA=	0xFF;		//PORT A - 1 na vystupu (pro tlačitka)
DDRA=	0x00;		//PORT A - Vstupy 
PORTB = 0x00;  		// LED
DDRB = 	0xFF;   	//Port B - jako vystup výkonových LED Kanálu
PORTC=	0xFF;		//PORT C - 1 na vystupu (zhasnou led)	
DDRC = 	0xFF;   	//PORT C - Vystupy7
bubbleSort();
 // Nastaveni TIMER0 a Preruseni (Timer0 zajistuje softwarove PWM)
  TCNT0 = REG_TCNT0;
  TIMSK |= (1<<TOIE0)|(1<<OCIE0) ; 		// Preruseni pri preteceni a  shodě
  TCCR0 |= (1<<CS01)|(1<<CS00); 			// Normalni rezim,delicka f/64

	for(;;)
	{

	}

}

Pokud by v tom někdo viděl zásadní problém byl bych rád za jakoukoli pomoc. Jinak mě napadlo když mám volný 16 bit č/č udělat to pomocí něj ten má přece jenom 2 OC registry ale zatím se mi nepodařilo rozběhnout nějakým způsobem toto tak se nechci pouštět do jiného.

Asi som to nejako zle pochopil, ale ty menis prerusenie od OCR0 v zavislosti na kanali?


ISR(TIMER0_COMP_vect)//přerušení pri shode registru
{
   // jestlize promenna [cislo][x] je 1 nastav kanal1
   do{
   
   SET_BIT(PORT_1,(Kanal[pozice][1])); //nastaví příslušný bit do 1
   pozice++;                     //posun na další kanál
   
   }while(Kanal[pozice-1][0]==Kanal[pozice][0]);   //opakuje nastaveni pokud se hodnoty rovnají
   OCR0=Kanal[pozice][0];                     //nastaví další kanál
   if(pozice==3){pozice=0;}                  //nulování
   
} 

tomuto som teda vobec neporozumel. Preco by sa Kanal[pozice-1][0]==Kanal[pozice][0] ? Ved tak ako tie data naplnis, podmienka nebude splnena. Prebublavanie som neskumal, mozno je tam nejaka odpoved troch patracov. :slight_smile:
To si myslel tak, ze budes posuvat prerusenie od OCR0 smerom k vyssej hodnote? To nie je zle vymyslene :slight_smile:

Jednoduche SW PWM spravis velmi velmi jednoducho
Trimer si nastav aby pocital iba do OCR0 a potom sa resetol a pocital od zaciatku. Ak chces, aby LED ocividne neblikali, potom musia mat frekvenciu aspon 100Hz a viac. Nech je to napriklad 100Hz.
Ak chcem mat 100Hz s jemnostou 255 krokov (0 je tma, tam na frekvencii PWM moc nezalezi a na 255 tiez nie :slight_smile: ), potom musim byt pripraveny spravit zmenu na pine MCU kazdych 39.06us. Za tu dobu spravi procesor na 14745600MHz cca 576 az 288 instrukcii, takze uplne v pohode mas casu dost aj na ine veci.

Casovac si nastav tak aby k preruseniu doslo 1x za menej ako 40us.


#define POCET_KANALOV 4 // ale kludne aj 12,len si potom prislusne uprav kod :-)

volatile static uint8_t hodnota_predvolba_kanala[POCET_KANALOV];
volatile static uint8_t cnt_pwm = 0;

ISR(TIMER0_COMP_vect)//přerušení pri shode registru 

{
   
    if (hodnota_predvolba_kanala[0] <=cnt_pwm) RES_VYST_BIT0;
    else SET_VYST_BIT0;
    if (hodnota_predvolba_kanala[1] <=cnt_pwm) RES_VYST_BIT1;
    else SET_VYST_BIT1;
    if (hodnota_predvolba_kanala[2] <=cnt_pwm) RES_VYST_BIT2;
    else SET_VYST_BIT2;
    if (hodnota_predvolba_kanala[3] <=cnt_pwm) RES_VYST_BIT3;
    else SET_VYST_BIT3;

   cnt_pwm++;
}

Za SET_VYST_BITx/RES_VYST_BITx si dosad konkretny pin procesora, ktory pouzivas. To iste sa tyka i polarity vystupu (SET…/RES…)

Podla mojich skusenosti uplne staci 64 urovni osvetlenia. Takze si prerusenie mozes 4x spomalit, alebo blikanie LED 4x zrychlit. Optimum je ak prerusenie 2x spomalis a frekvenciu PWM zvysis zo 100Hz na 200Hz.

To ze prerusenie nastane principialne aj vtedy ked sa navonok nic nedeje vobec nevadi. Procesor ma vykonu dost, a kod je prehladny co je omnoho dolezitejsie. Lebo procesorov cas drahy nieje, to co je drahe je Tvoj cas a preto kazde primerane a funkcne zjedodusenie prinasajuce usporu Tvojho casu (a samozrejme aj casu inych pri poskytovani konzultacii :slight_smile: ) v tomto smere ma zmysel.

Tady je program podle aplikační poznámky AVR AppNote 136 “Low-Jitter Multi-Channel Software PWM”, který funguje podobně jak psal Martin.

Používá jen přerušení při přetečení čítače.
Frekvence je F_CPU / 65536 (122 Hz při osc. 8 MHz).

[code]//Avrstudio 4.17.666
//Winavr 20100110
//Mega88, F_CPU=8MHz

#include <avr/io.h>
#include <avr/interrupt.h>
#include “uart.h”

// Pin mappings
#define CHMAX 4 // number of PWM channels

#define CH0_CLEAR (pinlevelB &= ~(1 << PB0)) // map CH0 to PB0
#define CH1_CLEAR (pinlevelB &= ~(1 << PB1)) // map CH1 to PB1
#define CH2_CLEAR (pinlevelB &= ~(1 << PB2)) // map CH2 to PB2
#define CH3_CLEAR (pinlevelB &= ~(1 << PB3)) // map CH3 to PB2

// Set bits corresponding to pin usage above this is to enable pullups
#define PORTB_MASK (1<< PB0)|(1<< PB1)|(1<< PB2)|(1<< PB3)

// these are the outputs
#define DDRB_MASK (1<< PB0)|(1<< PB1)|(1<< PB2)|(1<< PB3)

unsigned char compare[CHMAX];
volatile unsigned char compbuff[CHMAX];

void timer_init(void)
{
DDRB = DDRB_MASK; // set port pins to output (and input)
TIFR0 = (1 << TOV0); // clear interrupt flag
TIMSK0 = (1 << TOIE0); // enable overflow interrupt
TCCR0B = (1 << CS00); // start timer, no prescale

  sei();                       

//------------------ MAIN -----------------------------------------------------
int main(void)
{
unsigned char kanal, hodnota;

uart_init();
timer_init();
sei();              // enable interrupts


compbuff[0] = 10;   // duty cycle pro kanál 0 (10/246)
compbuff[1] = 80;
compbuff[2] = 160;
compbuff[3] = 250;

/*
Test
Na pwm výstupech jsou Ledky
Z terminálu můžeme nastavit 4 hodnoty jasu.
Např:
01 - kanál 0, jas 1
32 - kanál 3, jas 2
atd
*/

for(;;) 
{
   if (UCSR0A & (1<<RXC0)) 
   {  kanal = UDR0 - '0';       
      while ( !(UCSR0A & (1<<RXC0)) ) {}
      hodnota = UDR0 - '0';
      compbuff[kanal] = hodnota * 60;
   }

 }

}

//------------------ END MAIN -------------------------------------------------

ISR (TIMER0_OVF_vect)
{
static unsigned char pinlevelB = PORTB_MASK;
static unsigned char softcount = 0xFF; //počítá přerušení

PORTB = pinlevelB;             // update outputs

if(++softcount == 0)           // při každém přetečení čítače přerušení 
{                              // se obnoví komparační hodnoty
    compare[0] = compbuff[0];  
    compare[1] = compbuff[1];
    compare[2] = compbuff[2];
    compare[3] = compbuff[3];
    
    pinlevelB = PORTB_MASK;     // set all port pins high
}

// clear port pin on compare match 
if(compare[0] == softcount) CH0_CLEAR;
if(compare[1] == softcount) CH1_CLEAR;
if(compare[2] == softcount) CH2_CLEAR;
if(compare[3] == softcount) CH3_CLEAR;

}
[/code]

do{ 
    
   SET_BIT(PORT_1,(Kanal[pozice][1])); //nastaví příslušný bit do 1 
   pozice++;                     //posun na další kanál 
    
   }while(Kanal[pozice-1][0]==Kanal[pozice][0]);   //opakuje nastaveni pokud se hodnoty

ta podmínka je tam pro případ, že by ty hodnoty pwm kanalu byly stejne. Proto je nejprve “do” což provede nastavení pwm kanálu a pak se posune o jeden kanal dal a kontroluje jestli ta hodnota je stejna jako předchazí aby se nemuselo znovu nemuselo vyvolavat přerušení ale aby se ten další kanal nastavil taky.
Právě takové PWM jake jste mi zaslaly mam a bliká to , mam 16MHz krystal a proto jede na 244Hz, a výš už se touhle metodou bohužel nedostanu, proto zkouším jak to udělat jinak (aby mi přerušení tak často, místo 255x za periodu jen 4x) a rychlejší cca kolem toho 1kHz.

Jak tak hledím do toho meho kodu tak jsem si tam nejspíše našel chybu Kanal[pozice][0] je špatne Kanal[zde je hodnota pwm] zde pozice kanalu ] tak to zkusím předělat a uvidíme :slight_smile:

Ak Ti to blika, mas tam chybu a nerobi to presne to co by si chcel.

MUXujem 4x7segment LED display s prepinacou frekvenciou 100Hz. Neriadim jas, iba prepinam segmenty, t.j. kazdy segment svieti 1/4 casu t.j.2,5ms z 10ms. Cislice su stabilne a neblikaju.
Nemas nahodou zapnuty fuse pre CLK = FREKVvst/8?

Ale ten napad s malym poctom preruseni sa mi osobne velmi paci. :slight_smile:

Tak jsem program upravil díky Martinovi, který mě navedl na správnou cestu. Podíval jsem se na kód a hned tam videl chybu, ani nevím jak jsem k takovemu patvaru vůbec došel :smiley: . samozřejmě chyba byla v tom poli ktare bylo spatne použité, myslel jsem to dobře ale zmrvil jsem to v programu. Ted jsem to upravil a běží to zhruba na 966Hz akorát ale zatím bohužel jen pro postupné hodnoty. Zkusím tam dodelělat řazení pomocí boublesortu a uvidíme jak to bude fungovat.


ISR(TIMER0_COMP_vect)//přerušení pri shode registru
{
	// jestlize promenna [cislo][x] je 1 nastav kanal1
	do{
	
	SET_BIT(PORT_1,pozice); //nastaví příslušný bit do 1
	pozice++;							//posun na další kanál
	
	}while(Kanal[pozice-1]==Kanal[pozice]);	//opakuje nastaveni pokud se hodnoty rovnají
	OCR0=Kanal[pozice];							//nastaví další kanál
	if(pozice==4){pozice=0;}						//nulování 
	//lestli se dalsi promenna rovna predchyi inkremetnuj a proved yvovu

 
}

ISR(TIMER0_OVF_vect)//přerušení pri přetečení
{
	CLEAR_BIT(PORT_1,BITPOS_0); // nuluje piny 0
	CLEAR_BIT(PORT_1,BITPOS_1); // nuluje piny 1
	CLEAR_BIT(PORT_1,BITPOS_2); // nuluje piny 2
	CLEAR_BIT(PORT_1,BITPOS_3); // nuluje piny 3