"Ahora te mostraré un retrato ... Hmm ... Te advierto que este es un retrato ... De todos modos, trátalo como un retrato ...En esta publicación, hablaremos sobre el desarrollo y la depuración de programas para el CC1350 MK en el entorno de desarrollo CCS recomendado por el fabricante. Los méritos (y lo son) y las desventajas (y cómo sin ellos) de los productos anteriores se verán afectados. No habrá capturas de pantalla en el texto para mostrar (en un círculo) la ubicación del icono de compilación en el entorno de programación integrado o seleccionar un archivo en un directorio. Reconociendo la posibilidad fundamental de artículos en un estilo similar, trataré de enfocarme en temas conceptuales con la esperanza de que mi lector pueda descubrir los detalles.
El propósito de este opus, además de compartir la experiencia adquirida, es un intento de despertar una sana envidia entre los fabricantes nacionales de MK, que son competidores directos de TI ("en el país donde prosperamos"); la tarea es francamente desagradecida, pero dicen que una piedra se desgasta una gota.
Voy a enfatizar de inmediato que solo se tratará de la versión 7 de Windows 7 (además, solo), aunque el sitio web de TI tiene una opción para Mac y Linux, no los he probado, estoy bastante listo para creer que no todo está tan bien allí, pero ¿por qué pensar? sobre lo malo (o viceversa, todo es genial allí, pero entonces, ¿por qué la envidia?).
Entonces, ¿qué nos enseña el sitio web de TI? Para comenzar a trabajar con módulos de evaluación, debe realizar tres pasos necesarios:
- Comprar módulos de evaluación - hecho.
Nota sobre los márgenes (PNP): también tendrá que hacer esto, porque en el entorno de programación en cuestión, personalmente (desafortunadamente) no logré encontrar la capacidad de emular hardware para la depuración, al menos donde estaba buscando. - Instale el entorno de desarrollo: descargue, ejecute el instalador, todo funcionó. Conectamos el módulo de evaluación a USB: la leña se eleva sola y todo volvió a funcionar, listo. Cuando intentas programar el dispositivo, recibimos un mensaje sobre la necesidad de actualizar el firmware, estamos de acuerdo y nuevamente todo resultó. En general, no hay nada de qué escribir si fue siempre y en todas partes ...
- Vaya y estudie el curso TI SimpleLink Academy 3.10.01 para SimpleLink CC13x0 SDK 3.10: una sugerencia extraña, parece que me enseña, solo para estropear, pero que así sea, abro el enlace correspondiente y estoy atónito: cuántas cosas hay aquí.
Aquí vemos materiales de capacitación sobre cómo trabajar con controladores de hardware SYS / BIOS y con el sistema operativo TI-RTOS y el uso de la pila de red NDK, incluido USB, y el uso de protocolos inalámbricos y muchos más aspectos del trabajo con representantes de varias familias MK producidas por la compañía. Y toda esta riqueza viene acompañada de ejemplos listos para usar, y si tenemos en cuenta la presencia de manuales de usuario y descripciones de módulos, entonces quizás no hay nada más que desear. Pero también hay utilidades que facilitan el trabajo de preparación y configuración del código del programa, flasheo y depuración de varias maneras, y esta riqueza también está bastante documentada.
Pnp: si alguien se inclina a considerar este material como publicidad en relación con la empresa, sus productos y su sistema de programación, lo más probable es que sea correcto y estoy realmente impresionado con el volumen de software detectado. Su calidad se discutirá más a fondo y, espero, las sospechas de parcialidad se disiparán, el sentimiento no me cegó por completo y sigo viendo perfectamente los defectos del objeto de descripción, por lo que este no es el amor de la juventud, sino un sentimiento serio de un especialista adulto. Tengo miedo de imaginar la cantidad de costos de material necesarios para crear y mantener un volumen de software y documentación para ello, pero esto obviamente no se hizo en un mes, y la compañía probablemente comprende lo que está haciendo.
Bien, hasta que pospongamos el estudio de los materiales para más adelante, comprenderemos todo "en el camino con la pepita" y audazmente abriremos CCS. Implementa el concepto de espacios de trabajo, recibido del padre - Eclipse. Personalmente, el concepto del proyecto está más cerca de mí, pero nadie nos molesta para mantener exactamente un proyecto en el espacio, así que sigamos adelante.
Pero luego las cosas empeoran un poco: abrimos el espacio de trabajo (RP) para nuestra placa de depuración y vemos muchos proyectos (por regla general, en dos versiones, para RTOS y para "ironía"). Como dije antes, esto no es un delito, pero el hecho de que muchos proyectos contengan los mismos archivos con módulos de software idénticos no es nada bueno. El código se duplica muchas veces y respaldar los cambios se convierte en una tarea muy trivial. Sí, con tal solución es mucho más fácil transferir el proyecto simplemente copiando el directorio, pero para tales cosas hay exportación del proyecto, y está bastante bien implementado. Los enlaces a los archivos en el árbol del proyecto son adecuadamente compatibles, por lo que la decisión de incluir los archivos en los ejemplos proporcionados no puede considerarse satisfactoria.
Continuamos nuestra investigación: comenzaremos a trabajar con un proyecto terminado, pero sin parpadear un LED, aunque hay dos de ellos en la placa de depuración, pero trabajando con un puerto serie, un ejemplo de uartecho listo para usar. Creamos un nuevo RP, incluimos el proyecto que nos interesa y ... nada sale de él, queda claro por el mensaje que es necesario incluir un proyecto relacionado en el RP. No está muy claro por qué se hace esto, pero no es difícil cumplir con los requisitos del medio ambiente, después de lo cual el proyecto comienza a ensamblarse.
Pnp: en la máquina doméstica, utilicé el comando Importar proyecto y todas las inclusiones necesarias ocurrieron por su cuenta. Cuando se indiquen exactamente proyectos relacionados, no lo sé, dejemos el análisis de este aspecto para el futuro.
Compilamos, flasheamos y comenzamos a depurar. Encontramos un fenómeno interesante: la ejecución paso a paso no se muestra adecuadamente al considerar la biblioteca de trabajar con un puerto serie: los costos de optimización. Desactivamos la optimización en la configuración del compilador (configuración que no existe, hay realmente gente que las conoce y, además, las usa todas), armamos el proyecto nuevamente, y nada cambia. Resulta que solo se incluyen aquellos archivos que están incluidos en el árbol del proyecto, al menos en forma de enlaces. Agregamos enlaces a las fuentes de la biblioteca y después de reconstruir todo se depura correctamente (siempre que tengamos la opción de generar información de depuración habilitada).
Pnp: pero encontré opciones para habilitar la verificación de cumplimiento MISRA-C.
Pnp: otra forma es usar el comando "Limpiar ..." con el ensamblaje posterior, el comando "Construir todo" por alguna razón no afecta el proyecto asociado.
Luego descubrimos que no todo se depura siempre normalmente, a veces nos encontramos en áreas de código de máquina para las cuales el compilador no encuentra la fuente. Dado que el entorno de programación nos proporciona todos los archivos necesarios para el trabajo: el resultado del preprocesador, el código del ensamblador y la tarjeta de enlace (solo debe recordar habilitar las opciones correspondientes), pasamos a este último. Encontramos dos áreas del código del programa: a partir de 0x0000. y a partir de 0x1000. (Las arquitecturas de 32 bits son buenas para todos, pero la escritura de direcciones no es su punto fuerte). Pasamos a la documentación del microcircuito y descubrimos que dentro hay un área ROM asignada específicamente a 0x1000., Y contiene la parte integrada de las bibliotecas. Se argumenta que el uso de rutinas mejora el rendimiento y reduce el consumo en comparación con el espacio de direcciones 0x000. Si bien estamos dominando MK, no estamos tan interesados en los últimos parámetros, pero la conveniencia de la depuración es crucial. Puede deshabilitar el uso de ROM (pero para nuestros propósitos) configurando la opción NO_ROM en el compilador, lo que hacemos y reensamblamos el proyecto.
PNP: la transición a la subrutina en la ROM se ve muy divertida: no hay una transición larga en el sistema de comando, por lo que primero se realiza la transición con un retorno al punto intermedio en el área de dirección baja (0x0000), y allí yace el comando de arranque de la PC, cuyos parámetros no son reconocidos por el desensamblador. Algo que no puedo creer, como si con esos costos generales se pudiera ganar en velocidad, aunque para rutinas largas, ¿por qué no?
Por cierto, una pregunta interesante es cómo generalmente se garantiza que el contenido de la ROM corresponde a los códigos fuente amablemente proporcionados por la empresa. Puedo sugerir de inmediato un mecanismo para incorporar funciones adicionales (por supuesto, depuración y servicio) en la ROM, que para el usuario, el programador MK será completamente invisible. Y personalmente, no tengo dudas de que los desarrolladores del chip también conocen muchos otros mecanismos que implementan dicha funcionalidad, pero terminaremos el ataque de paranoia.
Por otro lado, solo puedo dar la bienvenida a la aparición de un análogo de este tipo de BIOS, porque a la larga esto hará realidad el sueño de los desarrolladores de la portabilidad real del código entre diferentes familias MK con un núcleo. También observamos la peculiaridad de la implementación de la interacción con los módulos de software "integrados". Si en los primeros intentos de crear un mecanismo similar implementado en los modelos TivaC, hubo un supervisor de llamadas al que se accedió con el número de grupo y el número del punto de entrada en el subprograma, lo que causó una sobrecarga significativa, aquí la resolución de las comunicaciones está en el nivel de enlace debido a los nombres dobles de funciones y Se insertan saltos largos directos a subrutinas en ROM. Esto es mucho más rápido en la ejecución, pero requiere una recompilación del proyecto al cambiar el modelo de uso.
Ahora que estamos completamente preparados para la depuración conveniente, volvemos a nuestro proyecto y comenzamos a depurar el programa en silencio con acceso a los códigos fuente de los módulos (bueno, eso es lo que pensé ...), lo que nos permitirá formar una opinión sobre la calidad de estos textos. El proyecto en estudio implementa un espejo del canal de comunicación en serie y es extremadamente conveniente para fines de capacitación. Por supuesto, tomamos la opción usando RTOS, no veo la menor razón para no usarlo en nuestra configuración (mucha memoria y memoria de programa).
Inmediatamente, notamos que los códigos fuente se presentan en C, a menudo no es muy conveniente, muchas construcciones de lenguaje parecen engorrosas en comparación con sus análogos en las ventajas, pero los creadores estaban más preocupados por la compatibilidad del código que el azúcar sintáctico. Aunque sería posible crear una versión C ++ de las bibliotecas, la compilación condicional se conoce desde hace mucho tiempo y se usa en todas partes, pero esto conlleva costos adicionales de material. Seguramente, la gerencia de la compañía sabe lo que están haciendo, y mis comentarios son una especie de "análisis astuto", pero me parece que también tengo derecho a mi opinión.
También conozco el enfoque opuesto, cuando la biblioteca está diseñada con las últimas herramientas de C ++, y cuando se le pregunta qué hacer para aquellos desarrolladores que usan compiladores que no cumplen con las últimas especificaciones, la respuesta perfecta es actualizar a nuevas versiones o no esta biblioteca (recomiendo la segunda opción en tales casos). Mi opinión personal es que si realmente queremos que se use nuestro producto (y TI claramente lo quiere, y no crea la biblioteca con el principio de "déjelo, aquí hay un nuevo tambor para usted"), entonces su enfoque es ciertamente cierto.
El código fuente del programa tiene un aspecto clásico: inicializa el entorno de hardware y software, crea tareas y lanza un programador en el módulo principal, el texto de la tarea en un módulo de compilación separado. En el ejemplo en consideración, la tarea es exactamente una: mainThread, el propósito no está completamente claro del nombre y, lo que me confunde un poco, el nombre del archivo que contiene el texto fuente no coincide con el nombre de la función (uartecho.c, aunque el nombre está hablando aquí), bueno, sí La búsqueda en el entorno de programación se implementa de forma estándar (menú contextual o F3 en el nombre de la entidad) y no hay ningún problema con esto.
El proceso de establecer los parámetros de la tarea antes de comenzar es bastante esperado:
- crear una estructura de parámetros (local, por supuesto),
- darle valores por defecto,
- establecer parámetros distintos al estándar, y
- use la estructura al crear la tarea.
A pesar del tipo de naturalidad de estas operaciones, no es obvio para todos los autores de bibliotecas, y vi varias implementaciones en las cuales, por ejemplo, no hubo etapa 2, lo que condujo a un comportamiento de programa divertido (para un observador externo, no para un programador). En este caso, todo está bien, la única pregunta que ha surgido es por qué los valores predeterminados no son constantes, probablemente este es un legado del pasado maldito.
PNP: en el conocido FREE-RTOS se adopta un enfoque ligeramente diferente con los parámetros de la tarea indicados directamente en el cuerpo de la llamada de la API de la función de creación de tareas. Los pros y los contras de estos enfoques son los siguientes:
- + le permite no especificar explícitamente parámetros que coincidan con los valores predeterminados, + no requiere recordar el orden de los parámetros, -más detallado, -Costos de memoria más grandes, -Usted necesita conocer los parámetros predeterminados, -crea un objeto intermedio con nombre
- - requiere especificar todos los parámetros, -requiere recordar el orden de los parámetros, + es más compacto, + requiere menos memoria, + no requiere objetos intermedios con nombre.
Hay un tercer método, defendido por el autor de esta publicación (al estilo de TURBO), que tiene su propio conjunto - + le permite no especificar explícitamente los parámetros que coinciden con el estándar, + no requiere recordar el orden de los parámetros, -multi-verbal, -costas de memoria más grandes, -Necesita conocer los parámetros predeterminados, + funciona en el estilo lambda, + hace que los errores estándar sean difíciles de implementar, -se ve algo extraño debido a los muchos corchetes correctos.
Bueno, hay otra cuarta opción, sin inconvenientes, pero que requiere C ++ no inferior a 14: nos lamemos los labios y pasamos de largo.
Comenzamos a depurar, ejecutamos el programa y abrimos uno de los dos puertos seriales provistos por la placa de depuración en la ventana de terminal proporcionada por el entorno de programación. ¿Cuál de los dos puertos (uno es la depuración, probablemente el segundo es el usuario, puede ver sus números en el sistema) es difícil de decir por adelantado, a veces el más joven, a veces el mayor, al menos no cambia cuando vuelve a conectar el tablero, por lo que puede escribirlo en el tablero. Bueno, un inconveniente más: los terminales abiertos no se guardan con el proyecto y no se restauran cuando abre una sesión de depuración, aunque no se cierran cuando sale de él. Verificamos el funcionamiento del programa e inmediatamente descubrimos un inconveniente más: el terminal no se puede configurar, por ejemplo, básicamente funciona en estilo Unix con un cierre / r, he perdido el contacto con tal minimalismo, aunque nadie nos molesta con el uso de un programa de terminal externo.
Pnp: Observamos una característica más de la depuración, bueno, esto es cierto para cualquier entorno de desarrollo: cuando cambiamos las tareas con un programador, perdemos el foco, los puntos de interrupción nos ayudarán a resolver este problema.
Para comenzar, considere el proceso de crear una instancia de un puerto serie: todo parece ser estándar aquí, se utiliza una estructura, cuyos campos tienen asignados los parámetros requeridos del objeto. Tenga en cuenta que en las ventajas tenemos la oportunidad, completamente ausente de C, de ocultar toda la inicialización muy bien "bajo el capó", pero ya he expresado posibles argumentos a favor de la segunda solución. Hay una función para inicializar la estructura de ajuste, y esto es bueno (por paradójico que parezca, esta función no parece obligatoria para los autores de algunas bibliotecas). En este punto de la historia, la luna de miel termina y comienza la vida ordinaria
(conyugal) .
Un estudio cuidadoso de las fuentes muestra que no todo es tan bueno. ¿Cuál es el problema? La función de inicialización copia los valores predeterminados del objeto que se encuentra en la región constante en nuestra estructura de control, y esto es maravilloso, pero por alguna razón:
- el objeto es global, aunque es usado por la única función para inicializar los parámetros (en un momento una práctica similar le costó a Toyota una cantidad decente) - bueno, agregar la directiva estática es fácil;
- el objeto de control se llama, en C no hay una solución hermosa para este problema, o más bien, hay una solución con una copia anónima y la di en una publicación de mucho tiempo, pero muchos corchetes correctos no permiten llamar a esta opción realmente hermosa, además hay una solución de belleza increíble, pero ¿qué? soñar con un sueño imposible;
- todos los campos del objeto son claramente redundantes en profundidad de bits, incluso los campos de bits (enumeraciones de dos valores posibles) se almacenan en palabras de 32 bits;
- las constantes de modo enumerado se definen en forma de define, lo que hace que sea imposible verificar en la etapa de compilación y necesario en tiempo de ejecución;
- repitiendo una sección de un bucle infinito en diferentes lugares de posibles fallas, sería mucho más correcto hacer un controlador (en este caso vacío);
- Bueno, todas las operaciones para configurar e iniciar una tarea pueden (y deberían) estar ocultas en una función o incluso en una macro.
Pero la inicialización del búfer de recepción está bien hecha: utilizamos memoria reservada previamente, no se manipula el montón, la cadena de llamadas es algo complicada, pero todo es legible.
Pnp: en la ventana de depuración, ante nuestros ojos, la pila de llamadas, todo se hace como debe y de manera sólida: respeto y respeto. Lo único sorprendente es que un intento de ocultar esta ventana conduce al final de la sesión de depuración.
Bueno, y una decisión más inesperada: establecer el número posible de objetos en la enumeración, para puertos serie y para esta placa de depuración igual a 1, en el estilo
typedef enum CC1310_LAUNCHXL_UARTName { CC1310_LAUNCHXL_UART0 = 0, CC1310_LAUNCHXL_UARTCOUNT } CC1310_LAUNCHXL_UARTName;
Dichas soluciones son estándar para transferencias reales, pero para la descripción de objetos de hardware, y no sabía que esto es posible, aunque funciona para mí. Hemos terminado la inicialización del hierro, sigamos adelante.
En una tarea en ejecución, observamos un clásico bucle infinito en el que la función lee los datos de un puerto serie
UART_read(uart, &input, 1);
e inmediatamente enviado de vuelta por función
UART_write(uart, &input, 1);
. Vayamos al primero y veamos un intento de leer caracteres del búfer de recepción
return (handle->fxnTablePtr->readPollingFxn(handle, buffer, size))
(¿Cómo odio esas cosas, pero en C es imposible de lo contrario?), profundizamos y nos encontramos en UARTCC26XX_read, y de ahí nos adentramos en la implementación del anillo de búfer, una función
RingBuf_get(&object->ringBuffer, &readIn)
. Aquí, la vida ordinaria entra en una fase aguda.
No me gustaba decir que no me gustaba este módulo en particular (el archivo ringbuf.c), estaba escrito de manera terrible y personalmente, habría avergonzado el lugar de una compañía tan respetada de autores de esta parte con vergüenza (todavía me puede llevar en su lugar, pero me temo que el nivel salarial de nuestros colegas indios no me conviene), pero probablemente no sé qué. Cuida tus manos:
1) el re-roll de punteros de lectura / escritura se implementa a través del resto de la división
object->tail = (object->tail + 1) % object->length;
y no hay optimizaciones del compilador cuando se realiza esta operación, como la superposición de una máscara de bits, ya que la longitud del búfer no es una constante. Sí, en este MK hay una operación de división de hardware y es bastante rápida (escribí sobre eso), pero aún así nunca toma 2 ciclos de reloj, como en la implementación correcta con re-roll honesto (y escribí sobre esto también),
Pnp: Recientemente vi una descripción de la nueva arquitectura M7 en la implementación y no recuerdo a nadie, así que por alguna razón, dividir 32 por 32 comenzó a realizarse en 2-12 ciclos en lugar de 2-7. O se trata de un error de traducción o ... Ni siquiera sé en qué pensar.
2) además, este fragmento de código se repite en más de un lugar: macros e inline para wimps, ctrl + C y ctrl + V, el principio DRY pasa por el bosque,
3) se implementó un contador completamente redundante de lugares de almacenamiento intermedio llenos, lo que conllevó el siguiente inconveniente,
4) secciones críticas tanto en lectura como en escritura. Bueno, todavía puedo creer que los autores de este módulo no leen mis publicaciones sobre Habré (aunque este comportamiento es inaceptable para los profesionales en el campo del firmware), pero deberían estar familiarizados con el Mustang Book, allí se examina este tema en detalle,
5) como una cereza en el pastel, se ha introducido un indicador del tamaño máximo del búfer, además, con un nombre muy difuso y una descripción completamente ausente (este último se aplica generalmente a todo el módulo). No excluyo que esta opción pueda ser útil para la depuración, pero ¿por qué arrastrarla a la versión? ¿Tenemos algún ciclo de procesador con la RAM?
6) al mismo tiempo, el procesamiento de desbordamiento del búfer está completamente ausente (hay una señalización de retorno de -1 sobre esta situación); incluso en Arduino lo es, dejaremos de lado la calidad de este procesamiento, pero su ausencia es aún peor. ¿O los autores se inspiraron en el hecho conocido de que cualquier suposición es cierta con respecto a un conjunto relativamente vacío, incluido el hecho de que no está vacío?
En general, mis comentarios son totalmente consistentes con la primera línea del desmotivador sobre el tema de revisión de código "10 líneas de código - 10 comentarios".
Por cierto, la penúltima de las deficiencias señaladas nos hace pensar en cosas más globales, pero ¿cómo implementamos la clase base para poder llevar a cabo su profunda modificación? Hacer que todos los campos sean seguros es una idea dudosa (aunque probablemente sea la única correcta), insertar una llamada de funciones amigables en los herederos es muy parecido a las muletas. Si en este caso en particular hay una respuesta simple a la pregunta de introducir un indicador de plenitud del búfer: una clase generada con escritura y lectura superpuestas y un contador adicional, entonces implementar la lectura sin avanzar el búfer (como en este caso) o reemplazar el último carácter colocado (vi tal implementación de búfer en anillo) no puede prescindir del acceso a los datos internos de la clase primaria.
Al mismo tiempo, no hay quejas sobre la implementación de la lectura real desde la interfaz en serie: la entrada está bloqueando, en ausencia de una cantidad suficiente de caracteres en el búfer de recepción, se amarta un semáforo y se transfiere el control al programador: todo se implementa de manera precisa y correcta. Personalmente, no me gusta controlar el equipo en un procedimiento de propósito general, pero esto reduce la anidación de los procedimientos y el índice de complejidad ciclomática, sin importar lo que signifique.
Ahora prestemos atención a la transmisión de los datos recibidos al canal serie, ya que al crear el objeto se le proporcionó solo un búfer de anillo: el receptor. De hecho, el búfer interno del hardware se utiliza para transmitir caracteres, y cuando se llena, se ingresa la espera de disponibilidad (al menos en el modo de operación de bloqueo). No puedo evitarlo, para no criticar el estilo de las funciones correspondientes: 1) por alguna razón, el objeto tiene un puntero generalizado, que dentro de la función se convierte constantemente en un puntero a los caracteres
*(unsigned char *)object->writeBuf);
2) la lógica del trabajo es completamente opaca y ligeramente confusa. Pero todo esto no es tan importante, ya que permanece oculto para el usuario y "no afecta la velocidad máxima".
En el proceso de investigación, nos encontramos con una característica más: no vemos el código fuente de algunas funciones internas en modo de depuración, esto se debe a un cambio de nombres para diferentes opciones de compilación (ROM / NO_ROM). Reemplace el archivo fuente requerido (C: \ Jenkins \ jobs \ FWGroup-DriverLib \ workspace \ modules \ output \ cc13xx_cha_2_0_ext \ driverlib \ bin \ ccs /./../../../ driverlib / uart.c--) Fallé (pero realmente no lo intenté), aunque encontré la fuente (por supuesto, en el archivo en el archivo uart.c, gracias, capitán), afortunadamente, este fragmento es simple y es fácil identificar el código del ensamblador con el código fuente en C (especialmente si conoce las características del equipo de ITxxx). No sé cómo resolver este problema para bibliotecas con funciones complejas, pensaremos cuando surja la necesidad.
Y finalmente, una pequeña observación: estoy listo para creer que el hardware de la implementación del canal en serie para los modelos MK CC13x0 es el mismo que para CC26x0, y la duplicación del contenido de un archivo llamado UARTCC26XX.c --- no se puede llamar la solución correcta, pero crear un archivo de definición intermedia con inclusión Agradecería el archivo fuente, anulando las funciones y el comentario correspondiente, ya que esto haría que el programa sea más comprensible, y esto siempre debería ser bienvenido, vout.
Entonces, el caso de prueba funciona, aprendimos mucho sobre la estructura interna de las bibliotecas estándar, notamos sus fortalezas y lados no tan buenos, en conclusión de la revisión trataremos de encontrar la respuesta a la pregunta que al programador generalmente le importa en el dilema "OS o no OS" - tiempo de cambio de contexto. Aquí son posibles dos formas: 1) la consideración del código fuente es más bien una forma teórica, requiere un nivel de inmersión en el tema que no estoy listo para demostrar, y 2) un experimento práctico. Por supuesto, el segundo método, a diferencia del primero, no da resultados absolutamente correctos, pero "la verdad siempre es concreta" y los datos obtenidos pueden considerarse adecuados si las mediciones se organizan correctamente.
Para comenzar, para estimar el tiempo de cambio, necesitamos aprender a evaluar el tiempo de ejecución general de varios fragmentos de programa. En esta arquitectura, hay un módulo de depuración, parte del cual es un contador del sistema. La información sobre este módulo es bastante accesible, pero el demonio, como siempre, se esconde en los detalles. Primero, intentemos configurar el modo necesario con los controladores directamente a través del acceso a los registros. Encontramos rápidamente el bloque de registro CPU_DWT y en él encontramos tanto el contador CYCCNT como el registro de control CTRL con el bit CYCCNTENA. Naturalmente, o, como dicen, por supuesto, no pasó nada y el sitio web de ARM tiene una respuesta a la pregunta de por qué: es necesario habilitar el módulo de depuración con el bit TRCENA en el registro DEMCR. Pero el último registro no es tan simple: en el bloque DWT no está allí, en otros bloques es lento para buscar, son bastante largos, pero no encontré ninguna búsqueda por nombre en la ventana del registro (pero sería bueno tenerlo). Entramos en la ventana de memoria, ingresamos la dirección del registro (lo conocemos desde la fecha) (por cierto, por alguna razón el formato hexadecimal de la dirección no es el predeterminado, debe agregar el prefijo 0x con plumas) y, de repente, vemos una celda de memoria con el nombre CPU_CSC_DEMCR. Es divertido, por decir lo menos, por qué la compañía cambió el nombre de los registros en comparación con los nombres propuestos por el licenciante de la arquitectura, probablemente fue necesario. Y seguro, en el bloque de registros CPU_CSC encontramos nuestro registro, establecemos el bit deseado en él, regresamos al contador, lo habilitamos y todo funcionó.
Pnp: todavía hay una búsqueda por nombre, la combinación Ctrl-F lo llama (naturalmente), solo existe en el menú contextual, pero en el habitual está cancelado, pido disculpas a los desarrolladores.
Inmediatamente noto otro inconveniente de la ventana de memoria: la impresión del contenido se interrumpe al indicar las celdas con nombre, lo que hace que la salida se rompa y no se segmente en higos en 16 (8.32, 64, sustituya las palabras necesarias). Además, el formato de salida cambia cuando se cambia el tamaño de la ventana. Tal vez todo esto se pueda configurar según las necesidades del usuario, pero, según mi propia experiencia (y de qué otra cosa debería proceder), declaro que configurar el formato de salida de la ventana de visualización de memoria no se aplica a soluciones intuitivamente obvias. Estoy totalmente a favor de habilitar una función tan conveniente como mostrar áreas de memoria con nombre en la ventana de visualización, de lo contrario, muchos usuarios nunca lo sabrían, pero también se debe tener cuidado con aquellos que desean deshabilitarla conscientemente.
Por cierto, no abandonaría por completo la posibilidad de crear macros (o scripts) para trabajar con el entorno, porque tenía que hacer esta configuración de registro (para permitir la medición del tiempo) cada vez después de restablecer el MK, ya que considero la corrección del código insertando manipulaciones de registro para fines de depuración No muy correcto. Pero, aunque nunca encontré macros, el trabajo con registros puede simplificarse en gran medida debido al hecho de que los registros individuales (necesarios) pueden incluirse en la ventana de expresión y, por lo tanto, facilitan y aceleran significativamente el trabajo con ellos.
Para enfatizar que el sentimiento del ingeniero por la familia MK no se ha enfriado (de lo contrario, reprendo diferentes aspectos del entorno de desarrollo), noto que el contador funciona bien: no pude encontrar ningún ciclo adicional en ninguno de los modos de depuración, pero antes de que esto sucediera ser, al menos en la serie MK, desarrollada por LuminaryMicro.
Por lo tanto, describimos el plan de experimento para determinar el tiempo de cambio de contexto: cree una segunda tarea que incremente un cierto contador interno (en un bucle infinito), inicie el MC durante un cierto tiempo, encuentre la relación entre el contador del sistema y el contador de tareas. Luego, inicie el MK por un tiempo similar (no necesariamente el mismo) e ingrese 10 caracteres a un ritmo aproximadamente una vez por segundo. Se puede esperar que esto resulte en 10 cambios a la tarea de eco y 10 cambios a la tarea de contador. Sí, estos cambios de contexto se realizarán no de acuerdo con el temporizador del planificador, sino de acuerdo con el evento, pero esto no debería afectar el tiempo total de ejecución de la función investigada, por lo que comenzamos a implementar el plan, crear la tarea de contador y comenzarla.
Aquí encontramos una característica de RTOS, al menos en la configuración estándar: no se desplaza "de verdad": si la tarea prioritaria está constantemente lista para su ejecución (y la tarea de contador es esa) y no le da control al programador (no espera señales, no se duerme, no bloqueado por banderas, etc.), entonces no se ejecutará una sola tarea de menor prioridad a partir de la palabra. Esto no es Linux, en el que se utilizan varios métodos para garantizar que todos obtengan un quantum, "para que nadie se sienta ofendido". Este comportamiento es bastante esperado, muchos RTOS ligeros se comportan de esta manera, pero el problema resulta ser más profundo, ya que la administración no recibe tareas de igual prioridad que una constantemente preparada. Es por eso que en este ejemplo, puse la tarea de eco, que está en espera, la prioridad es mayor que la tarea de contador constantemente lista, de lo contrario, esta última capturará todos los recursos del procesador a tiempo.
Comenzamos el experimento, la primera parte (solo esperando el tiempo de ejecución) proporcionó los datos sobre la relación de los contadores 406181 / 58015 = 7, es bastante esperado. La segunda parte (con 10 caracteres consecutivos durante ~ 10 segundos) da los resultados 351234k-50167k * 7 = 63k / 20 = 3160 ciclos, el último dígito es el tiempo asociado con el procedimiento de cambio de contexto en ciclos MK. Personalmente, este valor me parece algo mayor de lo esperado, seguimos investigando, parece que todavía hay algunas acciones que estropean las estadísticas.
PNP: un error común de un experimentador es no evaluar los resultados previstos anteriormente y creer en la basura recibida (hola a 737 desarrolladores).
Es obvio ("sí, bastante obvio") que el resultado, además de la conmutación de contexto real, también contiene el tiempo necesario para realizar las operaciones de lectura de un carácter desde el búfer y enviarlo al puerto serie. Menos obvio es que también tiene el tiempo necesario para procesar una interrupción cuando se recibe y colocar el carácter en el búfer de recepción). ¿Cómo podemos separar a un gato de la carne? Para esto tenemos un truco complicado, detenemos el programa, ingresamos 10 caracteres y lo iniciamos. Podemos esperar (deberíamos mirar la fuente) que se produzca una interrupción en la recepción solo 1 vez e inmediatamente se enviarán todos los caracteres desde el búfer de recepción al anillo, lo que significa que veremos menos sobrecarga. También es fácil determinar el tiempo de entrega al puerto serie: sacaremos cada segundo carácter y resolveremos las 2 ecuaciones lineales resultantes con 2 desconocidas. Y es posible e incluso más simple: no deducir nada, lo cual hice.
Y aquí están los resultados de manipulaciones tan complicadas: hacemos la entrada por paquete y los ticks faltantes se hacen más pequeños - 2282, apagamos la salida y los costos caen a 1222 ticks - es mejor, aunque esperaba 300 ticks.
Pero con el tiempo de lectura, no se puede encontrar nada como esto; se escala simultáneamente con el tiempo de cambio de contexto deseado. Lo único que puedo ofrecer es apagar el temporizador interno al comienzo de ingresar el carácter recibido y volver a encenderlo antes de ingresar a la espera del siguiente. Luego, dos contadores funcionarán sincrónicamente (con la excepción del cambio) y se puede determinar fácilmente. Pero este enfoque requiere una implementación profunda de los programas del sistema en los textos, y aún permanecerá el componente del manejo de interrupciones. Por lo tanto, propongo limitarme a los datos ya obtenidos, lo que nos permite afirmar firmemente que el tiempo de cambio de tareas en el TI-RTOS en consideración no excede los 1222 ciclos de reloj, que para una frecuencia de reloj dada es de 30 microsegundos.
PNP: de todos modos, mucho: conté ciclos a las 100:30 para guardar el contexto, 40 para determinar la tarea finalizada y 30 para restaurar el contexto, pero obtenemos un orden de magnitud más. Aunque la optimización se ha desactivado ahora, active –o2 y vea el resultado: no ha cambiado mucho, se ha convertido en 2894 en lugar de 3160.
Hay otra idea: si el sistema operativo admite el cambio de tareas de igual a igual, entonces podría ejecutar dos tareas con contadores, obtener mágicamente datos sobre el número de conmutadores en un momento y calcular la pérdida del contador del sistema, pero debido a las peculiaridades del programador, sobre el cual Ya dicho, este enfoque no conducirá al éxito. Si bien es posible otra opción: hacer ping-pong entre dos tareas de igual a igual (o incluso de igual a igual) a través de un semáforo, es fácil calcular la cantidad de cambios de contexto aquí; tendrá que intentarlo, pero será mañana.
Esta vez, la encuesta tradicional al final de la publicación estará dedicada no al nivel de presentación (es claro para cualquier lector imparcial que está más allá de cualquier elogio y excede cualquier expectativa), sino al tema de la próxima publicación.