Logiciel UART duplex intégral pour ATtiny13

Bonjour à tous les utilisateurs de Geektimes! Une fois, à la suite de mon service, j'ai dû implémenter le logiciel UART sur le microcontrôleur ATtiny13 populaire. Google, j'ai trouvé un grand nombre d'articles sur ce sujet, beaucoup d'entre eux sont affichés ici:


Et il existe d'autres ressources:


La dernière implémentation, en général, répond à mes besoins (full duplex). Mais, d'une part, le code est écrit en CodeVision AVR, que je n'utilise pas pour des raisons purement religieuses, et d'autre part, les inserts d'assembleur mal commentés d'autant plus découragent le désir de comprendre le code. Je me suis fixé comme objectif d'écrire en C pur une bibliothèque UART full-duplex conviviale. Et en même temps, écrivez un article à ce sujet, car la tâche est assez intéressante en raison de la quantité très limitée de ressources de mémoire et de contrôleur (un seul temporisateur 8 bits). Pour les débutants en programmation de microcontrôleurs, ce sera un bon matériel de formation, comme J'ai moi-même en train d'écrire la bibliothèque, presque à partir de zéro j'ai maîtrisé l'architecture d'AVR.

Peu importe - bienvenue à kat, il y aura beaucoup de texte, beaucoup de codes source avec des commentaires dans le code.

Commençons donc. Je vais simplement publier le fichier d'en-tête uart13.h sous la forme dans laquelle il se trouve avec des commentaires dans le code, tout est simple là-bas.

Fichier d'en-tête 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_ */



Mais je vais diviser la description du code d'implémentation de la bibliothèque en parties afin de ne pas transformer l'article en un énorme spoiler avec du code.

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


Ici, nous voyons le code du gestionnaire d'interruption du minuteur par rapport au registre OCR0A. Il fonctionne en permanence et se produit chaque fois que la minuterie atteint la valeur enregistrée dans le registre OCR0A. Lorsque cela se produit, la valeur de temporisation dans le registre TCNT0 est automatiquement réinitialisée (mode de temporisation CTC, défini dans le registre TCCR0A). Cette interruption est utilisée pour envoyer des données via UART. La variable txbyte est utilisée comme registre à décalage: chaque fois qu'une interruption se produit, le bit le moins significatif de la variable txbyte est défini sur la sortie TXD du microcircuit, après quoi le contenu de la variable est décalé vers la droite et un est écrit sur le bit haut libéré.

Tant qu'il n'y a pas de données à transférer, le nombre 0xFFFF est stocké dans la variable et, ainsi, le niveau logique est maintenu en continu à la sortie TXD. Lorsque nous voulons transférer des données, nous devons écrire dans le compteur de bits le nombre de bits pour la transmission: 1 début, 8 bits de données et 1 arrêt, pour un total de 10 (0x0A), et écrire en txbyte les données pour la transmission avec le bit de début. Après cela, ils commenceront immédiatement à être transmis. La fonction void uart_send (uint8_t tb) est engagée dans la formation du paquet.

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


Ici, nous voyons le gestionnaire d'interruption du temporisateur par rapport au registre OCR0B. Il fonctionne de manière similaire à l'interruption TIM0_COMPA, mais, contrairement à elle, lorsque cette interruption est exécutée, le temporisateur TCNT0 ne se réinitialise pas. Cette interruption n'est autorisée que lorsque nous recevons des données, le reste du temps, elle est interdite. Lorsque cela se produit, nous vérifions l'état logique de l'entrée RXD et, s'il en est un, puis en écrivons un dans l'ordre supérieur de la variable de réception rxbyte, puis nous diminuons de un le compteur des bits reçus et, s'il devient nul, nous terminons la réception. Sinon, nous décalons la variable rxbyte vers la droite pour la préparer à la réception du bit suivant.

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


Interrompre INT0. Il fonctionne sur le front descendant de l'impulsion à l'entrée INT0 et est utilisé pour suivre le début de la réception des informations. Définit le compteur de bits sur 9, réinitialise le contenu de la variable rxbyte. Définit la valeur du registre OCR0B, qui détermine la fréquence de l'interruption TIM0_COMPB, elle doit être dans le temps au milieu du bit reçu. Après cela, l'interruption TIM0_COMPB est activée et l'interruption INT0 est désactivée.

Voici les fonctions utilisateur pour travailler avec UART.

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


Fonction de transfert d'octets UART. Il prend un octet pour la transmission comme argument; il n'y a pas de valeur de retour. Si un octet est transféré au moment de l'appel de la fonction, il attend que le transfert soit terminé, après quoi il écrit des octets de txbyte dans les 8 bits inférieurs de la variable pour la transmission, et les 8 bits supérieurs restent 0xFF, puis décale la variable vers la gauche, créant ainsi le bit de départ dans l'ordre le plus bas. Définit le compteur de bits sur 10.

Fonction 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 ( )
	}
}


Fonction de réception d'octets UART. Prend un argument vers un pointeur vers une variable 8 bits où l'octet reçu sera contenu. Renvoie l'octet reçu, si l'octet est reçu et s'il n'y a rien à recevoir, renvoie (-1). Si au moment où la fonction est appelée, la fonction attend qu'elle se termine. Si la fonction est appelée deux fois, la première fois, elle renverra l'octet reçu et la deuxième fois (-1).

Fonction 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();				//   
}


Fonction d'initialisation UART. Aucun argument, aucune valeur de retour. Initialise les variables globales et les registres du microcontrôleur. D'après les commentaires dans le code, tout devrait être clair.

Il est donc temps d'écrire un simple main () en utilisant notre bibliothèque et de voir ce qui s'est passé en termes de quantité de code et de vérifier les performances.

principal 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);
}


Nous compilons:
Utilisation de la mémoire du programme: 482 octets 47,1% pleine
Utilisation de la mémoire des données: 5 octets 7,8% pleine

Pas mal, nous avons encore plus de la moitié de la mémoire du microcontrôleur en stock!
Enregistrement dans Proteus:

La simulation est OK!


Conclusion: l' implémentation s'est avérée tout à fait utilisable, les données sont transmises et reçues indépendamment, la bibliothèque delay.h n'est pas du tout utilisée et plus de la moitié de la mémoire du microcontrôleur est laissée en réserve.

Je joins le code source de la bibliothèque, ils sont compilés en avr-gcc: Sources sur GitHub

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


All Articles