Software PWM de 4 canales usando Atmega16 para controlar 4 ESC para motores de CC sin escobillas

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_pulsepara 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 OCR1Acon OCR1Ael 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í.

En este rango de tiempo, con un poco de ingenio para acoplar, probablemente pueda usar una tarjeta de sonido como un osciloscopio. La señal se distorsionará debido al acoplamiento de CA, pero debería poder calcular el tiempo.
¿Cuántos pasos posibles necesita entre 1 ms y 2 ms, es decir, qué tan fino debe ser su control?
Mucha gente que responde a esta pregunta parece no saber que en una aplicación RC tradicional, los canales se actualizan uno tras otro en forma rotativa, no todos a la vez. Es posible actualizarlos todos a la vez, pero no es necesario . Además, es probable que se requiera continuar actualizándolos incluso cuando no hayan cambiado, ya que se puede construir un ESC para apagarse como medida de seguridad si no recibe actualizaciones periódicas.

Respuestas (3)

La causa del problema: el doble almacenamiento en búfer OCR1A

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).

Otros problemas menores:

  • 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, PIND5y tal como los utiliza su código no se encuentran en las definiciones de registro proporcionadas por AVR-GCC PIND6. PIND7Las macros más genéricas PINxse PORTxdeclaran DDRxen "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_ty amigos en lugar de, por ejemplo unsigned char, o shorten 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

Esta es una gran respuesta, ya que tomó la idea esencialmente sólida que se intentó en la pregunta, explicó por qué no funciona como se esperaba y propuso cómo solucionarlo.
Gracias. Todavía no he probado la solución. Pero, ahora sé por qué no funcionó. ¡Debería haber leído la hoja de datos correctamente! Y en realidad no puse todo el código. Se saltó la parte de inclusión y el código relacionado con USART. Gracias de nuevo @jms.
El valor OCR1A es en realidad irrelevante. Podría haberlo mantenido en 0 en lugar de 1000. Fue solo un intento desesperado de descubrir qué estaba fallando en el código.

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.

Así que la solución podría ser simplemente omitir esoTCCR1A |= 1<<WGM11;
Sí. Eso pondrá el temporizador en modo CTC, que no hace doble búfer de OCR1A.
  • Lo primero que debe hacer es obtener CUALQUIER tipo de alcance. Puedes comprar un DSO138. Compré y ensamblé varios de estos para regalarlos a mis amigos. Son baratos y pueden funcionar para sus necesidades. (Muestra en 1 megahercio y solo admite sobre 200 kHz , pero eso puede estar bien para lo que tiene por delante). O puede obtener algo un poco más elegante que se conecte a una PC, como un Hantek 6022BE. O simplemente puede comprar un osciloscopio usado barato. Hay varios sitios para buscar buenos precios, muchos de los cuales estoy seguro que conoces. Pero el punto aquí es que si estás trabajando en un código para hacer algo importante para ti como este, realmente NECESITAS tener algo que te permita observar y medir lo que estás produciendo. Y tus necesidades son pedestres, así que no te cuesta un ojo de la cara. Probablemente menos que el costo de un controlador ESC. Vale la pena hacer.
  • El ATmega16 tiene cuatro PWM. Un PWM en cada uno de los dos temporizadores de 8 bits y solo dos PWM más en el Timer1 de 16 bits. Entonces entiendo por qué está tratando de hacer esto en el software, usando Timer1. No hay otra manera de ir.

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:

  1. Decidir sobre una resolución específica para mis cuatro salidas PWM. No soy un experto en controladores ESC y no sé qué resolución usan al interpretar el ancho de pulso que reciben. Pero creo que puedo inferir de su uso de 1000 y 2000 para sugerir que desea precisión hasta el microsegundo. Así que, en resumen, puede establecer 1.001 EM y establecer 1.583 EM , pero no puede establecer 1.6057 EM .
  2. Una vez que conozca la resolución requerida, puede configurar Timer1 para proporcionar esa resolución. Esto solo configura la velocidad del contador del temporizador. (No causa ninguna interrupción, por sí mismo. Eso sucede cuando configura la captura/comparación). En este caso, asumiré que ha configurado el Temporizador1 para contar a una velocidad de 1 megahercio . (No, no voy a investigar la hoja de datos del ATmega16 y tratar de averiguar lo que realmente hizo en su código anterior. En su lugar, le diré lo que debe hacer).
  3. Ahora que la resolución está determinada y el Timer1 está configurado correctamente para proporcionar una 1 megahercio tasa de contador, configura lo que se llama una 'cola delta'. Puede leer sobre ellos en el libro de Douglas Comer (el inventor) sobre XINU, fechado a mediados de la década de 1980, si está interesado en una fuente de la idea. Necesitará cuatro entradas en la cola. Uno para cada uno de sus pines de salida PWM, individualmente.
  4. Llamemos al 20 EM hilo de intervalo, PAG 0 . Los otros cuatro serán PAG 1 PAG 4 .
  5. Tiene dos registros de captura/comparación disponibles. Probablemente sería más conveniente usar ambos. Uno de ellos es asignado a PAG 0 por su habitual 20 EM evento. Asignemos OCR1A a PAG 0 , así que establezca ese valor en 20000 y configúrelo para la recarga automática, si es posible. (Si no, hágalo en el software.) Esto configurará el básico 20 EM momento.
  6. PAG 0 hará lo siguiente: (a) Poner todas las salidas PWM en ALTO. (b) Inserte cada uno de PAG 1 PAG 4 en la cola, que por ahora siempre está vacía, en función de sus valores de período. (c) Borre el contador del temporizador. (d) Cargue OCR1B con el valor del temporizador de la primera entrada en la cola delta y habilite el evento de interrupción OCR1B. (f) Salida. (Es posible que deba hacer algo para habilitar el próximo evento de interrupción. Pero eso depende de usted).
  7. En el evento OCR1B, conduce el pin asociado BAJO. Elimina la entrada actual de la cola. Mientras haya entradas restantes en la cola y mientras los 'ticks' restantes (valor delta) sean exactamente cero, coloque el pin asociado en BAJO y elimine la entrada de la cola. Ahora, tiene garantizado uno de estos dos casos: (a) no quedan entradas en la cola; en este caso, simplemente regrese, ya que no hay nada más que hacer. (b) quedan entradas en la cola y la superior tiene un valor de temporizador distinto de cero: cargue este valor en OCR1B y asegúrese de que pueda volver a interrumpir, luego simplemente regrese.

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 PAG 1 PAG 4 son: 1573, 2000, 1206 y 1573. Entonces la cola se vería así (sin incluir el pin asociado, que también se requiere aquí):

d mi yo t a O C R 1 B T h r mi a d 1 1206 1206 PAG 3 2 367 1573 PAG 1 3 0 1573 PAG 4 4 427 2000 PAG 2

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 h mi a d puntero. Entonces, a la cola anterior le gustaría lo siguiente en orden de matriz:

norte mi X t d mi yo t a O C R 1 B 0 3 norte / a norte / a 1 4 367 1573 2 0 427 2000 3 1 1206 1206 4 2 0 1573

Cuando PAG 0 establece la inserción de los cuatro en la cola (que está vacía cuando PAG 0 comienza, ya que todos PAG 1 pag 4 tienen en este momento todo caducado), esa es la cola resultante cuando PAG 0 sale y regresa de su evento de interrupción. Pero justo antes de salir, como se mencionó, PAG 0 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 PAG 0 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;
}
Para analizar señales digitales es preferible un analizador lógico a un osciloscopio.
@JimmyB Lo sé. También tengo un MSO. Pero son caros. A veces, los mendigos no pueden elegir.
La resolución de microsegundos puede ser un poco exagerada. Probablemente se saldrá con la suya con mucho menos, tal vez incluso con solo 32 pasos. Eso nos daría algo de espacio para el software PWM (500 relojes de CPU por incremento).
@JimmyB No tengo experiencia con ESC, así que no lo sabría. Pero usó esos valores de temporizador, así que ahí es donde comencé. Dicho esto, sí, sería bueno si pudiera salirse con la suya con una frecuencia de reloj primaria más lenta. Nada más cambia en la metodología que mencioné. Solo el temporizador y los valores utilizados en las matrices. Aparte de eso, es la misma idea independientemente. Solo detalles sin sentido.
De hecho, un analizador lógico basado en USB de velocidad moderada es mucho más económico que un osciloscopio. Más importante aún, afirmar que no hay cola es efectivamente falso, porque hay algo mucho más apropiado. El ISR está claramente escrito para recorrer los cuatro canales, con un pulso para actualizar cada uno a su vez. Puede haber problemas en los detalles de implementación, pero la idea es correcta. Su propuesta es muy complicada y ni siquiera está claro que necesariamente hará lo que se requiere, que es actualizar cada canal a un ritmo bastante regular.
@ChrisStratton No de una manera que funcione.
A continuación, arregle lo que realmente no funciona, no lo reemplace por completo con algo mucho menos apropiado para la aplicación.
@ChrisStratton Lo que escribí es apropiado y fácil de implementar. También es flexible para cambios posteriores. Pero no voy a discutir. Solo esperaré tu respuesta aquí.
No, lo que escribiste es una mala idea que realmente no se ajusta a la aplicación. Una cola es apropiada cuando no sabes lo que vas a tener que hacer; aquí (debe) saber exactamente lo que necesita hacer, que es proporcionar resultados regulares, ya sea actualizados o sin cambios. Por lo tanto, toda la estructura de la lista es una pérdida de tiempo de CPU, recursos de memoria y agrega modos de falla innecesarios. Mala ingeniería, simple y llanamente.
@ChrisStratton Entonces no estamos de acuerdo y debemos dejarlo así, a menos que desee tomar esto para conversar por algún motivo.
"Lo primero que debe hacer es obtener CUALQUIER tipo de alcance". - no esencial. El 95% de las veces encuentro que el simulador es suficiente. Simplemente siga el código y anote el número de ciclos en cada punto de interés. ¡No se requiere hardware!
@BruceAbbott Es un gran nivel de comodidad ver que está produciendo exactamente lo que su teoría dice que se debe producir. Considero que la comodidad es importante. También ve cosas que no verá en la simulación, de vez en cuando. (He encontrado errores de silicio de esa manera, varias veces). Supongo que creo que es 'esencial'.
Bueno, comencé con OTP PIC, para lo cual el simulador era esencial (¡la depuración en el hardware real se vuelve costosa cuando cada cambio de firmware necesita un nuevo chip!). Y no tengo un ATmega16, así que...
@BruceAbbott Cuando comencé con OTP PIC, que eran las partes PIC16C54 a PIC16C57 en 1988 o 1989, creo que el simulador no existía. Y fueron instrucciones, solo por un tiempo en MPLAB, cuando lo tenían. Pasó algún tiempo antes de que comenzaran a simular periféricos. Así que usé el ICE2000 hace muchos años (que todavía tengo) y las vainas apropiadas (bond-outs). Caro, sí. (Tenga el REAL ICE más nuevo, ahora). De acuerdo con el ATmega16: no usaré Atmel para proyectos que no sean pasatiempos por otras razones que no son apropiadas aquí, supongo.