Tutorial · Intermedio · 35 min
Temporizadores del 8051 y PWM por software para motores
El 8051 no tiene hardware de PWM: lo construyes con una interrupción del temporizador 0. Recarga automática, frecuencia-resolución y manejo de un puente H.
Introducción
Un Arduino fija la velocidad de un motor con analogWrite(pin, 180). Detrás de esa única llamada hay un periférico de hardware que genera una forma de onda continuamente, gratis, mientras la CPU hace otra cosa.
El 8051 no tiene tal periférico. Nada en el chip puede producir una forma de onda de ciclo de trabajo variable. Si quieres que un motor gire al 70 % tienes que generar tú mismo la forma de onda, desde una interrupción de temporizador, y lo pagarás en tiempo de CPU. Construir eso es lo más instructivo que puedes hacer en este chip, porque hace visible exactamente lo que analogWrite() hacía por ti.
Esto supone que ya conoces los puertos. Si la PWM es nueva para ti, el tutorial de PWM y puente H explica por qué conmutar un motor encendido y apagado rápidamente equivale a variar su tensión.
Los temporizadores que tienes
El 8051 clásico tiene dos temporizadores/contadores de 16 bits, el temporizador 0 y el temporizador 1; el AT89S52 añade un tercero. Cada uno cuenta ciclos de máquina —doce periodos del oscilador cada uno—, así que a 11,0592 MHz un temporizador avanza cada 1,085 µs.
En la práctica te queda uno libre. El temporizador 1 casi siempre está ocupado por el puerto serie, que deriva su velocidad en baudios de los desbordamientos del temporizador 1. Eso deja el temporizador 0 como tu temporizador de propósito general, y es sobre el que hay que construir la PWM.
Dos registros los configuran:
TMODelige el modo. El nibble bajo es el temporizador 0, el nibble alto el temporizador 1.TCONcontiene los bits de marcha (TR0,TR1) y las banderas de desbordamiento (TF0,TF1).
El temporizador 0 tiene cuatro modos, pero solo uno es adecuado para una señal periódica:
| Modo | nibble bajo de TMOD |
Qué hace |
|---|---|---|
| 0 | 0x00 |
Conteo de 13 bits — una reliquia de compatibilidad |
| 1 | 0x01 |
Conteo de 16 bits, recarga manual en cada desbordamiento |
| 2 | 0x02 |
Conteo de 8 bits con recarga automática |
| 3 | 0x03 |
Divide el temporizador 0 en dos temporizadores de 8 bits |
El modo 2 es el que quieres. Al desbordar, el hardware copia TH0 de vuelta a TL0 por sí solo, así que el periodo se mantiene exacto. El modo 1 hace que tu rutina de servicio de interrupción recargue la cuenta, y cada ciclo de máquina entre el desbordamiento y tu escritura es un ciclo de deriva que se acumula para siempre.
Fijar el tic
En el modo 2 el temporizador cuenta hacia arriba desde TH0 hasta 255 y desborda, así que el periodo es la distancia que debe recorrer:
ciclos por tic = 256 − TH0
periodo del tic = (256 − TH0) × 12 / f_osc
TH0 = 156, f_osc = 11,0592 MHz
ciclos = 100
tic = 100 × 12 / 11 059 200 = 108,5 µs
Activarlo lleva cinco líneas:
#include <reg52.h>
TMOD = (TMOD & 0xF0) | 0x02; // Temporizador 0 -> modo 2, deja el temporizador 1 en paz
TH0 = 156; // valor de recarga automática: 100 ciclos de máquina
TL0 = 156; // la primera cuenta parte del mismo sitio
ET0 = 1; // habilita la interrupción del temporizador 0
EA = 1; // habilitación global de interrupciones
TR0 = 1; // empieza a contar
Ese TMOD & 0xF0 importa. TMOD no es direccionable por bit, así que escribir TMOD = 0x02 destruye los bits de modo del temporizador 1 junto con ello —y si la UART está en marcha, acabas de romper tu salida serie mientras configurabas un motor.
PWM a partir de un contador
Con una interrupción periódica en la mano, la PWM es contabilidad. Cuenta tics hasta una longitud de periodo N, y mantén la salida en alto mientras la cuenta esté por debajo del valor de ciclo de trabajo:
#define PWM_LEVELS 20 // tics por periodo de PWM
sbit ENA = P2^0; // habilitación del puente H — el pin con PWM
volatile unsigned char tick = 0;
volatile unsigned char duty_a = 0; // 0 .. PWM_LEVELS
void timer0_isr(void) interrupt 1 {
if (++tick >= PWM_LEVELS) tick = 0;
ENA = (tick < duty_a); // en alto durante los primeros duty_a tics de cada periodo
}
interrupt 1 es el vector del temporizador 0 (dirección 0x000B). La bandera de desbordamiento TF0 la borra el hardware al vectorizar, así que a diferencia de las banderas que sondeas a mano, no hay nada que reiniciar.
Fijar una velocidad ahora es solo una variable:
duty_a = 14; // 14/20 = 70% de ciclo de trabajo
El compromiso del que no puedes escapar
Frecuencia de PWM, resolución de ciclo de trabajo y carga de CPU son una sola relación con tres nombres:
f_pwm = f_osc / (12 × C × N)
C = ciclos de máquina por tic (256 − TH0)
N = tics por periodo de PWM (también tu número de pasos de ciclo de trabajo)
Mantén C en 100 y varía N, y la resolución se compra a sí misma con frecuencia:
N |
Resolución del ciclo de trabajo | Frecuencia de PWM | Interrupciones/s |
|---|---|---|---|
| 10 | 10% | 921,6 Hz | 9 216 |
| 20 | 5% | 460,8 Hz | 9 216 |
| 50 | 2% | 184,3 Hz | 9 216 |
| 100 | 1% | 92,2 Hz | 9 216 |
La tasa de interrupción nunca cambia, porque solo depende de C. Lo que cambia es cuántos tics gastas antes de que el patrón se repita —y por debajo de unos 100 Hz un pequeño motor de CC deja de promediar los pulsos y empieza a recorrerlos de forma audible.
Así que da el siguiente paso obvio: mantén 460,8 Hz y consigue 1 % de resolución poniendo N = 100 y C = 20. La aritmética está de acuerdo —y el chip muere. Con C = 20 la interrupción se dispara cada 20 ciclos de máquina, mientras que la ISR de arriba necesita unos 25 solo para guardar registros, comparar y volver. Nunca termina antes de que llegue la siguiente, y nada más se ejecuta jamás.
Ese es el verdadero techo. Con una ISR que cuesta unos 25 ciclos de máquina, C = 100 gasta un cuarto de la CPU en PWM, y C = 50 gasta la mitad. La PWM por hardware en un Arduino cuesta cero, a cualquier frecuencia y resolución. Esta sola comparación vale más que cualquier tabla de características.
Dos cosas compran margen barato: cambia al banco de registros 1 dentro de la ISR (using 1) para que el compilador intercambie dos bits en lugar de apilar ocho registros, y mantén la ISR en contar y escribir pines. Todo lo que calcule pertenece a main.
Manejo de los motores
Un puente H toma dos entradas de dirección y una de habilitación por motor. Alimenta los pines de dirección desde pines ordinarios del puerto y la habilitación desde tu pin con PWM:
sbit IN1 = P2^1;
sbit IN2 = P2^2;
sbit ENA = P2^0; // con PWM desde la ISR
void motor_forward(unsigned char duty) {
IN1 = 1; IN2 = 0;
duty_a = duty;
}
void motor_reverse(unsigned char duty) {
IN1 = 0; IN2 = 1;
duty_a = duty;
}
void motor_stop(void) {
duty_a = 0;
IN1 = 0; IN2 = 0;
}
Un segundo motor es una segunda variable de ciclo de trabajo y una segunda línea de salida en la misma ISR —el contador de tics es compartido, así que ambos canales se mantienen en paso:
void timer0_isr(void) interrupt 1 using 1 {
if (++tick >= PWM_LEVELS) tick = 0;
ENA = (tick < duty_a);
ENB = (tick < duty_b);
}
Un detalle de cableado específico de este chip. Como se vio en el tutorial de GPIO, un pin del 8051 solo alimenta decenas de microamperios. Eso basta para una entrada CMOS, pero un L298N o L293D tiene entradas bipolares que pueden consumir hasta 100 µA en un alto lógico —justo al límite de lo que el pin puede suministrar. Un pull-up de 4,7 kΩ desde cada entrada del manejador a +5 V elimina la ambigüedad por unos céntimos, y convierte una placa que funciona en el banco pero no en el robot en una que simplemente funciona.
Cuando algo sale mal
- El motor zumba pero apenas gira. Tu frecuencia de PWM es demasiado baja, o el ciclo de trabajo está por debajo del umbral de fricción estática del motor. Sube la frecuencia por encima de ~100 Hz y espera que un motor real necesite 25–40 % antes de moverse.
- La salida serie se rompió al añadir el temporizador. Escribiste
TMODde golpe y borraste los bits de modo del temporizador 1. Enmascáralo:TMOD = (TMOD & 0xF0) | 0x02;. - El periodo de PWM deriva. Estás en el modo 1 y recargando a mano. Pasa al modo 2 y deja que el hardware lo haga.
- Todo el programa va lento. La ISR es demasiado cara para la tasa de tics. Sube
TH0hacia 200, añadeusing 1y saca cualquier aritmética de la interrupción. - Los cambios de ciclo de trabajo producen fallos en la salida.
mainestá escribiendoduty_amientras la ISR la lee. Una escritura de un solo byte es atómica en esta CPU así que unchares seguro, pero cualquier cosa más ancha necesita la interrupción desactivada durante la actualización. - Un motor te ignora por completo. Comprueba que el pin de habilitación sea realmente el que maneja la ISR, y que las entradas del manejador tengan sus pull-ups.
Explora el grafo
Forma parte de estos montajes
Proyectos y rutas de aprendizaje que incluyen este tutorial.
Lecturas adicionales