Biblioteca generadora de código ensamblador para microcontroladores AVR. Parte 1

Parte 2. Comenzando →


Biblioteca de generador de código ensamblador para microcontroladores AVR


Parte 1. Primer conocido


Buenas tardes, queridos Khabrovites. Quiero llamar su atención sobre el próximo proyecto (de los muchos disponibles) para programar los populares microcontroladores de la serie AVR.


Sería posible gastar una gran cantidad de texto para explicar por qué esto era necesario, pero en su lugar, solo mire ejemplos de cómo difiere de otras soluciones. Y todas las explicaciones y comparaciones con los sistemas de programación existentes estarán, según sea necesario, en el proceso de análisis de ejemplos. La biblioteca está ahora en proceso de finalización, por lo que la implementación de algunas funciones puede no parecer óptima. Además, se supone que algunas de las tareas que se asignan al programador en esta versión deben optimizarse o automatizarse aún más.


Entonces comencemos. Quiero aclarar de inmediato que el material presentado no debe considerarse en modo alguno como una descripción completa, sino solo como una demostración de algunas de las características de la biblioteca desarrollada para ayudar a comprender cuán interesante puede ser este enfoque para los lectores.


No nos desviaremos de la práctica establecida y comenzaremos con un ejemplo clásico, una especie de "Hola mundo" para los microcontroladores. Es decir, parpadeamos el LED conectado a una de las patas del procesador. Abramos VisualStudio de Microsoft (cualquier versión servirá) y creemos una aplicación de consola para C #. Para aquellos que no están al tanto, Community Edition, suficiente para el trabajo, es absolutamente gratis.


En realidad, el texto en sí es el siguiente:


Código fuente Ejemplo 1
using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].Toggle();}); Console.WriteLine(AVRASM.Text(m)); } } } 

Por supuesto, para que todo funcione y necesites la biblioteca que yo represento.
Después de compilar y ejecutar el programa, en la salida de la consola veremos el siguiente resultado de este programa.


Resultado de la compilación del ejemplo 1
 #include “common.inc” RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 L0000: in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL xjmp L0000 .DSEG 

Si copia el resultado a cualquier entorno que pueda funcionar con el ensamblador AVR y conecte la biblioteca de macros Common.inc (la biblioteca de macros también es uno de los elementos constitutivos del sistema de programación presentado y funciona junto con NanoRTOSLib ), entonces este programa puede compilarse y verificarse en un emulador o un chip real y Asegúrate de que todo funcione.


Considere el código fuente del programa con más detalle. En primer lugar, asignamos a la variable m el tipo de cristal utilizado. A continuación, configure el modo de salida digital para el bit cero del puerto B del cristal y active el puerto. La siguiente línea parece un poco extraña, pero su significado es bastante simple. En él, decimos que queremos organizar un bucle infinito, en cuyo cuerpo cambiamos el valor del bit cero del puerto B al opuesto. La última línea del programa realmente visualiza el resultado de todo lo escrito previamente en forma de código ensamblador. Todo es extremadamente simple y compacto. Y el resultado prácticamente no es diferente de lo que se podría escribir en ensamblador. Solo puede haber dos preguntas para el código de salida: la primera: ¿por qué inicializar la pila si aún no la usamos, y qué tipo de xjmp ? La respuesta a la primera pregunta y, al mismo tiempo, una explicación de por qué se genera el ensamblador, en lugar de un HEX preparado, será la siguiente: el resultado en forma de ensamblador le permite analizar y optimizar aún más el programa, permitiendo al programador seleccionar y modificar fragmentos de código que no le gustan. Y la inicialización de la pila se dejó al menos por las razones por las que sin usar la pila no se pueden encontrar muchos programas. Sin embargo, si no le gusta, no dude en limpiarlo. La salida al ensamblador es para este propósito. En cuanto a xjmp , este es un ejemplo del uso de macros para aumentar la legibilidad del ensamblador de salida. Específicamente, xjmp es un reemplazo para jmp y rjmp con la sustitución correcta dependiendo de la duración de la transición.


Si llena el programa con un chip, por supuesto, no veremos el parpadeo del diodo, a pesar de que el estado del pin cambia. Simplemente sucede demasiado rápido para poder verlo a través de los ojos. Por lo tanto, consideramos el siguiente programa, en el que seguimos parpadeando con un diodo, pero para que se pueda ver. Por ejemplo, un retraso de 0,5 segundos es bastante adecuado: ni demasiado rápido ni demasiado lento. Sería posible hacer muchos bucles anidados con NOP para formar un retraso, pero omitiremos este paso ya que no agregaremos nada a la descripción de las capacidades de la biblioteca e inmediatamente aprovecharemos la oportunidad de usar el hardware disponible. Cambiamos nuestra aplicación de la siguiente manera.


Código fuente Ejemplo 2
 using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.WDT.Clock = eWDTClock.WDT500ms; m.WDT.OnTimeout = () => m.PortB[0].Toggle(); m.WDT.Activate(); m.EnableInterrupt(); var loop = AVRASM.newLabel(); m.GO(loop); Console.WriteLine(AVRASM.Text(m)); } } } 

Obviamente, el programa es similar al anterior, por lo que solo consideraremos lo que ha cambiado. Primero, en este ejemplo, utilizamos WDT (temporizador de vigilancia). Para trabajar con grandes retrasos que no requieren una precisión extrema, esta es la mejor opción. Todo lo que se necesita para usarlo es establecer la frecuencia requerida configurando el divisor a través de la propiedad WDT.Clock y determinar las acciones que deben realizarse en el momento en que se desencadena el evento, definiendo el código a través de la propiedad WDT.OnTimeout. Como necesitamos interrupciones para trabajar, deben habilitarse con el comando EnableInterrupt. Pero el ciclo principal puede ser reemplazado por un maniquí. En él, todavía no planeamos hacer nada. Por lo tanto, declararemos y estableceremos una etiqueta y haremos una transición incondicional para organizar un ciclo vacío. Si te gusta LOOP más, por favor. El resultado de esto no cambiará.
Bueno, en la final, veamos el código resultante.


Resultado de la compilación del ejemplo 2
 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop jmp WDT ;Watchdog Timer Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 ldi TempL, (1<<WDCE) | (1<<WDE) sts WDTCSR,TempL ldi TempL, 0x42 sts WDTCSR,TempL sei L0000: xjmp L0000 WDT: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG 

Aquellos que estén familiarizados con este procesador sin duda tendrán una pregunta sobre dónde se han ido varios vectores de interrupción más. Aquí usamos la siguiente lógica, si el código no se usa, el código no es necesario. Por lo tanto, la tabla de interrupción termina en el último vector utilizado.
A pesar del hecho de que el programa hace frente a la tarea perfectamente, al más exigente puede no gustarle el hecho de que el conjunto de posibles retrasos es limitado y el paso es demasiado difícil. Por lo tanto, consideraremos otra forma y, al mismo tiempo, veremos cómo se organiza el trabajo con temporizadores en la biblioteca. En el cristal Mega328, que se toma como muestra, hay hasta 3 de ellos. 2 de 8 bits y uno de 16 bits. Los arquitectos se esforzaron mucho por invertir la mayor cantidad de características posibles en estos temporizadores, por lo tanto, su entorno es bastante voluminoso.


Primero, calculamos qué contador debe usarse para nuestro retraso de 0.5 segundos. Si tomamos la frecuencia del reloj de cristal de 16 MHz, incluso con el divisor periférico máximo es imposible mantenerlo dentro del contador de 8 bits. Por lo tanto, no complicaremos y utilizaremos el único contador Timer1 de 16 bits disponible para nosotros.


Como resultado, el programa toma la siguiente forma:


Código fuente Ejemplo 3
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) {var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; m.PortB.Activate(); m.Timer1.Mode = eWaveFormMode.CTC_OCRA; m.Timer1.Clock = eTimerClockSource.CLK256; m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); m.Timer1.OnCompareA = () => bit1.Toggle(); m.Timer1.Activate(); m.EnableInterrupt(); m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); Console.WriteLine(AVRASM.Text(m)); } } } 

Como utilizamos el generador principal como fuente de reloj para nuestro temporizador, para el cálculo correcto del retraso, debe especificar la frecuencia del reloj del procesador, la configuración del divisor y el fusible del reloj periférico. El texto principal del programa está configurando el temporizador en el modo deseado. Aquí, se elige deliberadamente un deliberador de 256 y no un máximo para la sincronización, porque cuando selecciona un divisor de 1024 para la frecuencia de reloj requerida de 500 ms, que queremos obtener, se obtiene un número fraccionario.


El código de ensamblador resultante de nuestro programa se verá así:


Resultado de la compilación del ejemplo 3
 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop reti ;Watchdog Timer Handler nop reti ;Timer2 Compare A Handler nop reti ;Timer2 Compare B Handler nop reti ;Timer2 Overflow Handler nop reti ;Timer1 Capture Handler nop jmp TIM1_COMPA ;Timer1 Compare A Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 outiw OCR1A,0x7A12 outi TCCR1A,0 outi TCCR1B,0xC outi TCCR1C,0x0 outi TIMSK1,0x2 outi DDRB,0x1 sei L0000: xjmp L0000 TIM1_COMPA: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG 

Parece que ya no hay nada más que comentar. Inicializamos los dispositivos, configuramos interrupciones y disfrutamos del programa.


Trabajar a través de interrupciones es la forma más fácil de crear programas para trabajar en tiempo real. Desafortunadamente, no siempre es posible cambiar entre tareas paralelas utilizando solo controladores de interrupciones para realizar estas tareas. La restricción es la prohibición del manejo de interrupciones anidadas, lo que lleva al hecho de que hasta que el procesador salga, el procesador no responde a todas las otras interrupciones, lo que puede conducir a la pérdida de eventos si el procesador funciona durante demasiado tiempo.


Una solución es separar el código de registro del evento y su procesamiento. El núcleo de procesamiento de subprocesos múltiples en paralelo de la biblioteca está organizado de tal manera que cuando ocurre un evento, el controlador de interrupciones solo registra el evento dado y, si es necesario, realiza las operaciones mínimas de captura de datos necesarias, y todo el procesamiento se realiza en la secuencia principal. El kernel verifica secuencialmente la presencia de indicadores no procesados ​​y, si se encuentra, continúa con la tarea correspondiente.


El uso de este enfoque simplifica el diseño de sistemas con varias tareas asincrónicas, lo que le permite considerar cada una de ellas de forma aislada, sin centrarse en los problemas de conmutar recursos entre tareas. Como ejemplo, considere la implementación de dos tareas independientes, cada una de las cuales cambia su salida con un cierto retraso.


Código fuente Ejemplo 4
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 4); tasks.Heap = new StaticHeap(tasks, 64); var t1 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit1.Toggle(); tsk.Delay(32); tsk.TaskContinue(loop); },"Task1"); var t2 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit2.Toggle(); tsk.Delay(48); tsk.TaskContinue(loop); }, "Task2"); var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1); tasks.ActivateNext(ca, tasks.AlwaysOn, t2); ca.Dispose(); m.EnableInterrupt(); tasks.Loop(); Console.WriteLine(AVRASM.Text(m)); } } } 

En esta tarea, configuramos las salidas cero y primera del puerto B a salida y cambiamos el valor de 0 a 1 y viceversa con un período de 32 ms para cero y 48 ms para la primera salida. Una tarea separada es responsable de administrar cada puerto. Lo primero a tener en cuenta es la definición de una instancia de Paralelo. Esta clase es el núcleo de la gestión de tareas. En su constructor, determinamos el número máximo permitido de subprocesos que se ejecutan simultáneamente. La siguiente es una asignación de memoria para almacenar flujos de datos. La clase StaticHeap utilizada en el ejemplo asigna un número fijo de bytes para cada flujo. Para resolver nuestro problema, esto es aceptable, y el uso de una asignación de memoria fija en comparación con la dinámica simplifica los algoritmos y hace que el código sea más compacto y rápido. Más adelante en el código, describimos un conjunto de tareas que están diseñadas para ejecutarse bajo el control del núcleo. Debe prestar atención a la función asíncrona Delay, que usamos para formar un retraso. Su peculiaridad es que cuando se llama a esta función, el retraso requerido se establece en la configuración de flujo y el control se transfiere al núcleo. Una vez transcurrido el intervalo establecido, el núcleo devuelve el control a la tarea desde el comando que sigue al comando Delay. Otra característica de la tarea es programar el comportamiento del flujo de la tarea al finalizar en el último comando de la tarea. En nuestro caso, ambas tareas están configuradas para ejecutarse en un bucle infinito con el control regresando al núcleo al final de cada ciclo. Si es necesario, completar una tarea puede liberar el hilo o pasarlo para realizar otra tarea.


La razón para invocar la tarea es activar la señal asignada al flujo de la tarea. La señal se puede activar tanto mediante programación como por hardware mediante interrupciones desde dispositivos periféricos. Una llamada de tarea restablece la señal. Una excepción es la señal predefinida AlwaysOn, que siempre está en estado activo. Esto hace posible crear tareas que recibirán control en cada ciclo de sondeo. La función LOOP es necesaria para invocar el ciclo de ejecución principal. Desafortunadamente, el tamaño del código de salida cuando se usa Parallel ya se está volviendo significativamente más grande que en los ejemplos anteriores (aproximadamente 600 comandos) y no se puede citar por completo en el artículo.


Y para dulce, algo más parecido a un proyecto en vivo, a saber, un termómetro digital. Todo es como siempre simple. Un sensor digital con una interfaz SPI, un indicador de 7 segmentos y 4 dígitos y varios hilos de procesamiento para mantener las cosas frescas. En uno, manejamos un ciclo para indicación dinámica, en otro, eventos que desencadenan un ciclo de lectura de temperatura, en el tercero leemos los valores recibidos del sensor y lo convertimos de un código binario a BCD y luego en un código de segmento para un búfer de indicación dinámica.


El programa en sí es el siguiente.


Código fuente Ejemplo 5
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var led7s = new Led_7(); led7s.SegPort = m.PortC; led7s.Activate(); m.PortD.Direction(0xFF); m.PortD.Activate(); m.PortB[0].Mode = ePinMode.OUT; var tc77 = new TC77(); tc77.CS = m.PortB[0]; tc77.Port = m.SPI; m.Timer0.Clock = eTimerClockSource.CLK64; m.Timer0.Mode = eWaveFormMode.Normal; var reader = m.DREG("Temperature"); var bcdRes = m.DREG("digits"); var tmp = m.BYTE(); var bcd = new BCD(reader, bcdRes); m.subroutines.Add(bcd); var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 64); var tmrSig = os.AddSignal(m.Timer0.OVF_Handler); var spiSig = os.AddSignal(m.SPI.Handler, () => { m.SPI.Read(m.TempL); m.TempL.MStore(tmp); }); var actuator = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureAsync(); tsk.Delay(16); tsk.TaskContinue(loop); }, "actuator"); var treader = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureCallback(os, reader, tmp); reader >>= 7; m.CALL(bcd); tsk.TaskContinue(loop); }, "reader"); var display = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); m.PortD.Write(0xFE); m.TempQL.Load(bcdRes.Low); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFD); m.TempQL.Load(bcdRes.Low); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFB); m.TempQL.Load(bcdRes.High); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xF7); m.TempQL.Load(bcdRes.High); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); tsk.TaskContinue(loop); }, "display"); var ct = os.ContinuousActivate(os.AlwaysOn, actuator); os.ActivateNext(ct, spiSig, treader); os.ActivateNext(ct, tmrSig, display); tc77.Activate(); m.Timer0.Activate(); m.EnableInterrupt(); os.Loop(); Console.WriteLine(AVRASM.Text(m)); } } } 

Está claro que este no es un borrador funcional, sino solo una demostración tecnológica diseñada para demostrar las capacidades de la biblioteca NanoRTOS. Pero en cualquier caso, menos de 100 líneas de código fuente y menos de 1 kb de código de salida es un resultado bastante bueno para una aplicación viable.


En los siguientes artículos, planeo, en caso de interés en este proyecto, profundizar más en los principios y características de la programación usando esta biblioteca.

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


All Articles