Software full-duplex UART para ATtiny13

Olá a todos os usuários do Geektimes! Uma vez, como resultado do meu serviço, eu precisei implementar o software UART no popular microcontrolador ATtiny13. Pesquisando no Google, encontrei um grande número de artigos sobre esse tópico, muitos deles são publicados aqui:


E existem outros recursos:


A implementação mais recente, em geral, satisfaz minhas necessidades (full duplex). Mas, primeiro, o código está escrito no CodeVision AVR, que eu não uso por razões puramente religiosas, e segundo, comentários pouco comentados sobre assembler inserem ainda mais desencorajar o desejo de entender o código. Eu me propus a escrever em C uma biblioteca UART full-duplex fácil de usar. E, ao mesmo tempo, escreva um artigo sobre isso, porque a tarefa é bastante interessante devido à quantidade muito limitada de memória e recursos do controlador (apenas um timer de 8 bits). Para iniciantes na programação de microcontroladores, este será um bom material de treinamento, pois Eu mesmo no processo de escrever a biblioteca, quase do zero eu dominei a arquitetura do AVR.

Quem se importa - bem-vindo ao kat, haverá muito texto, muitos códigos-fonte com comentários no código.

Então, vamos começar. Vou simplesmente postar o arquivo de cabeçalho uart13.h no formato em que ele está com comentários no código, tudo é simples lá.

Arquivo de cabeçalho Uart13.h
/*    UART   ATtiny */

#ifndef _UART13_H_
#define _UART13_H_ 1

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

/*
*	        
*	    UART.
*/

#define TXPORT PORTB		//    
#define RXPORT PINB		//    
#define TXDDR DDRB		//     
#define RXDDR DDRB		//     
#define TXD 0			//       
#define RXD 1			//       

/*
*	  ,     ()
*	 BAUD_DIV   :
*	BAUD_DIV = (CPU_CLOCK / DIV) / BAUD_RATE
*	 CPU_CLOCK -   , BAUD_RATE -   UART,
*	 DIV -    ,   TCCR0B.
*	,   9.6 ,   8,   9600 :
*	BAUD_DIV = (9 600 000 / 8) / 9600 = 125 (0x7D).
*/

//#define T_DIV		0x01	// DIV = 1
#define T_DIV		0x02	// DIV = 8
//#define T_DIV		0x03	// DIV = 64
#define BAUD_DIV	0x7D	//  = 9600 

/*
*	         UART
*/

volatile uint16_t txbyte;
volatile uint8_t rxbyte;
volatile uint8_t txbitcount;
volatile uint8_t rxbitcount;

void uart_init();
void uart_send(uint8_t tb);
int16_t uart_recieve(uint8_t* rb);

#endif /* _UART13_H_ */



Mas dividirei a descrição do código de implementação da biblioteca em partes para não transformar o artigo em um grande spoiler com código.

Interromper TIM0_COMPA
ISR(TIM0_COMPA_vect)
{
	TXPORT = (TXPORT & ~(1 << TXD)) | ((txbyte & 0x01) << TXD); //    TXD   txbyte
	txbyte = (txbyte >> 0x01) + 0x8000;	//  txbyte   1   1    (0x8000)
	if(txbitcount > 0)			//    (   ),
	{
		txbitcount--;			//     .
	}
}


Aqui vemos o código do manipulador de interrupção do timer em comparação com o registro OCR0A. Funciona constantemente e acontece sempre que o timer atinge o valor registrado no registro OCR0A. Quando isso acontece, o valor do timer no registro TCNT0 é redefinido automaticamente (modo do timer CTC, definido no registro TCCR0A). Essa interrupção é usada para enviar dados via UART. A variável txbyte é usada como um registro de deslocamento: toda vez que ocorre uma interrupção, o bit menos significativo da variável txbyte é definido como a saída TXD do microcircuito, após o qual o conteúdo da variável é deslocado para a direita e um é gravado no bit alto liberado.

Enquanto não houver dados a serem transferidos, o número 0xFFFF é armazenado na variável e, portanto, um alto nível lógico é mantido continuamente na saída TXD. Quando queremos transferir dados, devemos escrever no contador de bits o número de bits para transmissão: 1 inicialização, 8 bits de dados e 1 parada, para um total de 10 (0x0A), e gravar dados em txbyte para transmissão junto com o bit inicial. Depois disso, eles começarão a ser transmitidos imediatamente. A função void uart_send (uint8_t tb) está envolvida na formação do pacote.

Interromper TIM0_COMPB
ISR(TIM0_COMPB_vect)
{
	if(RXPORT & (1 << RXD))			//      RXD
		rxbyte |= 0x80;			//   1,   1    rxbyte
	
	if(--rxbitcount == 0)			//   1         
	{
		TIMSK0 &= ~(1 << OCIE0B);	//  ,   TIM0_COMPB
		TIFR0 |= (1 << OCF0B);		//    TIM0_COMPB
		GIFR |= (1 << INTF0);		//     INT0
		GIMSK |= (1 << INT0);		//   INT0
	}
	else
	{
		rxbyte >>= 0x01;		//   rxbyte   1
	}
}


Aqui vemos o manipulador de interrupção do timer em comparação com o registro OCR0B. Funciona de maneira semelhante à interrupção TIM0_COMPA, mas, diferentemente disso, quando essa interrupção é executada, o timer TCNT0 não é redefinido. Essa interrupção é permitida apenas quando recebemos dados, o resto do tempo é proibido. Quando isso acontece, verificamos o estado lógico da entrada RXD e, se for um, escrevemos um na ordem alta da variável de recebimento rxbyte, depois diminuímos o contador dos bits recebidos em um e, se tornar zero, encerramos a recepção. Caso contrário, deslocamos a variável rxbyte para a direita para prepará-la para receber o próximo bit.

Interromper INT0
ISR(INT0_vect)
{
	rxbitcount = 0x09;			// 8    1  
	rxbyte = 0x00;				//   rxbyte
	if(TCNT0 < (BAUD_DIV / 2))		//        
	{
		OCR0B = TCNT0 + (BAUD_DIV / 2);	//         
	}
	else
	{
		OCR0B = TCNT0 - (BAUD_DIV / 2);	//        
	}
	GIMSK &= ~(1 << INT0);			//    INT0
	TIFR0 |= (1 << OCF0A) | (1 << OCF0B);	//    TIM0_COMPA (B)
	TIMSK0 |= (1 << OCIE0B);		//    OCR0B
}


Interrompa INT0. Ele funciona na extremidade posterior do pulso na entrada INT0, é usado para rastrear o início da recepção de informações. Define o contador de bits como 9, redefine o conteúdo da variável rxbyte. Define o valor para o registro OCR0B, que determina a frequência da interrupção TIM0_COMPB, que deve estar no tempo no meio do bit recebido. Depois disso, a interrupção TIM0_COMPB é ativada e a interrupção INT0 é desativada.

A seguir, são apresentadas as funções do usuário para trabalhar com o UART.

Função Uart_send
void uart_send(uint8_t tb)
{
	while(txbitcount);		//      
	txbyte = (tb + 0xFF00) << 0x01; //     txbyte        1
	txbitcount = 0x0A;		//     10
}


Função de transferência de bytes UART. É necessário um byte para transmissão como argumento; não há valor de retorno. Se um byte estiver sendo transferido no momento em que a função é chamada, aguarda até que a transferência seja concluída, após o que grava bytes de txbyte nos 8 bits inferiores da variável para transmissão e os 8 bits superiores permanecem 0xFF, então desloca a variável para a esquerda, criando o bit inicial na ordem baixa. Define o contador de bits para 10.

Função Uart_recieve
int16_t uart_recieve(uint8_t* rb)
{
	if(rxbitcount < 0x09)		//       9
	{
		while(rxbitcount);	//     
		*rb = rxbyte;		//      
		rxbitcount = 0x09;	//    
		return (*rb);		// 
	}
	else
	{
		return (-1);		//   -1 ( )
	}
}


Função de recebimento de bytes UART. Leva um argumento para um ponteiro para uma variável de 8 bits em que o byte recebido estará contido. Retorna o byte recebido, se o byte for recebido e se não houver nada a receber, retorna (-1). Se, no momento, a função for chamada, ela estará aguardando a conclusão. Se a função for chamada duas vezes, na primeira vez retornará o byte recebido e na segunda vez (-1).

Função Uart_init
void uart_init()
{
	txbyte = 0xFFFF;		//     -  
	rxbyte = 0x00;			//     -  
	txbitcount = 0x00;		//     -  (   )
	rxbitcount = 0x09;		//      - 9 (  )
	
	TXDDR |= (1 << TXD);		//       
	RXDDR &= ~(1 << RXD);		//       
	TXPORT |= (1 << TXD);		//     TXD
	RXPORT |= (1 << RXD);		//     RXD
	OCR0A = BAUD_DIV;		//    OCR0A    
	TIMSK0 |= (1 << OCIE0A);	//   TIM0_COMPA
	TCCR0A |= (1 << WGM01);		//   CTC ( TCNT0   OCR0A)
	TCCR0B |= T_DIV;		//        
	MCUCR |= (1 << ISC01);		//   INT0    
	GIMSK |= (1 << INT0);		//   INT0
	sei();				//   
}


Função de inicialização UART. Sem argumentos, sem valor de retorno. Inicializa variáveis ​​globais e registradores de microcontroladores. Pelos comentários no código, tudo deve ficar claro.

Portanto, é hora de escrever alguns main () simples usando nossa biblioteca e ver o que aconteceu em termos de quantidade de código e verificar o desempenho.

main.c
#include "uart13.h"

int main(void)
{
	uint8_t b = 0;
	uart_init();
	while (1) 
	{
		if(uart_recieve(&b) >= 0)	//    ,    
			uart_send(b);		//   ,  
	}
	return (0);
}


Nós compilamos:
Uso de memória de programa: 482 bytes 47,1% de
dados completos Uso de memória de dados: 5 bytes 7,8% de dados completos

Nada mal, temos ainda mais da metade da memória do microcontrolador em estoque!
Verificando Proteus:

A simulação está OK!


Conclusão: a implementação acabou sendo bastante utilizável, os dados são transmitidos e recebidos independentemente, a biblioteca delay.h não é usada, e mais da metade da memória do microcontrolador fica em reserva.

Eu incluo o código fonte da biblioteca, eles são compilados no avr-gcc: Fontes no GitHub

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


All Articles