• Welcome to LusoRobótica - Robótica em Português.
 

Frequência máxima de pwm com controlo de duty cycle de 0 a 100%

Started by Njay, 20 de May de 2018, 19:59

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Njay

Este tópico vem na sequência de outro tópico em que este assunto estava a ficar off-topic.

Quote from: vasco on 20 de May de 2018, 13:44
Quote from: senso on 19 de May de 2018, 03:45
N é igual ao acima,  OCRnx pode ser no minimo zero, e tens 16000000/2 = 8Mhz, mas tens uma onda quadrada com um duty cycle de 50%.

É, acho que percebi mesmo mal o que li. Os nomes dos registos causam-me confusão, ao fim do segundo começo a misturar tudo. Eu sei que está tudo na datasheet. O problema é que está espalhado por toda a datasheet.

Olha que nem por isso... há um capítulo só sobre cada contador e um minúsculo sobre os prescalers. Os nomes parecem esquisitos mas têm uma lógica subjacente.

Quote from: vasco on 20 de May de 2018, 13:44
Uma pergunta objetiva, considerando um uno ou um nano, qual é a mais alta frequência possível para o pwm e ainda conseguir variar o duty cycle entre 0 e 100% ?

O senso foi buscar a informação à secção "Fast PWM Mode" do capítulo sobre o Timer/Counter0 (TC0) da datasheet.

Neste modo o contador (TCNT0) conta sempre de BOTTOM a TOP e depois volta a BOTTOM e repete. BOTTOM é sempre 0 (secção "Definitions") e TOP pode ser 0xFF (se WGM0[2:0] = 011b) ou o valor do registo OCR0A (se WGM0[2:0] = 111b).

Para teres 0 a 100% tens que usar o registo OCR0A como TOP, colocando o valor do duty cycle pretendido no registo OCR0B. Isto obriga a usar o pino OC0B (PD5, ou Arduino pino 5) e não funciona com o OC0A (PD6), pois o registo associado, OCR0A, neste modo está ocupado a servir de TOP. Quando o contador (TCNT0) chega a 0 (BOTTOM) o pino vai a 1 e quando chega a OCR0B (ocorrer o "compare match") vai a 0; depois quando o contador chega a OCR0A o contador volta a 0 (BOTTOM) no clock seguinte levando a saída novamente a 1. E tudo se repete.

Acho que a expressão da datasheet é para o caso do TOP ser 0xFF (256 = TOP + 1). O "TOP + 1" tem a ver com o facto do zero também representar uma fatia de tempo. A expressão geral é

fOCnx_PWM = fclk_IO / (N * (TOP + 1))

Incompletude da datasheet. Portanto quando se usa o registo OCR0A como TOP (definido com 99 para termos 0..100%) e N = 1

fOC0B_PWM = fclk_IO / (1 * (99 + 1)) = 160 KHz

O menor prescaler dá a maior frequência de contagem; o prescaler é um valor pelo qual o clock é dividido (scaled down) antes (pre) de ser usado.
fclk_IO é basicamente o clock de sistema, 16MHz no Arduino. Há um capítulo na datasheet sobre os clocks, e o capítulo do timer/counter também tem uma secção sobre o clock.
1% de duty cycle gera uma onda com impulsos de 62.5ns (1 / 160 KHz / 100) repetidos a cada 6.25us (1/160KHz).

Se meteres o ATmega a 20MHz chegas aos 200 KHz (1% -> 50ns).

Ainda segundo a datasheet, na mesma secção sobre "Fast PWM Mode", não é possível chegar a 0% (pino sempre a zero), pelo que para ter 0% é preciso desligar o PWM e meter o pino a zero. No sketch demonstrativo em anexo faço isso usando o campo "Compare Output Mode" no TCCR0A, que permite desligar o Timer/Counter0 do pino do chip.

Fica ainda a nota de que o Arduino usa o Timer/Counter0 para manter as funções relacionadas com tempo (como a millis()), portanto no programa exemplo essas funções não funcionam. Pode ser usado o Timer/Counter2 que é muito semelhante ao 0. Alterar o programa para funcionar no Timer/Counter2 fica como exercício para o leitor :D

vasco

Obrigado pela explicação.
Estou mesmo a tentar compreender isto, mas não está a ser fácil.

Sobre o timer0, continua a ser possível usar a função _ms_delay() em vez do delay(), certo ?

Mas não percebo muito bem onde dizem

The maximal possible delay is 262.14 ms / F_CPU in MHz.

Isto quer dizer que o delay máximo dado pelo _ms_delay(nnnnn) são aprox 16.4 ms ?

Stupid men are often capable of things the clever wouldn't dare to contemplate.

Njay

Os timers também me faziam muita confusão, até que um dia tive que me sentar com calma e ler o capítulo do inicio ao fim. Depois ficaram muito fáceis  :D

Sim, a _delay_ms() e _dely_us() continuam a poder ser usadas pois não dependem de timers nem de nenhum hardware excepto do CPU, são implementadas como ciclos só a comer tempo, como isto:

for (int ctr = 100; ctr; ctr--);

Como a implementação usa uma variável  contadora de tamanho limitado (não sei se 16, 24 ou 32 bits), o tempo máximo de delay também é limitado. A divisão pela velocidade do CPU vem mesmo disso, quanto mais rápido menor é o tempo máximo de delay. Sim, com o CPU a 16MHz como no Arduino UNO o máximo da _delay_ms() é 16.38375ms. Mas facilmente dás a volta a isso, esta dá até ~65s mesmo num AVR a 20MHz:

static void DelayMs (uint16_t ms)
{
    while (ms--)
    {
        _delay_ms(0.999);
    }
}

Isto pode ser melhorado para eliminar parte do overhead do controle do while, mas para um delay é meio irrelevante. De qualquer forma não queres fazer delays muito grandes com espera activa, consome CPU.

senso

Essa coisa do delay faz-me confusão.
Já li isso, já usei delays de 500ms e até de 1000ms, e funcionam perfeitamente.

Faz toogle a um led com _delay_ms(1000), vais ver que funciona como é esperado, é só definir o F_CPU antes do #include, o _delay_ms() usa floats(ou pelo menos usava, incluir _delay_ms incorre em 2-3KB extra de flash utilizada.
Avr fanboy

Njay

Já experimentei no passado e não se portava bem se o limite fosse excedido...
Ele só inclui código de floats se o argumento não for uma constante; se for constante ele calcula o valor do contador em compile-time e faz um loop optimizado.

senso

Com O0(O zero) usava floats mesmo com uma constante, mas tenho ideia de nunca ter falhado com valores grandes, mas a primeira coisa que faço em qualquer micro já á muitos anos é meter um timer a correr e ter bases de tempo de 1 e 10ms pelo menos para usar á lá milis do arduino e deixei de usar delays, tento fazer sempre código não blocante(errrr, blocking).

EDIT:

Posso estar a fazer confusão e efectivamente ele chegar a um ponto que se passa, já foi á uns anos.
Avr fanboy

vasco

Provavelmente a maneira mais certa de fazer um loop desses é em assembly, com as otimizações do compilador deve ser muito difícil chegar a um valor exato. No spectrum (z80) era só meter uns NOPs ou outra instrução inocula em sequência. Desde que se faça o disable dos interrupts deve ser estável.

Ainda tenho muita coisa a aprender dos AVR antes sequer de poder pensar em assembly.
Stupid men are often capable of things the clever wouldn't dare to contemplate.

Njay

bloqueante :)

Não substimes o poder de optimização do GCC, Vasco :) Eu comecei nos AVR em assembly mas fiz 1 projecto e passei para o C, sendo extremamente raro ter que recorrer a assembly, porque o GCC é muito bom e o AVR muito rápido na gama dele. Diga-se de passagem que assembly de Z80 também não me é estranho, até tenho publicado um compilador de um sub-set do C para Z80 (para a TI-86) que até nem é muito mau a optimizar (sendo prova disso que cheguei a ser contactado por um developer do SDCC que queria o meu optimizador :), a que chamo um peephole optimizer on steroids).
Bom, mas vejamos:

void test ()
{
    _delay_us(1.5);
    _delay_ms(1.5);
}


Compilado com -Os (optimizar para tamanho reduzido; mesmo rsultado com -O2) para um AVR a 5 MHz resulta em

00000126 <test>:
126:   82 e0          ldi   r24, 0x02   ; 2
128:   8a 95          dec   r24
12a:   f1 f7          brne   .-4         ; 0x128 <test+0x2>

12c:   83 e5          ldi   r24, 0x53   ; 83
12e:   97 e0          ldi   r25, 0x07   ; 7
130:   01 97          sbiw   r24, 0x01   ; 1
132:   f1 f7          brne   .-4         ; 0x130 <test+0xa>

134:   08 95          ret


As 1ªas 3 instruções são um _delay_us e as seguintes 4 o _delay_ms. Estes são os ciclos mais compactos que consegues fazer. Por exemplo no 1º caso, para 1.5us, o gajo gasta

  1 clock no load inicial do r24 com 2
  depois faz 3 clocks na 1ª iteração, 1 do dec e 2 do brne (jump para instrução anterior)
  mais 2 clocks na 2ª e última iteração, pois quando não há salto o brne (branch if not equal/zero) gasta 1 clock
  1 + 3 + 2 = 6 clocks @ 5MHz -> 1.2us

Por acaso esperava que ele optasse sempre por demorar um pouco mais do que o valor pedido, quando não dá para ter o valor exacto com um ciclo e a frequência de clock, mas está lá perto (mais uma iteração daria 1.8us, 1.5 fica mesmo no meio entre 1.2 e 1.8 :D).

No caso do _delay_ms são precisos mais de 256 iterações e então o ciclo usa um contador de 16 bits (r24 e r25).

De qualquer forma acho que as _delay_*s foram implementados para serem executados sempre desta forma, num ciclo vazio.

vasco

Acho que me expliquei mal, não estou a dizer que o gcc é bom ou mau a otimizar, apenas que é difícil para o comum dos mortais fazer código que depois de otimizado pelo gcc dê exatamente o delay que se quer.
É mais fácil (ou pelo menos mais linear) fazer o tunning juntando ou tirando uma instrução ao ciclo.

Os meus devaneios pelo z80 estão a anos-luz desse tipo de coisa, e nunca tive que fazer nada que me levasse a estudar teoria de compiladores.
Stupid men are often capable of things the clever wouldn't dare to contemplate.

Njay

Quote from: vasco on 22 de May de 2018, 22:49
Acho que me expliquei mal, não estou a dizer que o gcc é bom ou mau a otimizar, apenas que é difícil para o comum dos mortais fazer código que depois de otimizado pelo gcc dê exatamente o delay que se quer

Fazer em C, ou em qualquer outra linguagem acima do assembly, um delay por "consumo de ciclos", não me faz sentido nenhum, quer o compilador optimize ou não...

senso

Quote from: vasco on 22 de May de 2018, 22:49
Acho que me expliquei mal, não estou a dizer que o gcc é bom ou mau a otimizar, apenas que é difícil para o comum dos mortais fazer código que depois de otimizado pelo gcc dê exatamente o delay que se quer.
É mais fácil (ou pelo menos mais linear) fazer o tunning juntando ou tirando uma instrução ao ciclo.

Os meus devaneios pelo z80 estão a anos-luz desse tipo de coisa, e nunca tive que fazer nada que me levasse a estudar teoria de compiladores.

Usas um timer para coisas mais exactas, mas erro existe sempre.
Avr fanboy

Njay

Vasco, podes sempre forçar uma instrução assembly no meio do C. Em GCC é mais ou menos assim para instruções sem argumentos:

asm volatile ("sleep");

Njay

Se pegares num dos AVR com um PLL interno, como os ATtinyxxx (não me lembro se todos), consegues frequências mais altas.
Este PLL gera um clock até 64MHz a partir do clock de sistema, e podes usar esse clock para um contador. Daí que...

fOCxy_PWM = 64 MHz / (1 * (99 + 1)) = 640 KHz
  (untested)

Em todos os casos podes obter maior frequência abdicando de resolução.