Gestión de memoria PIC18

El tamaño de pila limitado de los PIC de presupuesto es un área problemática y he ajustado mi código para adaptarse a esta realidad. Actualmente adopto un paradigma aproximado de agrupar funciones estrechamente relacionadas en un módulo y declarar todas las variables estáticas globales en el módulo (para reducir la cantidad de variables almacenadas en el segmento automático, y los problemas de mutabilidad solo son relevantes en ISR, que considero .) No hago esto porque sea una buena práctica, pero la realidad es que tiene una cantidad finita de espacio para asignar todas las variables de funciones locales que existen en un proyecto completo.

En el mundo integrado de chips de 8/16 bits, ¿es este un método apropiado, siempre que esté seguro de tomar las precauciones necesarias? También hago cosas como asignar > 256 bytes de RAM para búferes de Ethernet y tengo que acceder a esa memoria a través de punteros para poder evitar la semántica de la banca de memoria. ¿Lo estoy haciendo mal? Mi aplicación funciona, pero estoy 100% abierto a sugerencias de mejora.

Solo una crítica constructiva sobre tu publicación. Creo que muchas personas pasarán por alto su pregunta debido a un párrafo tan extenso. Es posible que desee considerar editar su publicación para tener algunos saltos de línea en puntos significativos para que sea más fácil de leer. Esto probablemente le permitirá obtener mejores respuestas.
Punto tomado, así como acción correctiva :)
Si tu aplicación funciona, ¡genial! "Si es estúpido pero funciona, no es estúpido". -- Ley de combate #6 de Murphy
"funciona" != "hecho" != "mantenible"

Respuestas (3)

Tal vez me estoy perdiendo algo aquí (los párrafos ayudarían) pero con el compilador C18, las variables locales dentro de una función generalmente se asignan en la pila de software, por lo que no tengo idea de lo que significa lo siguiente:

pero la realidad es que tiene una cantidad finita de espacio para asignar todas las variables de funciones locales que existen en un proyecto completo

Al mover todas sus variables a globales dentro de un módulo, necesita que haya espacio para todas ellas al mismo tiempo.

buffers y tengo que acceder a esa memoria a través de punteros para poder evitar la semántica de la banca de memoria.

Qué compilador estas usando?

Parece que necesita aumentar la asignación de pilas, pensé que incluso c18 lo permitía. Cuesta dinero para que una corporación lo use y debería tener un buen optimizador, etc. Nunca he visto un compilador de dinero de costo que no permita la asignación de pilas.
Los dispositivos PIC18 tienen una pila fija. Usamos el compilador Hi-Tech PICC18 que realmente optimiza la pila para mí. El problema es que aún es posible ralentizar la ejecución si no se tiene cuidado de mantener corta la profundidad de la llamada.
Voy a mirar en esto. si yo fuera tu jefe, habría comprado un nuevo compilador hace un tiempo.
@nate, todavía me has perdido, ¿por qué tu solución para tener una longitud fija, una pila relativamente corta sería hacer que todo sea global para que ocupe una ubicación de memoria todo el tiempo en lugar de solo cuando se necesita en la pila? Debe minimizar la profundidad de la llamada y usar tantas variables locales como pueda dentro del tamaño objetivo de su pila para minimizar el uso de la memoria. Si hay una función que tiene una cadena profunda que hace que su pila sea grande, optimice el uso de la pila en esa función, no en todo el proyecto.
@Mark: el compilador coloca variables locales no estáticas en una sección bancaria de ram (psect o sección de programa), que tiene 256 bytes de ancho. El compilador optimiza estas variables lo mejor que puede para reutilizar la memoria disponible en este segmento. Me encuentro con problemas cuando la profundidad de la llamada se vuelve más profunda que 3 y las variables locales asociadas, los parámetros y los valores devueltos se arrastran hacia arriba. El compilador optimiza la pila, y el código se compilará y ejecutará, pero a una velocidad reducida debido a que el compilador se ve obligado a usar ubicaciones de ram que están "más lejos" (más lentas). Creo que lo hace a través de punteros.
@Mark: Mi pensamiento es, si tengo un ancho de pantalla de 21 caracteres en mi LCD y tengo un módulo que necesita acceso a dicho búfer en múltiples funciones, ¿no es una mejor idea para mí declararlo estático global dentro? el módulo y hacer que todas las funciones usen los mismos 21 bytes? Debo aclarar que tengo bastante RAM disponible. Mi opinión es que, al reservar 21 bytes para un búfer dentro de un módulo que puede cumplir muchas funciones, en realidad necesito 1 byte en la pila (un puntero), en lugar de incurrir en la situación posiblemente problemática de una profundidad de llamada > 3 y la sobrecarga asociada .
sí, ese es un lugar donde puede usar un global para el búfer de caracteres LCD. Puede poner cualquier dato que desee en la memoria RAM de acceso (no bancarizada) con la palabra clave 'cerca' cuando define la variable, pero hay una memoria RAM de acceso limitado. La ram almacenada no requiere un puntero, simplemente está almacenada, lo que significa que acceder a ella puede requerir un interruptor de banco, por lo que el acceso solo requiere más instrucciones. Las secciones de 256 bytes de ram no son fijas, puede cambiar el diseño de estas secciones en el script del enlazador.

Se utilizan dos enfoques para la asignación de variables en los compiladores PIC. Algunos usan una pila de software indexada a partir de FSR2, mientras que otros simplemente usan variables superpuestas estáticamente. ambos planteamientos tienen ventajas y desventajas. El uso de variables superpuestas significa que no hay posibilidad de un desbordamiento de pila en tiempo de ejecución. También significa que uno puede tener aproximadamente 64-128 bytes de variables globales a las que se puede acceder desde cualquier instrucción sin tener que preocuparse por la banca. Desafortunadamente, excluye la recursividad, dificulta ciertas situaciones que involucran punteros de función y, a menudo, conduce a un código que está inflado con movlbinstrucciones porque los compiladores a menudo no son muy buenos para organizar los bancos de manera eficiente.

La mejor disposición de las variables dependerá del tipo de compilador que esté utilizando. Desafortunadamente, el código optimizado para un compilador a menudo funcionará mal en otro.

Por cierto, no tengo idea de por qué Microchip no puede hacer un chip que, por ejemplo, proporcione 16 bytes de direccionamiento indirecto de FSR2 y ocho bytes de FSR0 y FSR1, mientras deja 64-96 bytes del "banco común" disponible para el almacenamiento del usuario. , pero por alguna razón, la mayoría de los PIC que he visto hacen que el uso del área "común" sea una propuesta de "todo o nada" a pesar de que pocas rutinas necesitarán más de 16 bytes de marco de pila local.

En realidad, el compilador C18 usa una pila de software indexada fuera de FSR1, no FSR2.
@OlinLathrop: ¿Incluso en partes con conjunto de instrucciones extendido? El conjunto de instrucciones extendidas permite acceder directamente a cualquiera de los primeros 64 a 96 bytes que siguen a la dirección dada en FSR2, lo que sería útil si FSR2 se usara como un puntero de pila de software. El código que intentara usar FSR1 como un puntero de pila de software sería bastante horrible, ¿es eso realmente lo que hace PIC18?
No sé qué hace C18 cuando se usa el conjunto de instrucciones extendidas. Sé que para el conjunto de instrucciones normal se usa FSR1 como puntero de pila y FSR2 como puntero de cuadro. La aplicación solo se queda con FSR0 para su propio uso. Sí, creo que esto también apesta.
@OlinLathrop: Eso es alucinantemente horrible. Tener tanto un puntero de pila como un puntero de marco codifican en lenguaje ensamblador más conveniente (ya que las variables locales vivirán en un desplazamiento constante del puntero de marco), pero en cualquier punto dado del código, la diferencia entre los dos punteros generalmente será constante, por lo que un compilador no debería necesitar ambos punteros [si se sabe que SP está cuatro bytes por debajo de donde estaría BP, el código que accedería (BP+6) puede acceder a (SP+10)]. ¿C18 usa un prólogo de ocho palabras y un epílogo de ocho palabras en cada rutina para manejar el puntero de cuadro?

Lo que describes no es una buena idea. Sin embargo, si lo tiene funcionando en un proyecto en particular y ha sido bien probado, déjelo en paz.

Generalmente asigno variables en un PIC 18 de cuatro maneras diferentes:

  1. Globales. Estos son los pocos valores que deben ser visibles a nivel del sistema fuera de los módulos individuales. Otra forma de verlo es que estos son los valores utilizados para comunicarse entre módulos. Si encuentra que una gran fracción de su estado debe ser global, es posible que no haya dividido bien el sistema en módulos.

    Un ejemplo de estado global podrían ser los valores A/D filtrados finales. Las señales analógicas se leen más rápido de lo que se necesitan los resultados en el módulo AD. Esto aplica dos polos de filtrado de paso bajo a cada señal y también posiblemente algo de escala. El estado del filtro es privado para el módulo AD con solo los valores finales filtrados y escalados declarados como globales, ya que estos son todo lo que el resto del sistema necesita conocer.

    Usualmente uso el banco de acceso para el estado global. Debe ser una colección de variables individuales de varios módulos, por lo que generalmente se ajusta fácilmente. Si necesita exportar búferes completos por algún motivo, cada uno de ellos debe estar en su propia sección y, por supuesto, cualquier otro módulo que acceda a dichos búferes debe saber que no están en el banco de acceso.

  2. Locales estáticos. Estos contienen los valores que deben ser persistentes, pero son privados para módulos individuales. En el ejemplo anterior, los valores de filtro intermedio estarían en esta categoría. Necesitan quedarse entre llamadas al módulo, o se utilizan para comunicarse entre rutinas del módulo a las que se puede llamar por separado.

    Por lo general, los coloco en la memoria almacenada, con todo el estado local de un módulo en el mismo banco. Defino la constante LBANK (banco local) en la parte superior del módulo para definir en qué banco estarán las variables locales de ese módulo. Esto se garantiza definiendo secciones del enlazador en el archivo del enlazador .BANKn donde N es el número de banco. Luego, en el código, las variables se definen en esos bancos nombrados para garantizar que el enlazador los colocará allí. Conocer el banco en el momento de la creación es útil para permitir una gestión bancaria inteligente.

  3. Dinámica. Mantengo una pila de datos y uso FSR2 como puntero de pila. Con la elección correcta del diseño de la pila, se pueden empujar y extraer bytes de RAM con instrucciones individuales. Los envuelvo en macros push y pop para no tener que pensar en la mecánica de la pila cada vez y para que el código sea más legible.

    C18 hace esto con lo que se denominan variables "automáticas" en C. Sin embargo, aunque C18 parece generar un código confiable, su elección de administración de memoria solo puede llamarse muerte cerebral en el mejor de los casos. Solo hay 3 FSR, lo que los hace preciosos. C18 toma dos de estos para su propio uso, dejando la aplicación solo con FSR0. C18 tiene una pila de datos, pero increíblemente, el diseño de la pila se elige para que empujar y sacar no sean instrucciones individuales. También tiene un modelo de paso de argumentos que limpia la persona que llama, que consume memoria innecesariamente para la mayoría de las llamadas a subrutinas normales, pero eso es una digresión para otro día.

  4. Registros generales. Normalmente defino los primeros 16 bytes de memoria como globales llamados REG0-REG15. Dado que estos están en el banco de acceso, se pueden usar como registros generales de otras máquinas. Estos son los caballos de batalla del estado temporal. También los uso para argumentos de subrutinas y valores devueltos la mayor parte del tiempo. Por ejemplo, la subrutina UART_PUT envía el byte en REG0 y la subrutina UART_GET devuelve el siguiente byte recibido en REG0. Las subrutinas generalmente los conservan, excepto aquellos en los que devuelven datos explícitamente. Estos son los valores borradores temporales que se usan internamente en las subrutinas. Para ayudar a las subrutinas a conservarlas, tengo macros para que enumere el conjunto de estos registros generales que una subrutina eliminará en un lugar de la definición de subrutina.

Me molesta que HiTech C no tenga un modelo de memoria/convención de llamada que reconozca un banco de 256 bytes como el "predeterminado" y requiera que las subrutinas dejen ese banco seleccionado al salir (pero puedo suponer que está seleccionado al ingresar). Muchas aplicaciones podrían adaptarse a sus variables no matrices de uso común en 320-384 bytes, evitando el 90% de las instrucciones bancarias, si tal convención estuviera disponible.
@supercat: configurar el banco es solo una instrucción en un PIC 18, lo cual es menos significativo si ya está llamando a una subrutina de todos modos. Se necesitan al menos 4 ciclos para entrar y salir de una subrutina, más cualquier ciclo que funcione la subrutina. Obligarlo a un banco en particular a la salida podría desperdiciar instrucciones tanto como salvarlas. Mi sistema utiliza el modelo de que el banco es desconocido al entrar y que las subrutinas pueden destruir el banco, a menos, por supuesto, que la subrutina esté específicamente documentada para hacer algo diferente.
Mi preocupación no es tanto con el tiempo como con el tamaño del código; solo si una rutina que requiere una instrucción adicional al salir para configurar el banco "principal" (utilizado para parámetros, locales y algunos globales si caben) nunca es un código cuyo primer acceso bancario siguiente sería a ese banco, ¿una convención que requiera que las rutinas establezcan la salida de ese banco no sea una "ganancia" neta. Requerir que el banco se configure en la entrada puede ser más nebuloso, pero a su favor estaría el hecho de que la última instrucción que precede a muchas llamadas es probable que sea un "movwf" a un parámetro almacenado en ese banco.
No digo que todos los proyectos deban usar necesariamente tal convención, pero si un proyecto usa un total de menos de 320 bytes (y muchos lo hacen), no debería haber necesidad de instrucciones bancarias. Si todas las partes pequeñas, excepto unas pocas, caben en un espacio de 320 bytes, es posible que esas partes necesiten más código bancario en la entrada/salida de lo que necesitarían de otro modo, pero aún podría haber una ganancia neta. Ha pasado un tiempo desde que perfilé mi código, pero a pesar de que trato de tener en cuenta la arquitectura del PIC, creo que perfilé una de mis aplicaciones...
... como un 20% inflado por instrucciones bancarias o instrucciones MOVFF hacia/desde WREG. La arquitectura de dirección única del PIC es buena cuando significa que toda la memoria que uno necesita está en el "conjunto de trabajo" la mayor parte del tiempo. Pero a menos que un compilador haga un buen trabajo con la banca, a menudo ese no será el caso. Por cierto, acabo de pensar en una forma de mejorar enormemente la utilidad del PIC, dado el espacio para 512 códigos de operación: tome 32 bytes del espacio de direcciones que se utilizan actualmente para el marco de pila FSR2 demasiado grande y haga que eliminen la referencia de 1 a 16 bytes cada uno apagado FSR0 y FSR1.
Luego agregue versiones de una sola palabra de LFSR para cargar FSR0 o FSR1 con direcciones que sean múltiplos de 16 bytes. Voila: uno tiene tres "bancos" activos a la vez en lugar de uno (dos de ellos tienen solo 17 bytes; uno tiene 256). Si uno tuviera 1024 códigos de operación, podría aliviar la restricción de cargar FSR0/1 con múltiplos de 8 (lo que significa que las estructuras de datos no alineadas de 32 bits o incluso de 64 bits podrían desreferenciarse directamente después de un LFSR de una sola palabra).
@supercat: si le preocupan los ciclos individuales en ese nivel, no debería usar un compilador.
El problema no son los ciclos, sino el espacio del código. Si más del 20% de las instrucciones son "MOVLB n", "MOVFF WREG,nnn" o "MOVFF nnn,WREG", eso significa que el código es probablemente un 15% más grande de lo que realmente debe ser. Cuando me enfrento a un límite de tamaño, puedo volver a trabajar en los puntos calientes que HT-PICC18 ha codificado mal (las llamadas indirectas de funciones con muchos parámetros son bastante horribles, por ejemplo), pero el 20% de las instrucciones bancarias es bastante molesto.