Memo. AVR Buzic


Essence


Eu já criei vários dispositivos eletrônicos de hobby diferentes e tenho uma característica estranha: se houver um emissor piezoelétrico (campainha) no quadro, depois de terminar o trabalho principal do projeto, começo a sofrer besteiras e fazê-lo tocar várias melodias (o máximo possível) ) É especialmente útil incluir uma melodia no final de um longo processo para atrair atenção. Por exemplo, usei-o quando construí uma câmera de exposição improvisada para iluminar o fotorresistente etc.

Mas quando comecei a procurar exemplos de geração de frequência para AVR na rede, por algum motivo me deparei com projetos monstruosos ou insuficientemente concisos implementando a geração de frequência sonora de maneira puramente programática. E então eu decidi descobrir isso sozinho ...

Digressão lírica


Meu hobby inclui a criação de vários dispositivos em microcontroladores, porque isso não se cruza com o meu professor. atividade (desenvolvimento de software), eu me considero um autodidata absoluto, e em eletrônica não é muito forte. Na verdade, eu prefiro microcontroladores PIC, mas aconteceu que acumulei um certo número de microcontroladores Atmel AVR (agora Microchip). Faça imediatamente uma reserva de que eu nunca tinha o AVR em minhas mãos, ou seja, Este é o meu primeiro projeto no Atmel MCU, o Atmega48pa. O projeto em si realiza alguma carga útil, mas aqui descreverei apenas parte dele relacionada à geração de frequências sonoras. O teste para gerar frequências chamei de "buzic", como uma abreviação de música de campainha. Sim, eu quase esqueci: no Habr, existe um usuário com o apelido buzic , eu queria avisar imediatamente que esse memorando não se aplica a ele de nenhuma maneira e, apenas no caso, peço desculpas imediatamente por usar a combinação de letras "Buzic".

Então vamos


Eu me familiarizei com um grande número de exemplos da rede - todos eles são construídos no ciclo mais simples no corpo principal do firmware ou na interrupção do temporizador. Mas todos eles usam a mesma abordagem para gerar frequência:

  1. alimente um nível alto ao pé do microcontrolador
  2. atrasar
  3. alimentação baixa ao pé do microcontrolador

Alteração dos atrasos e das configurações do timer - ajuste a frequência.

Essa abordagem não me agradou muito, porque Eu não queria escrever código para o controle manual do pé do microcontrolador. Eu gostaria que a “pedra” gerasse a frequência do som para mim e apenas defina os valores de certos registros, alterando-a (frequência).

Ao estudar a folha de dados (doravante denominada DS), ainda encontrei o modo de timer que eu precisava - e esse modo, como você deve ter adivinhado, é o modo CTC (Clear Timer on Compare Match). Como a função de tocar música não é, para dizer o mínimo, a principal funcionalidade, preferi selecionar o temporizador 2 para ela (parágrafo 22 do SD).

Todo mundo sabe que praticamente qualquer microcontrolador tem um modo de geração de sinal PWM que é implementado em temporizadores e é completamente hardware. Mas nesta tarefa, o PWM não é adequado porque apenas uma frequência será gerada no hardware. Portanto, precisamos de PFM (modulação de frequência de pulso). Alguma similaridade do PFM é o modo do temporizador CTC (cláusula 22.7.2 LH).

Modo CTC


O temporizador 2 no microcontrolador Atmega48pa é de 8 bits, ou seja, “marca” de 0 a 255 e depois circula. A propósito, o cronômetro pode ir em uma direção diferente, mas não no nosso caso. O próximo componente necessário é a Unidade de comparação. Falando muito grosseiramente, este módulo é o iniciador de qualquer evento relacionado ao cronômetro. Os eventos podem ser diferentes - como interrupções, mudanças no nível de certas pernas do microcontrolador, etc. (Obviamente, estamos interessados ​​no segundo). Como você pode imaginar, o módulo de comparação não é apenas nomeado - ele compara um valor específico selecionado pelo desenvolvedor do firmware com o valor atual do timer. Se o valor do timer atingir o valor definido, ocorrerá um evento. Eventos também podem ocorrer quando o cronômetro exceder o limite ou durante uma redefinição. Ok, chegamos à conclusão de que é conveniente para nós, em determinados momentos, que o timer, juntamente com o módulo de comparação, altere independentemente o nível no pé do microcontrolador para o oposto - gerando pulsos.

A segunda tarefa é definir os intervalos entre esses pulsos - ou seja, controlar a frequência de geração. Toda a singularidade do modo CTC reside no fato de que, neste modo, o temporizador não chega ao fim (255), mas é redefinido quando o valor definido é atingido. Assim, alterando esse valor, podemos realmente controlar a frequência. Por exemplo, se definirmos o valor do módulo de comparação como 10, a mudança de nível no pé do microcontrolador ocorrerá 20 vezes mais frequentemente do que se o configurássemos (o valor do módulo de comparação) para 200. Agora podemos controlar a frequência!



Ferro



A pinagem do microcontrolador mostra que precisamos conectar nossa campainha à perna do PB3 (OC2A) ou à perna do PD3 (OC2B), porque OC2A e OC2B significam exatamente que nessas pernas, o timer 2 pode gerar sinais.

O esquema que eu costumo usar para conectar a campainha é:


E assim montamos o dispositivo.

Registros


No parágrafo anterior, decidimos sobre a escolha da perna - este é o PB3 (OC2A), vamos trabalhar com ele. Se você precisar de PD3, para ela tudo será o mesmo, o que será claramente visível na história.

Vamos configurar nosso timer 2 alterando 3 registros:
  1. TCCR2A - configurações de modo e seleção de comportamento
  2. TCCR2B - configurações de modo e divisor de frequência do temporizador (também bits FOC - não os usamos)
  3. OCR2A (OCR2B para o estojo PD3) - valor do módulo de comparação


Considere primeiro os registros TCCR2A e TCCR2B

Como você pode ver, temos 3 grupos de bits que são significativos para nós - estes são bits das séries COM2xx, WGM2x e CS2x
A primeira coisa que precisamos mudar é o WGM2x - essa é a principal opção para escolher o modo de geração - esses bits são usados ​​para selecionar o modo CTC.


nota: obviamente, no LH, o erro de digitação em "Atualização do OCR0x em" deve ser OCR2x

I.e. o código será assim:
TCCR2A = _BV(WGM21) ; 

Como você pode ver, o TCCR2B ainda não é usado. WGM22 deve ser zero, mas já é zero.

O próximo passo é configurar os bits COM2xx, mais precisamente COM2Ax - porque trabalhamos com a perna PB3 (para PD3, COM2Bx são usados ​​de maneira semelhante). O que acontecerá com a nossa perna PB3 depende deles.

Os bits COM2xx dependem do modo que selecionamos com os bits WGM2x, portanto, teremos que encontrar a seção correspondente no LH. Porque temos o modo CTC, ou seja, PWM, procure a placa “Compare Output Mode, non-PWM”, aqui está:

Aqui você precisa selecionar "Alternar" - para que o nível na perna mude para o oposto quando o timer atingir o valor definido. A mudança constante de nível e implementa a geração da frequência que precisamos.

Porque os bits COM2xx também estão no registro TCCR2A - somente ele muda:
 TCCR2A = _BV(COM2A0) | _BV(WGM21) ; 

Naturalmente, você também precisa selecionar o divisor de frequência com bits CS2x e, é claro, definir o pé PB3 para a saída ... mas ainda não o faremos para que, quando ligarmos o MK, não obteremos um grito agudo a uma frequência incompreensível, mas quando fizermos todas as outras configurações e ligue o pé para sair - será descrito abaixo.

Então, vamos dar uma olhada completa na nossa inicialização:
 #include <avr/io.h> //set bit - using bitwise OR operator #define sbi(x,y) x |= _BV(y) //clear bit - using bitwise AND operator #define cbi(x,y) x &= ~(_BV(y)) #define BUZ_PIN PB3 void timer2_buzzer_init() { // PB3 cbi(PORTB, BUZ_PIN); // PB3  ,    cbi(DDRB, BUZ_PIN); //  TCCR2A = _BV(COM2A0) | _BV(WGM21) ; //   (      ) OCR2A = 0; } 

Usei as macros cbi e sbi (espionadas em algum lugar da rede) para definir bits individuais e as deixei assim. Essas macros, é claro, foram colocadas no arquivo de cabeçalho, mas para maior clareza, eu as coloquei aqui.

Cálculo da frequência e duração das notas


Agora chegamos à essência da questão. Há algum tempo, conhecidos de músicos tentaram introduzir no meu cérebro um programador com algumas informações sobre a equipe musical, meu cérebro quase ferveu, mas ainda assim eu trouxe um grão útil dessas conversas.
Eu o aviso imediatamente - são possíveis grandes imprecisões.
  1. cada medida consiste em 4 quartos
  2. Cada melodia tem um ritmo - ou seja, o número de tais quartos por minuto
  3. Cada nota pode ser tocada como um compasso inteiro, assim como sua parte 1/2, 1/3, 1/4, etc.
  4. Cada nota, é claro, tem uma certa frequência

Examinamos o caso mais comum; aliás, tudo é mais complicado, pelo menos para mim, por isso não discutirei esse tópico no quadro desta história.

Bem, tudo bem, vamos trabalhar com o que temos. O mais importante para nós é obter a frequência da nota (na verdade, o valor do registro OCR2A) e sua duração, por exemplo, em milissegundos. Portanto, é necessário fazer alguns cálculos.

Porque Como estamos dentro da estrutura de uma linguagem de programação, as melodias são mais fáceis de armazenar em uma matriz. A maneira mais lógica de definir cada elemento da matriz no formato é nota + duração. É necessário calcular o tamanho do elemento em bytes, porque escrevemos sob o microcontrolador e com recursos aqui está apertado - isso significa que o tamanho do elemento em bytes deve ser adequado.

Frequência


Vamos começar com a frequência. Porque temos o temporizador 2 de 8 bits, o registro de comparação do OCR2A também é de 8 bits. Ou seja, nosso elemento da matriz de melodia já terá pelo menos 2 bytes, porque você ainda precisará salvar a duração. De fato, 2 bytes é o limite para esse tipo de embarcação. Ainda não conseguimos um bom som, para dizer o mínimo, e gastar mais bytes não é razoável. Então, paramos em 2 bytes.

Ao contar a frequência, de fato, surge outro grande problema.
Se você observar as frequências das notas, veremos que elas são divididas em oitavas.

Para a maioria das melodias simples, 3 oitavas são suficientes, mas eu decidi me esquivar e implementar 6: grande, pequena e as próximas 4.

Agora vamos desviar da música e voltar ao mundo da programação de microcontroladores.
Qualquer temporizador no AVR (e a grande maioria dos outros MK) está vinculado à frequência do próprio MK. A frequência do quartzo no meu circuito é 16Mhz. A mesma frequência é determinada pelo F_CPU "define" para ser igual a 16000000 no meu caso. No registro TCCR2B, podemos selecionar divisores de frequência para que nosso timer 2 não "marque" a uma velocidade frenética de 16000000 vezes por segundo, mas um pouco mais lenta. O divisor de frequência é selecionado pelos bits CS2x, como mencionado acima.


nota: obviamente, no LH, um erro de digitação em vez de "CA2x" deve ser CS2x

Surge a pergunta - como configurar o divisor?

Para fazer isso, você precisa entender como calcular os valores para o registro OCR2A. E calcular é bem simples:
OCR2A = F_CPU / (divisor de frequência de quartzo * 2) / frequência de notas
Por exemplo, tome a nota ANTES da primeira oitava e do divisor 256 (CS22 = 1, CS21 = 1, CS20 = 0):
OCR2A = 16000000 / (256 * 2) / 261 = 119

Explicarei imediatamente de onde veio a multiplicação por 2. O fato é que selecionamos o modo "Alternar" com os registros COM2Ax, o que significa que a alteração dos níveis no pé de baixo para alto (ou vice-versa) e vice-versa ocorrerá em 2 passagens do temporizador: primeiro o temporizador atinge o valor de OCR2A e altera o pé do microcontrolador, por exemplo, de 1 para 0, é redefinido e apenas na segunda volta muda de 0 para 1. Portanto, 2 voltas do temporizador vão para cada onda completa, respectivamente, o divisor deve ser multiplicado por 2, caso contrário, obteremos apenas metade da frequência da nossa nota.

Daí o infortúnio mencionado ...

Se tomarmos a nota ANTES da grande oitava e deixar o divisor 256:
OCR2A = 16000000 / (256 * 2) / 65 = 480 !!!
480 - esse número é claramente superior a 255 e não se encaixa fisicamente no registro OCR2A de 8 bits.

O que fazer? Obviamente, alterando o divisor, mas se colocarmos o divisor 1024, com uma oitava grande, tudo ficará bem. Os problemas começarão com as oitavas superiores:
LA quarta oitava - OCR2A = 16000000 / (1024 * 2) / 3520 = 4
Uma quarta oitava acentuada - OCR2A = 16000000 / (1024 * 2) / 3729 = 4
Os valores do OCR2A não são mais diferentes, o que significa que o som também para de diferente.

Existe apenas uma saída: para a frequência das notas, é necessário armazenar não apenas os valores do registro OCR2A, mas também os bits do divisor de frequência de quartzo. Porque para oitavas diferentes, haverá um valor diferente do divisor de frequência de quartzo, que precisaremos definir no registro TCCR2B!

Agora tudo se encaixa - e finalmente expliquei por que não conseguimos preencher imediatamente o valor do divisor na função timer2_buzzer_init ().

Infelizmente, o divisor de frequência tem mais 3 bits. E eles terão que ser obtidos no segundo byte do elemento da matriz de melodia.

Viva as macros
 #define DIV_MASK (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_1024 (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_256 (_BV(CS21) | _BV(CS22)) #define DIV_128 (_BV(CS20) | _BV(CS22)) #define DIV_64 _BV(CS22) #define DIV_32 (_BV(CS20) | _BV(CS21)) #define NOTE_1024( x ) ((F_CPU / (1024 * 2) / x) | (DIV_1024 << 8)) #define NOTE_256( x ) ((F_CPU / (256 * 2) / x) | (DIV_256 << 8)) #define NOTE_128( x ) ((F_CPU / (128 * 2) / x) | (DIV_128 << 8)) #define NOTE_64( x ) ((F_CPU / (64 * 2) / x) | (DIV_64 << 8)) #define NOTE_32( x ) ((F_CPU / (32 * 2) / x) | (DIV_32 << 8)) //  #define DOB NOTE_1024( 65 ) #define DO_B NOTE_1024( 69 ) #define REB NOTE_1024 ( 73 ) #define RE_B NOTE_1024 ( 78 ) #define MIB NOTE_1024 ( 82 ) #define FAB NOTE_1024 ( 87 ) #define FA_B NOTE_1024 ( 93 ) #define SOLB NOTE_1024 ( 98 ) #define SOL_B NOTE_1024 ( 104 ) #define LAB NOTE_1024 ( 110 ) #define LA_B NOTE_1024 ( 116 ) #define SIB NOTE_1024 ( 123 ) //  #define DOS NOTE_256( 131 ) #define DO_S NOTE_256( 138 ) #define RES NOTE_256 ( 146 ) #define RE_S NOTE_256 ( 155 ) #define MIS NOTE_256 ( 164 ) #define FAS NOTE_256 ( 174 ) #define FA_S NOTE_256 ( 185 ) #define SOLS NOTE_256 ( 196 ) #define SOL_S NOTE_256 ( 207 ) #define LAS NOTE_256 ( 219 ) #define LA_S NOTE_256 ( 233 ) #define SIS NOTE_256 ( 246 ) //  #define DO1 NOTE_256( 261 ) #define DO_1 NOTE_256( 277 ) #define RE1 NOTE_256 ( 293 ) #define RE_1 NOTE_256 ( 310 ) #define MI1 NOTE_256 ( 329 ) #define FA1 NOTE_256 ( 348 ) #define FA_1 NOTE_256 ( 369 ) #define SOL1 NOTE_256 ( 391 ) #define SOL_1 NOTE_256 ( 414 ) #define LA1 NOTE_256 ( 439 ) #define LA_1 NOTE_256 ( 465 ) #define SI1 NOTE_256 ( 493 ) //  #define DO2 NOTE_128( 522 ) #define DO_2 NOTE_128( 553 ) #define RE2 NOTE_128 ( 586 ) #define RE_2 NOTE_128 ( 621 ) #define MI2 NOTE_128 ( 658 ) #define FA2 NOTE_128 ( 697 ) #define FA_2 NOTE_128 ( 738 ) #define SOL2 NOTE_128 ( 782 ) #define SOL_2 NOTE_128 ( 829 ) #define LA2 NOTE_128 ( 878 ) #define LA_2 NOTE_128 ( 930 ) #define SI2 NOTE_128 ( 985 ) //  #define DO3 NOTE_64( 1047 ) #define DO_3 NOTE_64( 1109 ) #define RE3 NOTE_64 ( 1175 ) #define RE_3 NOTE_64 ( 1245 ) #define MI3 NOTE_64 ( 1319 ) #define FA3 NOTE_64 ( 1397 ) #define FA_3 NOTE_64 ( 1480 ) #define SOL3 NOTE_64 ( 1568 ) #define SOL_3 NOTE_64 ( 1661 ) #define LA3 NOTE_64 ( 1760 ) #define LA_3 NOTE_64 ( 1865 ) #define SI3 NOTE_64 ( 1976 ) //  #define DO4 NOTE_32( 2093 ) #define DO_4 NOTE_32( 2217 ) #define RE4 NOTE_32 ( 2349 ) #define RE_4 NOTE_32 ( 2489 ) #define MI4 NOTE_32 ( 2637 ) #define FA4 NOTE_32 ( 2794 ) #define FA_4 NOTE_32 ( 2960 ) #define SOL4 NOTE_32 ( 3136 ) #define SOL_4 NOTE_32 ( 3322 ) #define LA4 NOTE_32 ( 3520 ) #define LA_4 NOTE_32 ( 3729 ) #define SI4 NOTE_32 ( 3951 ) 



E para a duração da nota, temos apenas 5 bits restantes, então vamos calcular a duração.

Duração


Primeiro, você precisa traduzir o valor do andamento em unidades temporárias (por exemplo, em milissegundos) - eu fiz assim:
Duração de uma medida musical em ms = (60.000 ms * 4 quartos) / valor do andamento.

Portanto, se estamos falando de partes de batida, esse valor precisa ser dividido e, a princípio, pensei que o turno à esquerda usual para os divisores seria suficiente. I.e. o código era este:

 uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { return (precalced_tempo / _BV((note >> 11) & 0b00111)); } 


I.e. Eu usei 3 bits (dos 5 restantes) e consegui partes da batida musical dos graus 2 para 1/128. Mas quando eu dei a um amigo que me pedia para escrever algum tipo de toque no meu pedaço de ferro, havia perguntas sobre por que não havia 1/3 ou 1/6 e comecei a pensar ...

No final, criei um sistema complicado para obter tais durações. Um pouco do restante de 2x - gastei no sinal de multiplicação por 3 para o divisor de relógio obtido após o turno. E o último bit é indicar se você deve subtrair 1. É difícil descrever, é mais fácil ver o código:
 uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { note >>= 11; uint8_t divider = _BV(note & 0b00111); note >>= 3; divider *= ((note & 0b01) ? 3 : 1); divider -= (note >> 1); return (precalced_tempo / divider); } 

Então, eu "defini" todas as notas possíveis (exceto aquelas que são menores que 1/128).
Aqui estão eles
 #define DEL_MINUS_1 0b10000 #define DEL_MUL_3 0b01000 #define DEL_1 0 #define DEL_1N2 1 #define DEL_1N3 (2 | DEL_MINUS_1) #define DEL_1N4 2 #define DEL_1N5 (1 | DEL_MINUS_1 | DEL_MUL_3) #define DEL_1N6 (1 | DEL_MUL_3) #define DEL_1N7 (3 | DEL_MINUS_1) #define DEL_1N8 3 #define DEL_1N11 (2 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N12 (2 | DEL_MUL_3) #define DEL_1N15 (4 | DEL_MINUS_1) #define DEL_1N16 4 #define DEL_1N23 (3 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N24 (3 | DEL_MUL_3) #define DEL_1N31 (5 | DEL_MINUS_1) #define DEL_1N32 5 #define DEL_1N47 (4 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N48 (4 | DEL_MUL_3) #define DEL_1N63 (6 | DEL_MINUS_1) #define DEL_1N64 6 #define DEL_1N95 (5 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N96 (5 | DEL_MUL_3) #define DEL_1N127 (7 | DEL_MINUS_1) #define DEL_1N128 7 



Juntando tudo


Total, temos o seguinte formato para o elemento da nossa matriz de toques.

  • 1bit: divisor de atraso - 1
  • 1bit: divisor de atraso * 3
  • 3bit: deslocamento do divisor de atraso
  • 3bit: divisor do relógio da cpu
  • 8bit: valor OCR2A

Apenas 16 bits.

Caro leitor, se desejar, você mesmo pode sonhar com o formato, talvez algo mais amplo do que o meu nascerá.

Esquecemos de adicionar uma nota vazia, ou seja, silêncio. E, finalmente, expliquei por que, no início, na função timer2_buzzer_init (), definimos especialmente o trecho do PB3 na entrada e não na saída. Alterando o registro DDRB, ligamos e desligamos a reprodução do "silêncio" ou da composição como um todo. Porque não podemos ter notas com o valor 0 - será uma nota "vazia".

Defina as macros ausentes e a função para ativar a geração de som:
 #define EMPTY_NOTE 0 #define NOTE(delay, note) (uint16_t)((delay << 11) | note) ........ ........ ........ void play_music_note(uint16_t note) { if (note) { TCCR2B = (note >> 8) & DIV_MASK; OCR2A = note & 0xff; sbi(DDRB, BUZ_PIN); } else cbi(DDRB, BUZ_PIN); } 

Agora vou mostrar como é um toque escrito de acordo com esse princípio:

 const uint16_t king[] PROGMEM = { NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N2, SI3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, EMPTY_NOTE), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, FA3), NOTE(DEL_1N2, LA3), NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, MI4), NOTE(DEL_1N4, RE4), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N2, RE4), NOTE(DEL_1N2, EMPTY_NOTE), }; 


Tocando toque


Ainda temos uma tarefa - tocar a melodia. Para fazer isso, precisamos "executar" o arranjo de toques, suportando as pausas apropriadas e alterando as frequências das notas. Obviamente, precisamos de outro temporizador, que, a propósito, possa ser usado para outras tarefas gerais, como eu normalmente faço. Além disso, você pode alternar entre os elementos da matriz na interrupção deste timer ou no loop principal e usar o timer para calcular o tempo. Neste exemplo, usei a segunda opção.

Como você sabe, o corpo de qualquer programa para MK inclui um loop infinito:
 int main(void) { for(;;) { //   } return 0; } 

Nele, "correremos" ao longo de nossa matriz. Mas precisamos de uma função semelhante ao GetTickCount do WinApi, que retorne o número de milissegundos nos sistemas operacionais Windows. Mas, naturalmente, no mundo do MK não existem essas funções "prontas para uso", então devemos escrevê-lo.

Temporizador 1


Para calcular os intervalos de tempo (não intencionalmente escrevo milissegundos, mais tarde você entenderá o porquê). Usei o timer 1 em conjunto com o modo CTC já conhecido por nós. O timer 1 é um timer de 16 bits, o que significa que o valor do módulo de comparação já está indicado por 2 registros de 8 bits OCR1AH ​​e OCR1AL - para os bytes alto e baixo, respectivamente. Não quero descrever em detalhes o trabalho com o timer 1, pois isso não se aplica ao tópico principal deste memorando. Portanto, direi apenas em duas palavras.

Na verdade, precisamos de 3 funções:
  • Inicialização do timer
  • Manipulador de interrupção do temporizador
  • função que retorna o número de intervalos de tempo.

Arquivo de código C
 #include <avr/io.h> #include <avr/interrupt.h> #include <util/atomic.h> #include "timer1_ticks.h" volatile unsigned long timer1_ticks; //  ISR (TIMER1_COMPA_vect) { timer1_ticks++; } void timer1_ticks_init() { //   // CTC ,     8 TCCR1B |= (1 << WGM12) | (1 << CS11); //     OCR1AH = (uint8_t)(CTC_MATCH_OVERFLOW >> 8); OCR1AL = (uint8_t) CTC_MATCH_OVERFLOW; //    TIMSK1 |= (1 << OCIE1A); } unsigned long ticks() { unsigned long ticks_return; //  ,   ticks_return   //     ATOMIC_BLOCK(ATOMIC_FORCEON) { ticks_return = timer1_ticks; } return ticks_return; } 



Antes de mostrar o arquivo de cabeçalho com uma certa constante CTC_MATCH_OVERFLOW, precisamos voltar no tempo para a seção "Duração" e determinar a macro mais importante para a melodia, que calcula o andamento da melodia. Esperei muito tempo para determiná-lo, pois ele está diretamente conectado ao player e, portanto, ao timer 1.
Em uma primeira aproximação, ficou assim (consulte os cálculos na seção "Duração"):
 #define TEMPO( x ) (60000 * 4 / x) 

O valor que obtemos na saída, deve substituir posteriormente o primeiro argumento na função calc_note_delay . Agora, observe atentamente a função calc_note_delay, a saber a linha:
 return (precalced_tempo / divider); 

Vemos que o valor obtido pelo cálculo da macro TEMPO é dividido por um determinado divisor. Lembre-se de que o divisor máximo que definimos é DEL_1N128 , ou seja, o divisor será 128.

Agora vamos pegar o valor do andamento comum igual a 240 e fazer alguns cálculos simples:
60000 * 4/240 = 1000
Oh horror! Temos apenas 1000, já que esse valor ainda será dividido por 128, corremos o risco de cair para 0, a taxas altas. Esta é a segunda questão de duração.

Como resolver isso? Obviamente, para expandir o intervalo de valores de andamento, precisamos aumentar o número obtido calculando a macro TEMPO. Isso pode ser feito de apenas uma maneira - para evitar milissegundos e contar o tempo em determinados intervalos de tempo. Agora você entende por que todo esse tempo eu evitei mencionar "milissegundos" na história. Vamos definir outra macro:
  #define MS_DIVIDER 4 

Seja o nosso divisor de milissegundos - divida o milissegundo, por exemplo, por 4 (250 μs).
Então você precisa alterar a macro TEMPO:
 #define TEMPO( x ) (60000 * MS_DIVIDER * 4 / x) 


Agora, com a consciência limpa, darei o arquivo de cabeçalho para trabalhar com o timer 1:
 #ifndef TIMER1_TICKS_H_INCLUDED #define TIMER1_TICKS_H_INCLUDED #define MS_DIVIDER 4 #define CTC_MATCH_OVERFLOW ((F_CPU / 1000) / (8 * MS_DIVIDER)) void timer1_ticks_init(); unsigned long ticks(); #endif // TIMER1_TICKS_H_INCLUDED 

Agora podemos, alterando MS_DIVIDER, ajustar o intervalo para nossas tarefas - eu tenho 4 no meu código - isso foi suficiente para minhas tarefas. Atenção: se você ainda tiver alguma tarefa "vinculada" ao timer 1, não se esqueça de multiplicar / dividir os valores de controle de tempo para eles por MS_DIVIDER.

Plataforma giratória


Agora vamos escrever nosso player. Eu acho que tudo ficará claro com o código e os comentários.

 int main(void) { timer1_ticks_init(); //   sei(); timer2_buzzer_init(); //    MS_DIVIDER long time_since = ticks(); //       MS_DIVIDER uint16_t note_delay = 0; //     uint16_t note_pos = 0; //  uint16_t length = sizeof(king) / sizeof(king[0]); //     uint16_t tempo = TEMPO(240); for(;;) { unsigned long time_current = ticks(); if (time_current - time_since > note_delay) { //   uint16_t note = pgm_read_word(&king[note_pos]); //   play_music_note(note); //    note_delay = calc_note_delay(tempo, note); //  if (++note_pos >= length) note_pos = 0; time_since = time_current; } } return 0; } 


Conclusão


Espero que este memorando seja útil para um leitor respeitado e para mim, para não esquecer todas as nuances da reprodução de músicas, caso eu pegue os microcontroladores AVR novamente.

Bem, tradicionalmente, o vídeo e o código-fonte (eu o desenvolvi no ambiente Code Blocks, portanto, não tenha medo de arquivos obscuros):



Código fonte

Source: https://habr.com/ru/post/pt402329/


All Articles