Estoy tratando de implementar PWM implementado por software para controlar 4 ESC usando un microcontrolador atmega 16.
Para lograr eso, estoy generando secuencialmente los pulsos para cada ESC uno por uno en cada período de la señal.
Aquí está el código -
#define TOTAL_ESC 4
int ESC_pulse[TOTAL_ESC];
int ESC_pins[TOTAL_ESC];
int currentESC;
int main(void)
{
initESCs();
initUSART();
sei();
while (1)
{
}
}
void initESCs()
{
ESC_pulse[0] = ESC_pulse[1] = ESC_pulse[2] = ESC_pulse[3] = 2000;
ESC_pins[0] = PIND4;
ESC_pins[1] = PIND5;
ESC_pins[2] = PIND6;
ESC_pins[3] = PIND7;
currentESC = TOTAL_ESC - 1;
ICR1 = 39999; //50Hz signal @ 16MHz clock
OCR1A = 1000;
DDRD |= 1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3];
PORTD &= ~(1<<ESC_pins[0] | 1<<ESC_pins[1] | 1<<ESC_pins[2] | 1<<ESC_pins[3]);
TIMSK |= 1<<OCIE1A;
TCCR1A |= 1<<WGM11;
TCCR1B |= 1<<WGM13 | 1<<WGM12 | 1<<CS11;
}
ISR(TIMER1_COMPA_vect)
{
PORTD &= ~(1<<ESC_pins[currentESC]); //End prev ESC pulse
if(currentESC == TOTAL_ESC - 1 && OCR1A != 1000)
{
OCR1A = 1000;
return;
}
currentESC = (currentESC + 1) % TOTAL_ESC;
PORTD |= 1<<ESC_pins[currentESC]; //Start next ESC pulse
OCR1A += ESC_pulse[currentESC];
}
Entonces, estoy tratando de generar señales de 50Hz con pulsos de 1ms - 2ms. El reloj de mi CPU es de 16 MHz y el reloj del temporizador está preescalado a 2 MHz.
Tengo una matriz ESC_pulse
para almacenar el ancho de los pulsos para cada ESC. Los valores en él variarán de 2000 a 4000 para los pulsos de 1 ms a 2 ms que requieren los ESC. La lógica que estoy aplicando es que cada vez que ocurre la interrupción de comparación del temporizador, borro el último pin de salida del ESC y configuro el siguiente y actualizo el valor OCR1A
con OCR1A
el valor actual + el ancho de pulso del ESC actual como está almacenado en ESC_pulse
.
Con el PWM generado por hardware usando el temporizador, mi ESC puede hacer funcionar el motor de CC sin escobillas. Sin embargo, la técnica generada por software no funciona. No tengo ningún equipo para ver realmente cuáles son las señales PWM generadas. Todo lo que sucede es que el ESC genera pitidos que significan que no hay señal.
No estoy seguro de lo que estoy haciendo mal aquí.
Su programa falla porque OCR1A tiene doble búfer en todos los modos de temporizador PWM y está utilizando dicho modo (Fast PWM, TOP = ICR1). Cuando escribe un nuevo valor en OCR1A, en realidad no cambia el valor utilizado por el hardware del temporizador. En cambio, el valor almacenado en OCR1A se copia en el "registro de sombra" separado que realmente usa el temporizador solo una vez que el valor del contador llega a TOP y se reinicia desde cero nuevamente. Esto es muy útil para generar PWM de hardware sin fallas, pero evita lo que está tratando de hacer (múltiples actualizaciones de OCR1A por ciclo de temporizador).
Dado que esta actualización de OCR1A ocurre solo una vez por ciclo de temporizador (a 50 Hz) y se supone que su código de interrupción genera 4 1000 μs - 2000 μs retrasos + un retraso largo, termina con un período PWM de 100 ms (5 ciclos de temporizador) y un tiempo alto de ~20 ms.
La solución es configurar el temporizador en un modo no PWM. El modo que mejor se adapta a su programa es el temporizador claro en la coincidencia de comparación (CTC, TOP = ICR1) que funciona casi de manera idéntica pero no duplica el búfer de OCR1A y OCR1B. Los bits WGM que se encuentran en TCCR1A y TCCR1B deben configurarse en 1100 para lograr esto (consulte la hoja de datos para obtener más detalles).
Si accede a una variable en una rutina de servicio de interrupción, debe declarar la variable volatile
. Omitir la palabra clave (entre otras cosas) permite que el compilador realice optimizaciones que asumen que solo el flujo del programa principal puede modificar el estado.
PIND4
, PIND5
y tal como los utiliza su código no se encuentran en las definiciones de registro proporcionadas por AVR-GCC PIND6
. PIND7
Las macros más genéricas PINx
se PORTx
declaran DDRx
en "portpins.h".
La lógica de secuenciación podría escribirse de manera más clara. Su código actual tiene 4 estados explícitos en la secuencia (uno para cada pin de salida), de los cuales el último se usa para dos coincidencias de comparación: la primera para establecer la salida del canal 4 en baja y la segunda para esperar hasta que la secuencia se reinicie. Esto se hace extrañamente usando un valor OCR1A específico (1000) como bandera. Esto me tuvo rascándome la cabeza por un tiempo.
Esto podría estar basado en opiniones, pero usaría uint8_t
, int16_t
y amigos en lugar de, por ejemplo unsigned char
, o short
en software integrado. De esta manera, usted y los demás sabrán exactamente qué tan grandes son sus variables, y también son menos detalladas.
Su ejemplo de código debe incluir <avr/io.h>
y <avr/interrupt.h>
, y debe proporcionar una declaración de función para void initESCs()
. initUSART()
es superfluo
Si entiendo su código correctamente, tiene Timer1 en modo Fast PWM usando OCR1A para medir el tiempo de trabajo de PWM e ICR1 para el período. Cuando OCR1A coincide con el valor actual del temporizador, activa una interrupción. Luego lo vuelve a cargar con un tiempo más largo, la idea es que coincida con el temporizador 1 ~ 2 ms más tarde para el próximo pulso de servo.
El problema con esta técnica es que en los modos PWM, el registro de comparación de salida tiene doble búfer y se sincroniza con el período PWM, por lo que escribirlo durante el ciclo PWM actual solo tendrá efecto en el próximo ciclo . Esto se describe en la hoja de datos en la página 98:-
El registro OCR1x tiene doble búfer cuando se usa cualquiera de los doce modos de modulación de ancho de pulso (PWM)... El doble búfer sincroniza la actualización del registro de comparación OCR1x con la parte SUPERIOR o INFERIOR de la secuencia de conteo. La sincronización evita la aparición de pulsos PWM no simétricos y de longitud impar
Entonces, en lugar de obtener una interrupción 1 ~ 2 ms después de escribir en OCR1A, la obtiene 20 ms + 1 ~ 2 ms después.
No estoy seguro de si es posible hacerlo a su manera usando un modo de temporizador que no sea PWM, pero podría ser más fácil usar un temporizador básico para cronometrar cada pulso por separado, luego sume todos los tiempos y reste de 20 ms para obtener el tiempo de pausa final.
Muchos ESC modernos pueden manejar frecuencias de 250 Hz o más, por lo que incluso puede salirse con la suya simplemente empujando los pulsos lo más rápido posible uno tras otro.
TCCR1A |= 1<<WGM11;
Lo que NO veo en su código es una cola de ningún tipo. Y necesitas uno, creo.
Así es como abordaría esto como una solución de diseño de software:
eso. Todo el proceso. La cola delta está configurada para contener un valor de 'marca', que es el conteo en microsegundos para que ocurra el próximo evento. Al insertar una entrada en la cola, resta todos los 'ticks' (valor delta) de las entradas anteriores antes de insertarla. Así que permítanme demostrar con un ejemplo para dejar esto claro.
Supongamos que el valor de tiempo para son: 1573, 2000, 1206 y 1573. Entonces la cola se vería así (sin incluir el pin asociado, que también se requiere aquí):
Eso está listado en orden de cola. Sin embargo, crearía una matriz de cinco elementos con índices del 0 al 4, con [0] asignado como la cola puntero. Entonces, a la cola anterior le gustaría lo siguiente en orden de matriz:
Cuando establece la inserción de los cuatro en la cola (que está vacía cuando comienza, ya que todos tienen en este momento todo caducado), esa es la cola resultante cuando sale y regresa de su evento de interrupción. Pero justo antes de salir, como se mencionó, cargará el primer valor OCR1B en la cola (1206 aquí) y lo colocará en OCR1B.
Cuando se activa el evento OCR1B, la primera entrada en la cola le dice al código qué pin debe conducir a BAJO. Luego, la entrada se elimina de la cola. Para el primer evento OCR1B, esto elimina la entrada 1206, dejando el valor delta de 367, que no es cero. Entonces, el valor OCR1B de 1573 ahora se carga en OCR1B.
Cuando ocurra el próximo evento, el evento OCR1B ahora también impulsará ese pin asociado a BAJO y eliminará esa entrada de la cola. Ahora, la siguiente entrada tiene un valor delta de 0. Debido a eso, DEBE significar que hay otro pin para conducir BAJO, por lo que el evento OCR1B continúa y también conduce ese pin BAJO y elimina esa entrada de la cola. En este punto, solo queda una entrada en la cola, que tiene el valor delta 427, que también es distinto de cero. Entonces, el código ahora carga el valor OCR1B de 2000 en el registro de comparación, OCR1B, y sale.
El último evento OCR1B ahora se activa y el código también activa ese pin BAJO y elimina la entrada. No hay más entradas. Entonces el código simplemente sale. Todo está hecho y lo único que queda es esperar a que vuelva a ocurrir el evento OCR1A.
Ese es el proceso a seguir, creo.
Aquí hay un ejemplo de cómo podría configurar las colas para esto, sobre todo ilustrando cómo insertar en la cola delta. La función qinsert() hace este trabajo. Tendría llame a qinsert() para cada uno de los cuatro PWM, como parte de su trabajo.
uint8_t qp[5]; /* prior in queue reference */
uint8_t qn[5]; /* next in queue reference */
uint16_t qk[5]; /* delta value */
uint16_t qv[5]; /* OCR1B value */
uint8_t qpin[5]; /* pin position 0..7 */
void qinit( void ) {
qn[0]= qp[0]= 0;
qk[0]= 0xFFFF;
return;
}
uint8_t qinsert( uint8_t node, uint16_t key ) {
uint8_t prv= 0, nxt;
uint16_t nxtkey;
qv[node]= key; /* optional, depending on overall design */
for ( nxt= qn[prv]; (nxtkey= qk[nxt]) < key; prv= nxt, nxt= qn[nxt] )
key -= nxtkey;
if ( nxt != 0 )
qk[nxt]= nxtkey - key;
qk[node]= key;
qp[node]= prv;
qn[node]= nxt;
qp[nxt]= qn[prv]= node;
return node;
}
uint8_t qunlink( uint8_t node ) {
uint8_t prv= qp[node], nxt= qn[node];
qn[prv]= nxt;
qp[nxt]= prv;
return node;
}
chris stratton
jimmyb
chris stratton