Software UART full duplex para ATtiny13

¡Hola a todos los usuarios de Geektimes! Una vez, como resultado de mi servicio, necesitaba implementar el software UART en el popular microcontrolador ATtiny13. En Google, encontré una gran cantidad de artículos sobre este tema, muchos de ellos se publican aquí:


Y hay otros recursos:


La última implementación, en general, satisface mis necesidades (full duplex). Pero, en primer lugar, el código está escrito en CodeVision AVR, que no utilizo por razones puramente religiosas, y en segundo lugar, los comentarios mal inseridos en el ensamblador insertan aún más, desalientan el deseo de entender el código. Me propuse el objetivo de escribir en C puro una biblioteca UART full-duplex fácil de usar. Y al mismo tiempo, escriba un artículo sobre esto, porque la tarea es bastante interesante debido a la cantidad muy limitada de recursos de memoria y controlador (solo un temporizador de 8 bits). Para principiantes en la programación de microcontroladores, este será un buen material de capacitación, ya que Yo mismo en el proceso de escribir la biblioteca, casi desde cero dominé la arquitectura de AVR.

A quién le importa: bienvenido a Kat, habrá mucho texto, muchos códigos fuente con comentarios en el código.

Vamos a empezar. Simplemente publicaré el archivo de encabezado uart13.h en la forma en que está con comentarios en el código, todo es simple allí.

Archivo de encabezado 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_ */



Pero dividiré la descripción del código de implementación de la biblioteca en partes para no convertir el artículo en un gran spoiler con código.

Interrumpir 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--;			//     .
	}
}


Aquí vemos el código del controlador de interrupción del temporizador en comparación con el registro OCR0A. Funciona constantemente y ocurre cada vez que el temporizador alcanza el valor registrado en el registro OCR0A. Cuando esto sucede, el valor del temporizador en el registro TCNT0 se restablece automáticamente (modo de temporizador CTC, configurado en el registro TCCR0A). Esta interrupción se utiliza para enviar datos a través de UART. La variable txbyte se usa como un registro de desplazamiento: cada vez que ocurre una interrupción, el bit menos significativo de la variable txbyte se establece en la salida TXD del microcircuito, después de lo cual el contenido de la variable se desplaza hacia la derecha, y uno se escribe en el bit alto liberado.

Mientras no haya datos para transferir, el número 0xFFFF se almacena en la variable y, por lo tanto, se mantiene continuamente un alto nivel lógico en la salida TXD. Cuando queremos transferir datos, debemos escribir en el contador de bits el número de bits para la transmisión: 1 inicio, 8 bits de datos y 1 parada, para un total de 10 (0x0A), y escribir en txbyte los datos para la transmisión junto con el bit de inicio. Después de eso, comenzarán a transmitirse inmediatamente. La función void uart_send (uint8_t tb) se dedica a la formación del paquete.

Interrumpir 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
	}
}


Aquí vemos el controlador de interrupción del temporizador en comparación con el registro OCR0B. Funciona de manera similar a la interrupción TIM0_COMPA, pero, a diferencia de esto, cuando se ejecuta esta interrupción, el temporizador TCNT0 no se reinicia. Esta interrupción está permitida solo cuando recibimos datos, el resto del tiempo está prohibido. Cuando sucede, verificamos el estado lógico de la entrada RXD y, si es uno, luego escribimos uno en el orden superior de la variable de recepción rxbyte, luego disminuimos en uno el contador de los bits recibidos y, si se convierte en cero, finalizamos la recepción. De lo contrario, desplazamos la variable rxbyte a la derecha para prepararla para recibir el siguiente bit.

Interrumpir 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
}


Interrumpir INT0. Funciona en el borde posterior del pulso en la entrada INT0, se utiliza para rastrear el comienzo de la recepción de información. Establece el contador de bits en 9, restablece el contenido de la variable rxbyte. Establece el valor para el registro OCR0B, que determina la frecuencia de la interrupción TIM0_COMPB, debe estar a tiempo en el medio del bit recibido. Después de eso, la interrupción TIM0_COMPB está habilitada y la interrupción INT0 está deshabilitada.

Las siguientes son las funciones de usuario para trabajar con UART.

Función Uart_send
void uart_send(uint8_t tb)
{
	while(txbitcount);		//      
	txbyte = (tb + 0xFF00) << 0x01; //     txbyte        1
	txbitcount = 0x0A;		//     10
}


Función de transferencia de bytes UART. Se necesita un byte para la transmisión como argumento; no hay valor de retorno. Si se transfiere un byte al momento de llamar a la función, espera hasta que se complete la transferencia, después de lo cual escribe bytes de txbyte en los 8 bits inferiores de la variable para la transmisión, y los 8 bits superiores permanecen 0xFF, luego desplaza la variable hacia la izquierda, creando así el bit de inicio en el orden inferior. Establece el contador de bits en 10.

Función 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 ( )
	}
}


Función de recepción de bytes UART. Lleva un argumento a un puntero a una variable de 8 bits donde estará contenido el byte recibido. Devuelve el byte recibido, si se recibe el byte y si no hay nada que recibir, devuelve (-1). Si en este momento se llama a la función, la función está esperando que se complete. Si la función se llama dos veces, la primera devolverá el byte recibido y la segunda vez (-1).

Función 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();				//   
}


Función de inicialización UART. Sin argumentos, sin valor de retorno. Inicializa variables globales y registros de microcontroladores. De los comentarios en el código todo debe quedar claro.

Entonces, es hora de escribir algunos main () simples usando nuestra biblioteca y ver qué sucedió en términos de la cantidad de código y verificar el rendimiento.

C Principal
#include "uart13.h"

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


Compilamos:
Uso de la memoria del programa: 482 bytes 47.1% lleno
Uso de la memoria de datos: 5 bytes 7.8% lleno

¡No está mal, tenemos incluso más de la mitad de la memoria del microcontrolador en stock!
Comprobación en Proteus:

¡La simulación está bien!


En pocas palabras : la implementación resultó ser bastante utilizable, los datos se transmiten y se reciben de forma independiente, la biblioteca delay.h no se utiliza en absoluto y más de la mitad de la memoria del microcontrolador queda en reserva.

Adjunto el código fuente de la biblioteca, están compilados en avr-gcc: Fuentes en GitHub

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


All Articles