LusoRobótica - Robótica em Português

Sistemas específicos => Arduino / AVR => Tópico iniciado por: Pinout em 12 de Dezembro de 2014, 09:19

Título: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 09:19
Olá pessoal,

fui implementar um código para mover um servo motor utilizando um potenciômetro e depois de analizar e testar o modo Fast PWM e CTC, achei que o modo CTC é muito melhor para implementar a movimentação desejada, uma vez que consigo fazer toda a implementação do sinal na interrupção gerada no modo CTC. E me possibilita também utilizar qualquer pino digital e não os restritos ao Fast PWM. Estou colocando o código aqui para que alguém com mais experiência possa ver e dizer se é equivocado implementar com o modo CTC ou se não há problema. No código que envio, utilizei apenas um potenciômetro, ligado ao pino PC0 e um servo motor ligado ao pino PD6. O ADC roda em modo contínuo, pois como se trata de apenas um potenciômetro, o valor do potenciômetro sempre estará disponível para a interrupção por comparação do timer0 atribuir ao servo motor, promovendo maior desempenho (na minha opinião).

Bom, segue o código:

Código: [Seleccione]
/*
 *
 * Created: 10/12/2014 09:29:27
 */
#include <avr/io.h>
#include <avr/interrupt.h>

#define SERVO (1 << PD6)

volatile char pulseCount = 0; // Essa é a nossa variável volatile compartilhada, que será usada como contador pulseCount

void ADC_init() { // inicia ADC em modo contínuo
    ADCSRA = (7 << ADPS0);
    ADMUX = (1 << REFS0);
ADCSRB &= ~(7 << ADTS0);
ADCSRA |= (1 << ADATE);
    ADCSRA |= (1 << ADEN);
    ADCSRA |= (1 << ADSC);
}

void CONFIG_servo() {
    DDRD = (SERVO); // seta pino como saída
    PORTD &= ~SERVO; // assegura nível lógico 0 para o pino
}

void timer0_init(void) {
    TCCR0A = (1<<WGM01); // modo CTC
    TCCR0B = ((1<<CS01)|(1<<CS00)); // prescaler 1:64
    OCR0A = 24; // interrupção à cada 0.1ms
    TIMSK0 = (1<<OCIE0A); // habilita interrupções no timer
sei(); // habilita interrupções globais
}

int main(void) {
    CONFIG_servo();
    ADC_init();
    timer0_init();   
    while(1);       
    return 1;
}

ISR(TIMER0_COMPA_vect) {
    pulseCount++; // incrementa o contador à cada 0.1ms
    if(pulseCount == 200) { // atende à condição à cada 20ms - frequência de 50Hz
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca em nível lógico 1)
    } else if(pulseCount >= (203 + (ADC / 44))) { // verifica valor do contador, compara com potenciômetro e, quanto iguais, limpa o pino
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca nível lógico 0)
        pulseCount = 0; // zera contador
    }
}
Título: Re: PWM para servo com timer0 e modo CTC
Enviado por: jm_araujo em 12 de Dezembro de 2014, 09:48
Como tens o código a frequência vai variar conforme tamanho do pulso do servo. Corrigir é fácil:
Código: [Seleccione]
ISR(TIMER0_COMPA_vect) {
    pulseCount++; // incrementa o contador à cada 0.1ms
    if(pulseCount == 200) { // atende à condição à cada 20ms - frequência de 50Hz
        PORTD &= ~SERVO; // coloca em nível lógico 0)
        pulseCount = 0; // zera contador
    } else if(pulseCount >= (197 - (ADC / 44))) { // verifica valor do contador, compara com potenciômetro e, quanto iguais, liga o pino
        PORTD |= SERVO; // coloca nível lógico 1)
    }
}

Mas é um preciosismo, os servo são tolerantes na frequência de entrada.

Outra recomendação é não usar o valor do ADC diretamente, fazer um passa baixo em sw (média das últimas leituras) faz maravilhas para reduzir o ruído (que na ADC de um micro é sempre algum).

Isto foi o que reparei numa análise superficial do código ;)


Edit: umas correções ao codigo. A estas horas ainda não tenho café suficiente no organismo para sair código bem à primeira :P
Título: Re: PWM para servo com timer0 e modo CTC
Enviado por: Pinout em 12 de Dezembro de 2014, 12:44
jm_araujo, coloquei daquela forma pois sempre li que o pwm para servo começa com uma borda de subida, não sei se entederá o termo, pois falo portguês do Brasil, mas ele começa com nível lógico 1 e o dutty cycle o mantém por tempo curto e depois vem a borda da descida. Calculei um contador de 24, que me gera uma interrupção à cada 0.1ms (para poder manipular facilmente o dutty cycle), ou seja depois de 200 interrupções terei um intervalo de tempo de 20ms - 50Hz, por isso estou incrementando o pulseCount na interrupção e verificando se ele atingiu 200, para gerar a frequência de 50Hz e depois verifico com o 'else if' o valor do potenciômetro, para me gerar um dutty cycle com base no valor do mesmo. Da forma como colocou o pwm começaria na borda da descida e o duty cycle só trabalharia com uma porcentagem muito pequena do potencial do servo.

Veja bem a expressão:  else if(pulseCount >= (203 + (ADC / 44)))  que implementa o dutty cycle, divide o valor convertido do potenciômetro, que é no máximo 1023 por 44, ou seja, gera
um número máximo de 23, somado à 203 (que no meu servo é o grau 0). Sendo assim, quendo o potenciômetro está gerando 0, o servo vai ficar em grau 0, e o duty cycle vai ser de 0.3ms e
quando o potenciômetro estiver no máximo, o duty cycle vai ser de 2.6ms. Esses valores de dutty cycle funcionam perfeitamente para os servos Tower Pro 9g.

O passa baixas com o ADC, realmente, é o melhor jeito de fazer, principalmente quando é um valor que queira colocar num lcd ou que exija um valor mais estável, porém, não tive esse cuidado, de fato os servos são tolerantes e não quis complicar muito o código, coloquei uns registradores que nem seriam necessários, mais pra ficar bem claro tudo que estou fazendo.

jm_araujo, obrigado pelo comentário e todas as críticas e sugestões são bem vindas.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 15:04
Não dizes qual é a frequência do clock do AVR.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: jm_araujo em 12 de Dezembro de 2014, 15:40
Se aos 200 ciclos adicionas mais um número indeterminado dependente da ADC, não podes em rigor dizer que a frequência são 50Hz, pois varia consoante esses ciclos adicionados, mas é uma aproximação que os servos toleram( como já tinha dito).

Para o caso não é importante, mas quis deixar o comentário porque pode aparecer que reuse o código e depois encontre esta imprecisão.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 16:03
Não dizes qual é a frequência do clock do AVR.

A frequência é de 16Mhz.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 16:06
Se aos 200 ciclos adicionas mais um número indeterminado dependente da ADC, não podes em rigor dizer que a frequência são 50Hz, pois varia consoante esses ciclos adicionados, mas é uma aproximação que os servos toleram( como já tinha dito).

Para o caso não é importante, mas quis deixar o comentário porque pode aparecer que reuse o código e depois encontre esta imprecisão.

É verdade, passou desapercebido. Entendi seu ponto, para ficar exato o código deveria ficar assim:


Código: [Seleccione]
ISR(TIMER0_COMPA_vect) {
    pulseCount++; // incrementa o contador à cada 0.1ms
    if(pulseCount == 200) { // atende à condição à cada 20ms - frequência de 50Hz
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca em nível lógico 1)
    } else if(pulseCount >= (203 + (ADC / 44))) { // verifica valor do contador, compara com potenciômetro e, quanto iguais, limpa o pino
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca nível lógico 0)
        pulseCount = 3 + (ADC / 44); // zera contador
    }
}
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: jm_araujo em 12 de Dezembro de 2014, 16:17
Pensa melhor no que estás a dizer.
Não passam 200 ciclos entre cada vez que ligas o pino porque o reset do contador varia com o valor do ADC.
Por exemplo se o ADC estiver a 0:
Conta de 0 a 200, liga o pino, conta até 203, reset do contador e do pino, e volta a contar 200. Entre as duas vezes que ligaste o pino passaram 200+3 ciclos.
Se o ADC for 440, conta de 0 a 200, liga o pino, conta até 213, faz reset do contador e do pino, e só liga passado 200 ciclos. Total de ciclos: 200+13

Se num caso demora 203 e noutro 213, como é que a frequência é sempre a mesma? No PWM a frequencia é o inverso do tempo a 0 mais o tempo a 1!


Se queres que realmente seja assincrono com menos modificações que o meu código é substítuir o "pulseCount = 0;" por "pulseCount = pulseCount -200". Aí sim a duração é sempre 200 ciclos

Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 16:23
Pensa melhor no que estás a dizer.
Não passam 200 ciclos entre cada vez que ligas o pino porque o reset do contador varia com o valor do ADC.
Por exemplo se o ADC estiver a 0:
Conta de 0 a 200, liga o pino, conta até 203, reset do contador e do pino, e volta a contar 200. Entre as duas vezes que ligaste o pino passaram 200+3 ciclos.
Se o ADC for 440, conta de 0 a 200, liga o pino, conta até 213, faz reset do contador e do pino, e só liga passado 200 ciclos. Total de ciclos: 200+13

Se num caso demora 203 e noutro 213, como é que a frequência é sempre a mesma? No PWM a frequencia é o inverso do tempo a 0 mais o tempo a 1!


Se queres que realmente seja assincrono com menos modificações que o meu código é substítuir o "pulseCount = 0;" por "pulseCount = pulseCount -200". Aí sim a duração é sempre 200 ciclos

É verdade, não tinha percebido. Obrigado pela observação, meu caro.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 16:52
Neste caso parece-me que não é importante, mas também convém dizer qual é o AVR, porque existem algumas diferenças nos periféricos entre os vários modelos de AVR.

No código original a pulseCount vai a zero em alturas diferentes (200 + duty_cycle), portanto sim, a frequência não é constante, embora seja um jitter (variação de frequência) pequeno e o servo não se importe com isso.

A resolução de posição é que me parece grosseira (20 posições, num intervalo de 180º dá um passo de 9º), mas provavelmente serve para muitos casos. Na minha opinião o código deveria estar preparado para fazer os valores tradicionais dos servos no que diz respeito ao ciclo activo, ou seja, impulso a high durante 0.5ms para 0º, 1.5ms para 90º e 2ms para 180º, e depois ter um parametro para ajustar a gama de posições (um deslocamento, offset) a cada servo (há diferenças entre servos).

Quanto ao código em si posso tecer alguns comentários no que diz respeito a práticas de programação.

Antes de mais eu faria e sem pensar 2 vezes como fez o jm_araujo: se é para meter a zero, mete-se explicitamente a zero e o mesmo para a um. Fazer com toggle é facilitar a vida ao Murphy.

A configuração dos registos deveria estar melhor comentada, incluindo menção aos cálculos. Por exemplo, em vez de apenas
  // prescaler 1:64
deveria ser qualquer coisa como
  // prescaler 1:64 => 16MHz sys clock / 64 = 250KHz
Na configuração do ADC não há nenhum comentário; a "regra" geral deve ser "ao ler o código, quanto menos precisar de olhar para a datasheet ou outro documento, melhor".

A variável pulseCount, como só é usada dentro do ISR, pode ser declarada lá dentro, assim:

ISR(...
{
    static uint8_t  pulseCount = 0;
    ...
}


Mais uma vez, ao ler o código desta rotina ficamos logo sem dúvidas se a variável é usada noutro sítio e como. Isto acelera e facilita a leitura do código, e ainda pode permitir ao compilador efectuar melhores optimizações, resultando em menor tamanho e maior velocidade.
Não é preciso usar volatile porque a variável só é usada dentro da routina.
É melhor usar o tipo uint8_t (ou unsigned char na pior das hipóteses), porque o tipo char pode ter sinal (agora não me lembro se no gcc para AVR tem, mas como o teu código funciona, não deve ter) e nesse caso o teu if ia falhar porque estarias a comparar a variável com valores que ela não pode ter; um (signed) char não pode ter valores maiores que 127, logo nunca é maior que 203 + qualquer_coisa.

A indentação deve ser coerente, há sítios que tens espaços e outros que tens tabs.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 17:30
Neste caso parece-me que não é importante, mas também convém dizer qual é o AVR, porque existem algumas diferenças nos periféricos entre os vários modelos de AVR.

No código original a pulseCount vai a zero em alturas diferentes (200 + duty_cycle), portanto sim, a frequência não é constante, embora seja um jitter (variação de frequência) pequeno e o servo não se importe com isso.

A resolução de posição é que me parece grosseira (20 posições, num intervalo de 180º dá um passo de 9º), mas provavelmente serve para muitos casos. Na minha opinião o código deveria estar preparado para fazer os valores tradicionais dos servos no que diz respeito ao ciclo activo, ou seja, impulso a high durante 0.5ms para 0º, 1.5ms para 90º e 2ms para 180º, e depois ter um parametro para ajustar a gama de posições (um deslocamento, offset) a cada servo (há diferenças entre servos).

Quanto ao código em si posso tecer alguns comentários no que diz respeito a práticas de programação.

Antes de mais eu faria e sem pensar 2 vezes como fez o jm_araujo: se é para meter a zero, mete-se explicitamente a zero e o mesmo para a um. Fazer com toggle é facilitar a vida ao Murphy.

A configuração dos registos deveria estar melhor comentada, incluindo menção aos cálculos. Por exemplo, em vez de apenas
  // prescaler 1:64
deveria ser qualquer coisa como
  // prescaler 1:64 => 16MHz sys clock / 64 = 250KHz
Na configuração do ADC não há nenhum comentário; a "regra" geral deve ser "ao ler o código, quanto menos precisar de olhar para a datasheet ou outro documento, melhor".

A variável pulseCount, como só é usada dentro do ISR, pode ser declarada lá dentro, assim:

ISR(...
{
    static uint8_t  pulseCount = 0;
    ...
}


Mais uma vez, ao ler o código desta rotina ficamos logo sem dúvidas se a variável é usada noutro sítio e como. Isto acelera e facilita a leitura do código, e ainda pode permitir ao compilador efectuar melhores optimizações, resultando em menor tamanho e maior velocidade.
Não é preciso usar volatile porque a variável só é usada dentro da routina.
É melhor usar o tipo uint8_t (ou unsigned char na pior das hipóteses), porque o tipo char pode ter sinal (agora não me lembro se no gcc para AVR tem, mas como o teu código funciona, não deve ter) e nesse caso o teu if ia falhar porque estarias a comparar a variável com valores que ela não pode ter; um (signed) char não pode ter valores maiores que 127, logo nunca é maior que 203 + qualquer_coisa.

A indentação deve ser coerente, há sítios que tens espaços e outros que tens tabs.

Obrigado pela resposta Njay

Seguindo sua sugestão e a do jm_araujo, aqui vai o código:

Código: [Seleccione]
/*
 *
Cálculos:
Fosc: 16Mhz
Prescaler: 64

Primeiro converto para Hz o tempo que desejo ser realizada a interrupção (0.1ms): (1/0.1) * 1000 = 10000Hz ou 10Khz
Calculo, com base na frequência, o valor para o contador: (((16Mhz / 64) / 10000) -1) = 24

 * Created: 10/12/2014 09:29:27
 */
#include <avr/io.h>
#include <avr/interrupt.h>

#define SERVO (1 << PD6)

volatile uint8_t pulseCount = 0; // variável volatile compartilhada, que será usada como contador pulseCount

void ADC_init() { // inicia ADC em modo contínuo
    ADCSRA = (7 << ADPS0);
    ADMUX = (1 << REFS0);
    ADCSRB &= ~(7 << ADTS0);
    ADCSRA |= (1 << ADATE);
    ADCSRA |= (1 << ADEN);
    ADCSRA |= (1 << ADSC);
}

void CONFIG_servo() {
    DDRD = (SERVO); // seta pino como saída
    PORTD &= ~SERVO; // assegura nível lógico 0 para o pino
}

void timer0_init(void) {
    TCCR0A = (1<<WGM01); // modo CTC
    TCCR0B = ((1<<CS01)|(1<<CS00)); // prescaler 1:64
    OCR0A = 24; // interrupção à cada 0.1ms
    TIMSK0 = (1<<OCIE0A); // habilita interrupções no timer
    sei(); // habilita interrupções globais
}

int main(void) {
    CONFIG_servo();
    ADC_init();
    timer0_init();
    while(1);
    return 1;
}

ISR(TIMER0_COMPA_vect) {
    pulseCount++; // incrementa o contador à cada 0.1ms
    if(pulseCount == 200) { // atende à condição à cada 20ms - frequência de 50Hz
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca em nível lógico 1)
    } else if(pulseCount >= (203 + (ADC / 44))) { // verifica valor do contador, compara com potenciômetro e, quanto iguais, limpa o pino
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca nível lógico 0)
        pulseCount -= 200; // atribui ao contador valor para frequência de 50Hz subtraindo o que foi gerado no dutty cycle
    }
}

Com relação à variável pulseCount, realmente o char é signed e não me antentei ao range. Pelo visto ele é unsigned por padrão, senão jamais daria certo, mas coloquei o uint8_t, para ficar mais claro que é 1 byte unsigned, mas ela ser declarada dentro da ISR, não vejo como declará-la e incrementá-la a cada interrupção. Pode me dizer se há realmente um jeito?

Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 17:35
Exactamente como exemplifiquei. Em C/C++, se colocares a keyword  static no inicio da declaração de uma variável local, ela deixa de ser local e passa a ser "global" (para ser rigoroso, passa a ser "estática", ou seja, existe sempre, não está na pilha), mas apenas visivel dentro do bloco ({...}) em que está declarada, e só é inicializada 1 vez, no inicio do programa (tal como uma variável "global").
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 17:40
Exactamente como exemplifiquei. Se colocares a keyword  static no inicio da declaração de uma variável local, ela deixa de ser local e passa a ser global, mas apenas visivel dentro do bloco ({...}) em que está declarada, e só é inicializada 1 vez, no inicio do programa (tal como uma variável global).

Maravilha!
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 17:49
fiquei na dúvida se havia diferença de performance, entre as duas abordagens e achei o texto abaixo:

- Making the variable static may incur a small cost on every call of the function determining if the object was already initialized, especially with C++11 where the initialization is thread-safe. Even if it doesn't need a check, the stack is likely to be in cached memory while a static variable is not.

- Making a variable global will increase the chances that it isn't in cached memory, i.e., there is a good chance that it will be slower (aside from adverse other potential like making it a good candidate to introduce data races).
Making a variable const may help if the compiler can compute the value as compile time.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 17:57
fiquei na dúvida se havia diferença de performance,
Não há, porque a tua variável já era "global". Na verdade, até é mais rápido declarado como estática dentro do ISR, pois o compilador vai poder fazer algumas optimizações que não pode fazer quando declaras a variável global (fora de funções). Mesmo declarando a variável global, podes ainda assim tornar o acesso mais rápido se ... adicionares static no inicio :) . A diferença entre uma variável global com static e sem static é que sem static ela pode ser acedida a partir de outro ficheiro de código (que também esteja a ser linkado na a tua aplicação), e com static ela só é visivel dentro do ficheiro onde foi declarada.

Mais ainda: podes também adicionar static à declaração de funções, podendo com isto tornar o código mais pequeno. A "desvantagem" é que não podes invocá-las a partir de outro ficheiro, mas se o teu programa só tem um (como é agora o caso), isso não é um problema.

Faz esta experiência: compila o teu código actual (pulseCount global) e vê o tamanho do código gerado, depois adiciona static à declaração da pulseCount, compila e compara o tamanho do código gerado com a versão sem static. (e também podes experimentar com e sem volatile).

Citar
entre as duas abordagens e achei o texto abaixo:

- Making the variable static may incur a small cost on every call of the function determining if the object was already initialized, especially with C++11 where the initialization is thread-safe. Even if it doesn't need a check, the stack is likely to be in cached memory while a static variable is not.
Não se aplica ao actual GCC para AVR, além de que threads não são suportadas nativamente.

Citar
- Making a variable global will increase the chances that it isn't in cached memory, i.e., there is a good chance that it will be slower (...)
Não se aplica ao AVR, o AVR não tem cache no CPU.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 18:05
Excelente. Mais conhecimento agregado.

Obrigado pelos esclarecimentos.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 19:25
Código melhorado, com range de 206 (teoricamente o range do servo é de 180, mas o tower pro 9g tem um pouco mais) seguindo o que o jm_araujo e o Njay me esclareceram:

Código: [Seleccione]
/*
 *
 * Created: 10/12/2014 09:29:27
 */
#include <avr/io.h>
#include <avr/interrupt.h>

#define SERVO (1 << PD6)

void ADC_init() { // inicia ADC em modo contínuo
    ADCSRA = (7 << ADPS0); // prescaler de 128
    ADMUX = (1 << REFS0); // tensão de referência a própria alimentação do mcu
    ADCSRB &= ~(7 << ADTS0); // modo contínuo
    ADCSRA |= (1 << ADATE); // habilita modo contínuo
    ADCSRA |= (1 << ADEN); // habilita ADC
    ADCSRA |= (1 << ADSC); // inicializa conversões - faz uma primeira conversão
}

void CONFIG_servo() {
    DDRD = (SERVO); // seta pino como saída
    PORTD &= ~SERVO; // assegura nível lógico 0 para o pino
}

void timer0_init(void) {
    TCCR0A = (1<<WGM01); // modo CTC
    TCCR0B = (1<<CS01); // prescaler 1:8
    OCR0A = 19; // interrupção à cada 0.01ms - (((Fosc / prescaler) / ((1/tempo alvo) * 1000)) -1), ou seja ((( 16Mhz / 8) / ((1/0.01) * 1000)) -1)
    TIMSK0 = (1<<OCIE0A); // habilita interrupções no timer
    sei(); // habilita interrupções globais
}

int main(void) {
    CONFIG_servo();
    ADC_init();
    timer0_init();
    while(1);
    return 1;
}

ISR(TIMER0_COMPA_vect) {
    static uint16_t pulseCount; // variável static, utilizada como contador
    pulseCount++; // incrementa o contador à cada 0.1ms
    if(pulseCount == 2000) { // atende à condição à cada 20ms - frequência de 50Hz
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca em nível lógico 1)
    } else if(pulseCount >= (2047 + (ADC / 5))) { // verifica valor do contador, compara com potenciômetro e, quanto iguais, limpa o pino
        PORTD ^= SERVO; // inverte estado lógico do pino (sempre coloca nível lógico 0)
        pulseCount -= 2000; // atribui ao contador valor para frequência de 50Hz subtraindo o que foi gerado no dutty cycle
    }
}


Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 20:35
E agora o teu próximo desafio é: como suportar vários servos ao mesmo tempo :)


p.s.: A biblioteca de suporte ao GCC para AVR trás uma macro que faz o "1 << XX", chamada _BV() (ex.: _BV(PD6)) . "BV" penso que vem de "Bit Value". O help da libc para AVR tem tudo o que existe para AVR, é documentação imprescindível para quem programa a este nível, logo a seguir à datasheet.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 21:00
Opa! Esse já está pronto também...

Código: [Seleccione]
/*
 *
 * Created: 11/12/2014 11:15:59
 */

#include <avr/io.h>
#include <avr/interrupt.h>

#define POT_STEER (1 << PC0);
#define POT_GEAR (1 << PC1);

#define SERVO_STEER (1 << PD5)
#define SERVO_GEAR (1 << PD6)

volatile uint16_t servo_steer_pulse;
volatile uint16_t servo_gear_pulse;

void ADC_init() {
ADCSRA = (7 << ADPS0); // prescaler de 128
ADMUX = (1 << REFS0); // tensão de referência a própria alimentação do mcu
ADCSRA |= (1 << ADEN); // habilita ADC
ADCSRA |= (1 << ADSC); // faz primeira conversão e habilita conversões
}

void getPulseSteer() {
ADMUX &= 0XC0; // limpa canais
ADMUX |= (uint8_t) 0; // seleciona canal 0 do ADC (PC0)
ADCSRA |= (1 << ADSC); // realiza conversão
while(ADCSRA & (1 << ADSC)); // espera conclusão da conversão
servo_steer_pulse = ADC; // atribui valor do ADC à variável relativa ao servo
}

void getPulseGear() {
ADMUX &= 0XC0;
ADMUX |= (uint8_t) 1; // seleciona canal 1 do ADC (PC1)
ADCSRA |= (1 << ADSC);
while(ADCSRA & (1 << ADSC));
servo_gear_pulse = ADC;
}

void CONFIG_servos() {
DDRD = (SERVO_STEER) | (SERVO_GEAR); // Seta o pino como saída
PORTD &= ~SERVO_STEER;
PORTD &= ~SERVO_GEAR;
}

void timer0_init(void) {
TCCR0A = (1<<WGM01); // Timer no modo CTC
TCCR0B = (1<<CS01); // Prescaler em 1:8
OCR0A = 19; // interrupção à cada 0.01ms - (((Fosc / prescaler) / ((1/tempo alvo) * 1000)) -1), ou seja ((( 16Mhz / 8) / ((1/0.01) * 1000)) -1) = 19
TIMSK0 = (1<<OCIE0A); // Habilita interrupções no timer
}

void timer2_init(void) {
TCCR2A = (1<<WGM21); // Timer no modo CTC
TCCR2B = (1<<CS21); // Prescaler em 1:8
OCR2A = 19; // interrupção à cada 0.01ms - (((Fosc / prescaler) / ((1/tempo alvo) * 1000)) -1), ou seja ((( 16Mhz / 8) / ((1/0.01) * 1000)) -1) = 19
TIMSK2 = (1<<OCIE2A); // Habilita interrupções no timer
}

int main(void) {
CONFIG_servos();
ADC_init();
timer0_init();
timer2_init();
sei(); // Habilita interrupções globais
while(1) {
getPulseSteer();
getPulseGear();
}
return 1;
}

ISR(TIMER0_COMPA_vect) {
static uint16_t counterSteer;
counterSteer++; // Incrementa o contador milisSteer em um milisSteeregundo
if(counterSteer == 2000) { // implementa em 20ms
PORTD ^= SERVO_STEER; // inverte nível lógico do pino ha cada 20ms/50Hz
} else if(counterSteer >= (2047 + (servo_steer_pulse / 5))) {
PORTD ^= SERVO_STEER; // inverte nível lógico do pino, segundo o valor do potenciômetro, gerando o dutty cycle
counterSteer -= 2000;
}
}

ISR(TIMER2_COMPA_vect) {
static uint16_t counterGear;
counterGear++; // Incrementa o contador milisSteer em um milisSteeregundo
if(counterGear == 2000) { // implementa em 20ms
PORTD ^= SERVO_GEAR; // inverte nível lógico do pino ha cada 20ms/50Hz
} else if(counterGear >= (2047 + (servo_gear_pulse / 5))) {
PORTD ^= SERVO_GEAR; // inverte nível lógico do pino, segundo o valor do potenciômetro, gerando o dutty cycle
counterGear -= 2000;
}
}

Tá lá, com dois servos. Nesse eu não consegui fugir de colocar algo no while(1), li que quando utiliza-se mais de um canal ADC, colocar no modo contínuo gera atraso na leitura, então fiz a captura do valor do ADC, no modo 'single', que tive que fazer no loop infinito, já que também gera atrasos chamar funções nas ISR. Mas está bem rápido e num nível bom de precisão.

Quero só dar um agradecimento formal ao moderador senso, que fez o blog https://hekilledmywire.wordpress.com/category/avr/avr-tutorials/ que me ajudou muito a esclarecer os pontos básicos da programação para AVR, com o winavr. E já aproveitar para fazer um pedido para que ele continue com os tutoriais.

Njay, tenho essa biblioteca baixada, vou dar mais atenção a ela. =)
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 21:47
Assim é batota, heheheh. O desafio é implementar usando apenas um timer, e para N servos ;)

"essa bibliioteca" já vem com o winavr, "faz parte" do GCC (o compilador).

Já que tás à vontade com interrupções, podes fazer a leitura de vários canais de ADC "em background", usando a interrupção do ADC. É assim que costumo fazer quando preciso de ler vários canais; a interrupção vai armazenando o resultado das conversões num array indexado por canal, e eu depois a qualquer momento e em qualquer sitio do programa uso o valor que estiver no array (com atenção à sincronização, se for caso disso).
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 21:51
Entendo o jeito que fala, seria implementar um modo contínuo, mas fazendo com o ADC configurado como single, passando para o ADC os canais que quer ler, num loop for, por exemplo, mas resetando o contador para que as leituras se façam infinitamente. Assim daria pra fazer sem utilizar o loop infinito do main. É verdade, vou fazer desse jeito. =)

O exemplo com os dois servos que te passei era o que eu queria fazer desde o começo. Tenho um projeto em que controlo dois servos remotamente e com potenciômetros. Esse arquivo aí veio antes dos de um servo só, daí hoje melhorei bastante o código, depois de postar aqui.. hahah

Vou fazer com o mesmo timer, mas dependendo da aplicação não me servirá, pois preciso do servo fixo na posição que eu lhe enviar, mas todo conhecimento agrega, mais tarde testo um exemplo com isso.

Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 22:18
Faço isso do ADC no código do DiffTrike, aqui está a ISR do ADC (o link é directo para a ISR):

https://github.com/vnevoa/DiffTrike/blob/MarkIV_RasPi_NJAY/Electronics/PowerBridge/sw/main.c#L447

O programa é um pouco complexo e tem lá mais umas coisas, mas sempre dá para ter uma ideia geral. No inicio do ISR lê-se logo o ADC (o ISR ocorre para indicar o fim de uma conversão) e vê-se qual o canal que foi lido (vendo o canal selecionado no mux), depois aponta-se o mux para o próximo canal a ler (no inicio de cada case do switch), guarda-se o valor lido do ADC na posição certa do array (isto é feito aqui no meio antes de dar inicio a nova conversão para dar um tempo ao mux para estabilizar no novo canal) e dá-se inicio a nova conversão (que terminará em novo ISR). O ADC não está em modo continuo, a inicialização do ADC tá logo a seguir à ISR. O AVR aqui é um ATtiny26 (2KB de flash!) mas o ADC é quase igual ao que tás a usar (presumo que tás com um ATmega).
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 12 de Dezembro de 2014, 22:22
Vou dar uma olhada. Do jeito que eu falei, não tem como fazer, não daria pra fugir do loop infinito do main, sem travar o programa.
Tentei também implementar a leitura do canal ADC dentro das ISR's dos respectivos timers, mas fica lento demais, chega a travar.
Vou ver essa página que me passou. E sim, estou usando o Atmega328p. Um arduino uno e uma outra placa que também tem um atmega328p.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 12 de Dezembro de 2014, 22:31
Uma conversão normal do ADC demora mais de 10us, por isso não dá para fazer isso e esperar dentro de um a ISR.
Não há nada de fundamentalmente errado em fazer coisas no loop principal, e é o que eu faço muitas vezes. Mas quando queremos optimizar é mais flexível tirar o maior proveito do hardware, deixá-lo fazer a maior fatia possível do trabalho, e isto implica usar ISRs para as coisas que demoram algum tempo. Há uma frase que não sei quem foi o autor, que disse algo como "quando usamos interrupções, é o trabalho que vem à procura do CPU e não o contrário" :)
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 13 de Dezembro de 2014, 00:12
Njay, implementei um código, colocando tudo em interrupções, mas ocorre que quando eu inicializo o timer0 e o timer2 (cada um com sua respectiva interrupção) e mais a interrupção do ADC, algo acontece e não funciona. Caso eu não inicialize um dos timers funciona, mas se inicializo os dois (faço isso através de chamada das funções  timer0_init(); e timer2_init(); na função main). Testei comentando o timer0_init() aí funciona, depois comentando só o timer2_init, também funciona, mas os dois timers, consequentemente com os dois pots e os dois servos, não funciona. Me faz pensar se existe algum limite de interrupções simultâneas, ou se a interrupção do ADC faz uso de algum dos timers de 8bits (mascaradamente).

Sabe o que pode ser?
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 13 de Dezembro de 2014, 00:15
Njay, segue o código:


Código: [Seleccione]
/*
 *
 * Created: 11/12/2014 11:15:59
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>

#define SERVO_STEER (1 << PD6)
#define SERVO_GEAR (1 << PD5)

volatile uint16_t servoSteerPulse;
volatile uint16_t servoGearPulse;

volatile uint8_t selectedPot = 0;

void ADC_init() {
    ADCSRA = (7 << ADPS0); // prescaler de 128
    ADMUX = (1 << REFS0); // tensão de referência a própria alimentação do mcu
    ADCSRA |= (1 << ADIE); // habilita interrupção por conversão concluída
    ADCSRA |= (1 << ADEN); // habilita ADC
    ADCSRA |= (1 << ADSC); // inicializa conversões - faz uma primeira conversão
}

void CONFIG_servos() {
    DDRD = (SERVO_STEER) | (SERVO_GEAR); // Seta o pino como saída
    PORTD &= ~SERVO_STEER;
    PORTD &= ~SERVO_GEAR;
}

void timer0_init(void) {
    TCCR0A = (1<<WGM01); // Timer no modo CTC
    TCCR0B = (1<<CS01); // Prescaler em 1:8
    OCR0A = 19; // interrupção à cada 0.01ms - (((Fosc / prescaler) / ((1/tempo alvo) * 1000)) -1), ou seja ((( 16Mhz / 8) / ((1/0.01) * 1000)) -1) = 19
    TIMSK0 = (1<<OCIE0A); // Habilita interrupções no timer
}

void timer2_init(void) {
    TCCR2A = (1<<WGM21); // Timer no modo CTC
    TCCR2B = (1<<CS21); // Prescaler em 1:8
    OCR2A = 19; // interrupção à cada 0.01ms - (((Fosc / prescaler) / ((1/tempo alvo) * 1000)) -1), ou seja ((( 16Mhz / 8) / ((1/0.01) * 1000)) -1) = 19
    TIMSK2 = (1<<OCIE2A); // Habilita interrupções no timer
}

int main(void) {
    CONFIG_servos();
    ADC_init();
    timer0_init();
    //timer2_init();
    sei(); // Habilita interrupções globais
    while(1);
    return 1;
}

ISR(ADC_vect) {
    if(!selectedPot) {
        servoSteerPulse = ADC; // atribui valor do ADC à variável
        selectedPot = 1; // altera valor do pot da vez       
    } else {
        servoGearPulse = ADC;
        selectedPot = 0;
    }
        ADMUX &= 0XF0; // limpa canais do ADC
        ADMUX |= selectedPot; // configura novo canal para conversão
        ADCSRA |= (1 << ADSC); // realiza a conversão
}

ISR(TIMER0_COMPA_vect) {
    static uint16_t counterSteer;
    counterSteer++; // Incrementa o contador
    if(counterSteer == 2000) { // implementa em 20ms
        PORTD ^= SERVO_STEER; // inverte nível lógico do pino ha cada 20ms/50Hz
    } else if(counterSteer >= (2047 + (servoSteerPulse / 5))) {
        PORTD ^= SERVO_STEER; // inverte nível lógico do pino, segundo o valor do potenciômetro, gerando o dutty cycle
        counterSteer -= 2000;
    }
}

ISR(TIMER2_COMPA_vect) {
    static uint16_t counterGear;
    counterGear++; // Incrementa o contador
    if(counterGear == 2000) { // implementa em 20ms
        PORTD ^= SERVO_GEAR; // inverte nível lógico do pino ha cada 20ms/50Hz
    } else if(counterGear >= (2047 + (servoGearPulse / 5))) {
        PORTD ^= SERVO_GEAR; // inverte nível lógico do pino, segundo o valor do potenciômetro, gerando o dutty cycle
        counterGear -= 2000;
    }
}

eu estou comentando a função timer2_init();, para funcionar, mas se descomentar ela e comentar a timer0_init(); também funciona, porém, com as duas juntas não funciona. Não sei, poderia ser falta de energia também, mas o código antigo funcionava com os dois simultaneamente e usando a mesma fonte de energia.

Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Njay em 13 de Dezembro de 2014, 00:47
À partida não vejo nenhum problema óbvio. Não verifiquei a programação dos timers, é melhor re-verificares o nome dos registos e dos bits, eu tinha a ideia de que eles não era assim tão "iguais".

Também podes experimentar fazer

static uint16_t counterGear = 1000;

A ideia aqui é "des-sincronizar" as interrupções dos timers para não ocorrerem ao "mesmo tempo", o que vai fazer uma delas atrasar-se.

Também se pode estar a dar o caso de que não há CPU power suficiente para executar tudo; estás a ter 2 interrupções de timer a cada 10us, isto pode já ser muita carga no CPU e ele não consegue fazer tudo a tempo. A 16MHz são 160 instruções, 80 instruções por interrupção, mais o resto das coisas que ele tem para fazer (ADC ISR). Tenta aumentar o periodo das interrupções, por exemplo para ~50us para experimentar.
Título: Re: PWM, Potenciômetro e Servo; Com timer0, modo CTC e rodando assíncrono
Enviado por: Pinout em 13 de Dezembro de 2014, 11:35
São bem parecidos mesmo, praticamente os mesmos, com exceção de que muda-se de 0 para 2 nos nomes. Não é erro neles pois eles funcionam separadamente,
e quando coloco o ADC sem interrupção, funciona com os dois perfeitamente. Vou continuar a pesquisar o que pode ser.