Full text
Proyecto Fin de Carrera Ingeniería de Telecomunicación Formato de Publicación de la Escuela Técnica Superior de Ingeniería Autor: F. Javier Payán Somet Tutor: Juan José Murillo Fuentes Dep. Teoría de la Señal y Comunicaciones Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2013 Trabajo Fin de Grado Grado en Ingeniería Electrónica, Robótica y Mecatrónica Desarrollo de aplicaciones basadas en aprendizaje automático en sistemas empotrados STM32 Autor: Juan Manuel Cuerva Gutiérrez Tutor: Antonio Luque Estepa Dpto. Ingeniería Electrónica Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2021
Trabajo Fin de Grado Grado en Ingeniería Electrónica, Robótica y Mecatrónica Desarrollo de aplicaciones basadas en aprendizaje automático en sistemas empotrados STM32 Autor: Juan Manuel Cuerva Gutiérrez Tutor: Antonio Luque Estepa Profesor Titular Dpto. Ingeniería Electrónica Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2021
Trabajo Fin de Grado: Desarrollo de aplicaciones basadas en aprendizaje automático en sistemas empotrados STM32 Autor: Juan Manuel Cuerva Gutiérrez Tutor: Antonio Luque Estepa El tribunal nombrado para juzgar el trabajo arriba indicado, compuesto por los siguientes profesores: Presidente: Vocal/es: Secretario: acuerdan otorgarle la calificación de: El Secretario del Tribunal Fecha:
Agradecimientos En primer lugar, me gustaría agradecer a mi tutor, el Doctor Antonio Luque Estepa, por aportar su experiencia, su visión y sus inestimables consejos durante todo el proceso de elaboración de este proyecto. Ojalá más profesores como usted. A mis padres, por darme la oportunidad de estudiar esta carrera en el ámbito de la Robótica, que tanto me apasiona desde pequeño. A mi novia, Elena, por su incombustible apoyo en los momentos más duros a lo largo de estos años. Y, por último, agradecer a la World Wide Web y a todos aquellos que brindan su ayuda y comparten su conocimiento de forma de libre. Juan Manuel Cuerva Gutiérrez Sevilla, 2021 I
Índice Abreviado Índice Abreviado III 1 Introducción Y Objetivos 1 1.1 Introducción 1 1.2 Objetivos 4 2 Desarrollos existentes 5 2.1 TensorFlow 5 2.2 Edge Impulse 6 2.3 MCUNet 8 2.4 Herramientas de STMicroelectonics 10 2.5 Red neuronal de ST 11 2.6 Hardware utilizado 13 2.7 Conexión entre la placa Núcleo-64 STM32F401RE y la tarjeta X-NUCLEO- IKS01A1 15 3 Diseño e implementación 21 3.1 Consideraciones previas 21 3.2 Diseño del driver para el acelerómetro LSM6DS0 21 3.3 Diseño del framework de la red neuronal 31 3.4 Sistema completo 36 4 Pruebas 47 4.1 Pruebas parciales 47 4.2 Demo del sistema completo 62 5 Conclusiones y desarrollos futuros 69 5.1 Conclusión de objetivos 69 5.2 Desarrollos futuros 69 Apéndice A Configuración del entorno y posibles problemas 71 A.1 Configuración de STM32CubeMX 71 A.2 Posibles problemas 81 Apéndice B Códigos principales 87 III
4Capítulo 1. Introducción Y Objetivos de bajas prestaciones en comparación a los usados habitualmente con esta tecnología. Además, se engloba también el desarrollo de los drivers para los sensores que se usarán en este proyecto. 1.2 Objetivos En este proyecto se pretende, partiendo de herramientas ya existentes, realizar el desarrollo de un framework que permita aplicar técnicas de aprendizaje automático en un sistema embebido de capacidades limitas así como crear un driver propio para la interacción de la placa de desarrollo con periféricos que habiliten la posibilidad de capturar datos del entorno. Además se estudiarán las dificultades que ésto presenta. Como punto de partida, se utilizará la placa de desarrollo Núcleo-64 STM32F401RE [ 15 ] y el módulo ambiental X-NUCLEO-IKS01A1 [ 33 ] y se creará un driver para poder hacer uso del acelerómetro integrado. Una vez se puedan obtener medidas, se empleará una red neuronal preentrenada que, mediante la herramienta STM32CubeMX [ 19 ], se transformará para poder ser interpretada por la placa de desarrollo y, con STM32CubeIDE [ 18 ], se creará un framkework que permita al microcontrolador realizar inferencias así como hacer uso de una o varias redes neuronales al mismo tiempo. La aplicación de ejemplo utilizará una red neuronal proporcionada por STMicroelectronic que permite la clasificación de actividad humana. Para concluir, se modificará dicha red para reconocer un movimiento adicional y se presentarán las posibles líneas futuras de desarrollo del proyecto.
2 Desarrollos existentes El placer más noble es el júbilo de comprender - Leonardo da Vinci - En este capítulo se mostrará el panorama actual de las tecnologías existentes para la implementación de aplicaciones de inteligencia artificial en dispositivos embebidos de bajo coste y se presentarán las herramientas elegidas para el desarrollo de este proyecto. 2.1 TensorFlow TensorFlow es la apuesta clave de Google para crear un ecosistema de Machine Learning end-to-end que ayude a potenciar la revolución del Deep Learning. Según su propia página, TensorFlow [ 5 ] es una plataforma de código abierto de extremo a extremo para el aprendizaje automático que cuenta con un ecosistema integral y flexible de herramientas, bibliotecas y recursos que permite tanto a investigadores como desarrolladores compilar e implementar con facilidad aplicaciones basadas en redes neuronales. TensorFlow cuenta, a parte de su biblioteca principal, con TensorFlow.js, una biblioteca de JavaScript para entrenar e implementar modelos en el navegador y en Node.js, TensorFlow Lite para implementar modelos en dispositivos integrados y móviles y TensorFlow Extended, una plataforma de extremo a extremo para la preparación de datos, el entrenamiento, la validación e implementación de modelos en entornos de gran producción. Figura 2.1 Logo de TensorFlow. Una de las virtudes de Tensorflow es su arquitectura simple, que permite la capacidad de compilar y entrenar modelos de apredizaje automático de forma fácil haciéndo uso de APIs intuitivas y de 5
6Capítulo 2. Desarrollos existentes alto nivel como Keras [ 12 ], con ejecución inmediata, rápido prototipado e iteración de modelos inmediata y con depuración sencilla. Además, cabe destacar su flexibilidad, ya que permite entrenar e implementar con facilidad modelos en la nube, locales, en el navegador o en el propio dispositivo empleando lenguajes como Python, JavaScript o Swift. En sus tres años de historia ha acumulando más de 41 millones de descargas y es el proyecto con mayor número de contribuciones en Github año tras año, con más de 1.800 contribuciones [ 16 ] y con decenas de ejemplos de uso en distintas plataformas. 2.2 Edge Impulse Edge Impulse [ 10 ] es una plataforma de desarrollo que permite la aplicación de machine learning en sistemas embebidos. Fundada en 2019, ha conseguido posicionarse como una de las líderes del sector en la actualidad, con más de 5,4 millones de dólares en financiación [ 6 ] y usada por más de 1000 empresas a través de más de 11000 proyectos de ML (Machine Learning) en todo el mundo. Se especializa en aplicaciones industriales y profesionales que incluyen mantenimiento predictivo, seguimiento y monitoreo de activos y detección de actividades en humanos y animales. La gran ventaja de Edge Impulse es que proporciona herramientas para dar soporte al usuario en todo el proceso de desarrollo de ML (Figura 2.2), acelerando el tiempo de desarrollo de aplicaciones de años a semanas, habilitando modelos altamente optimizados que se pueden implementar en una amplia gama de hardware, desde MCUs hasta CPUs. Figura 2.2 Ciclo de trabajo en Edge Impulse. Para ello, gran parte de sus herramientas se centran en la manipulación de los conjuntos de datos: adquisición de datos (Figura 2.3), visualización (Figura 2.4), análisis (Figura 2.5), procesamiento y filtrado y creación de datasets entre otros. También posee una biblioteca de algoritmos extensibles y personalizables y un proceso de aplicación de datos a dichos algoritmos de forma automatizada. Esto incluye una potente infraestructura para la creación y entrenamiento de redes neuronales (Figura 2.6) que permite al usuario tener el control de cada parte del proceso y visualizar que está
2.2 Edge Impulse 7 sucediendo en cada momento. Su implementación se centra principalmente en microcontroladores y CPUs de gama baja. Figura 2.3 Adquisición de datos con Edge Impulse. Figura 2.4 Visualización de los conjutos de datos con Edge Impulse. Con Edge Impulse, se puede recopilar datos, crear una red, entrenarla y realizar el despligue de la aplicación en cualquier dispositivo. Para ello, permite crear librerías en C++ sin dependencias externas, librerías para Arduino con ejemplos que pueden ejecutarse en la gran mayoría de placas basadas en Arm, crear un CMSIS-PACK como una librería de C++, el cual se puede instalar en la mayoría de MCUs de ST haciendo uso de STM32CUBE.AI y crear un paquete de WebAssembly, permitiendo la creación directa de firmware para una gran variedad de placas de desarrollo. Edge Impulse opera con un modelo de negocio típico de "Software como Servicio" (SaaS). Posee un nivel gratuito para ingenieros individuales que permite realizar todo el flujo de desarrollo (desde la captura y procesamiento de datos hasta el entrenamiento y salida a producción) y ofrece servicios premium a empresas mediante una suscripción, que incluye funciones para la colaboración entre
8Capítulo 2. Desarrollos existentes Figura 2.5 Análisis de los datos con Edge Impulse. Figura 2.6 Creación de una red neuronal con Edge Impulse. equipos de ingenieros, trabajar con conjuntos de datos más grandes, control de versiones y mayores opciones de seguridad. 2.3 MCUNet Cabe destacar en este apartado MCUNet, un sistema capaz de diseñar redes neuronales compactas que se adaptan a las necesidades de los dispositivos integrados a la red IoT, ofreciendo máxima velocidad y precisión a pesar de las limitaciones en cuanto a memoria o potencia de procesamiento que suelen presentar estos dispositivos. Este sistema fue diseñado por Ji Lin como autor principal y Wei-Ming Chen, Yujun Lin, John Cohn, Chuang Gan y Song Han como co-autores, del Instituto Tecnológico de Massachusett, la Universidad Nacional de Taiwan y el laboratorio de AI MITIBM Watson [13] y fue presentado en la "Conferencia Anual sobre Sistemas de Procesamiento de Información Neuronal" en diciembre de 2020.
2.3 MCUNet 9 MCNet consiste en el uso combinado de dos tecnologías desarrolladas por el MIT; TinyNAS y TinyEngine (Figura 2.8). TinyNAS es un algoritmo de búsqueda de arquitectura neuronal capaz de manejar diversas restricciones automáticamente (tipo de dispositivo, latencia, energía y memoria) y crear redes de tamaño personalizado para cada tipo de microprocesador (Figura 2.7). TinyEngine sirve como motor de inferencia encargado de dirigir la gestión de los recursos a través de un código esencial, que incorpora los requisitos básicos necesarios para su funcionamiento, adaptando la programación de la memoria de acuerdo con la topología general de la red en lugar de la optimización por capas, lo que reduce el tiempo de compilación, reduce el uso de memoria en 3.4x y acelera la inferencia en 1.7-3.3x en comparación con el uso de TF-Lite Micro y CMSIS-NN (Figura 2.9). Figura 2.7 Comparación del uso de la memoria con MCUNet. Imagen extraida de [13]. Figura 2.8 Sistema de trabajo del algortimo de MCUNet. Imagen extraida de [13]. Figura 2.9 Comparación del uso de la memoria y el tiempo de inferencia entre otros frameworks actuales y MCUNet. Imagen extraida de [13].
10 Capítulo 2. Desarrollos existentes 2.4 Herramientas de STMicroelectonics De forma cada vez más frecuente, los fabricantes de microcontroladores ofrecen como parte de su software un IDE que permite, de forma sencilla, programar y hacer debug de sus microcontroladores. En concreto, la empresa STMicroelectronics proporciona un ecosistema de desarrollo conocido como STM32Cube ecosystem, el cual ofrece una solución software completa para microcontroladores y microprocesadores de STM32. STM32Cube es una combinación de herramientas software y librerías integradas que cubren todas las necesidades de un ciclo completo de desarrollo de proyectos. Como se comentará más adelante en la sección 2.6, al hacer uso de una placa de ST, éste será el ecosistema de desarrollo que se utilizará en este proyecto. Figura 2.10 Ecosistema de desarrollo de software STM32Cube. Principalmente, se van a usar dos herramientas de este ecosistema: el programa de configuración STM32CubeMX y el entorno de desarrollo STM32CubeIDE. 2.4.1 STM32CubeMX STM32CubeMX es una herramienta gráfica que permite una fácil configuración de los microcontroladores y microprocesadores de STM32, así como la generación del código C necesario para la inicialización de éstos en los núcleos Arm ® Cortex ® -M y para la generación del código fuente para árboles de dispositivos en Linux en los núcleos Cortex-A, a través de procesos paso a paso. Algunas de las características que hacen de STM32CubeMX un software gran utilidad para trabajar con microcontroladores de STMicroelectronics son las siguientes: •Fácil selección de microcontroladores, cubriendo toda la cartera de STM32. •Selección de placas de una lista de placas de STMicroelectronics.
2.5 Red neuronal de ST 11 • Configuración sencilla de microntroladores (pines, reloj, periféricos, middleware) y generación de su correspondiente código de inicialización en lenguaje C. •Generación de informes de configuración • Generación de proyectos listos para importar a un IDE para una selección de de entornos de desarrollos integrados. Todos los proyectos de STM32CubeMX incluyen código C de inicialización, drivers HAL para STM32, middleware para la configuración del usuario y todos los archivos necesarios para abrir y construir el proyecto en el IDE seleccionado. •Cálculo del consumo de energía para aplicaciones definidas por el usuario. •Auto-actualizaciones que permiten al usuario mantener actualizado el STM32CubeMX. El ciclo de configuración dentro de STM32CubeMX se puede observar en la Figura 2.12. Figura 2.11 Ciclo de uso de la herramienta STM32CubeMX. 2.4.2 STM32CubeIDE STM32CubeIDE es una avanzada plataforma de desarrollo para C/C++ que incluye configuración de periféricos, generación de código, compilación y debug para los microcontroladores y microprocesadores de la familia STM32. Está basado en el framework Eclipse ® /CDT y GCC como toolchain, y GDB para realizar debugs. Por ello, permite la integración con los cientos de plugins existentes que completan las características del IDE de Eclipse®. Una de las principales ventajas de STM32CubeIDE es que permite integrar la configuración de STM32 y las funcionalidades de creación de proyectos de STM32CubeMX, lo que ofrece una experiencia de herramienta todo en uno y permite ahorrar tiempo de instalación y desarrollo. 2.5 Red neuronal de ST En este apartado se presentará la red neuronal de ST que se va a utilizar como base en este proyecto. Mas adelante, en la sección 4.1.2, se comentarán los cambios realizados a dicha red. Esta red puede ser descargada desde el Github de STMicroelectronics [ 1 ], directamente en formato ".h5" lista para su implementación con STM32CubeMX. Para realizar modificaciones en dicha red y ver su estructura, se debe acceder a [ 20 ] donde se hace uso de la herramienta de Google Research
12 Capítulo 2. Desarrollos existentes Figura 2.12 Plataforma de desarrollo STM32CubeIDE. Colaboratory [ 2 ], un servicio cloud basado en notebooks de Jupyter, que permite el uso gratuito de las GPUs y TPUs de Google, no requiere configuración y permite compartir contenido fácilmente. La red distingue entre tres movimientos; estacionario, caminando y corriendo. Para ello, hace uso de un dataset propio, el cual también se puede encontrar en el Github de STMicroelectronics [ 1 ]. La estructura de este dataset será tratada en detalle en el apartado 4.1.2. En la Figura 2.13 se pueden ver algunas gráficas de estos movimientos. Figura 2.13 Gráficas de los movimientos contenidos en el dataset de la red neuronal de ST.
2.6 Hardware utilizado 13 La red está construida sobre un modelo secuencial de keras (tf.keras.models.Sequential) donde se apilan seis capas, cada una de ellas con un tensor de entrada y un tensor de salida. En concreto, su estructura es la siguiente: Código 2.1 Fragmento del archivo main.c. ## Conv1D based model model = tf.keras.models.Sequential([ tf.keras.layers.Conv1D(filters=16, kernel_size=3, activation=’relu’, input_shape=(26, 3)), tf.keras.layers.Conv1D(filters=8, kernel_size=3, activation=’relu’), tf.keras.layers.Dropout(0.5), tf.keras.layers.Flatten(), tf.keras.layers.Dense(64, activation=’relu’), tf.keras.layers.Dense(3, activation=’softmax’) ]) model.compile(optimizer=’adam’, loss=’sparse_categorical_crossentropy’, metrics=[’accuracy’]) model.fit(x_train, y_train, epochs=30) test_loss, test_acc = model.evaluate(x_test, y_test, verbose=2) print("Test loss:", test_loss) print("Test acc:", test_acc) model.summary() Las dos primeras capas son de tipo Conv1D, es decir, crean un núcleo de convolución (kernel) que convolucionan con la entrada de la capa para producir un tensor de salida. Como función de activación utilizan una ReLu, función de tipo no lineal de las más utilizadas debido a que permite un aprendizaje muy rápido en las redes neuronales. Después existe una capa de Dropout, una técnica de regularización que tiene como objetivo reducir el sobreajuste (overfitting) omitiéndo aleatoriamente neuronas, tanto ocultas como visibles, durante el proceso de entrenamiento de la red. A continuación, la siguiente capa (Flatten) convierte los datos multidimensionales en un vector de características 1D. Dicho vector es la entrada de la siguiente capa, de tipo Dense, en la que cada neurona recibe información de todas las neuronas de su capa anterior. Por último, la capa de salida, también de tipo Dense, tiene 3 neuronas (tres posibles tipos de salida) y tiene como función de activación Softmax, la cual asigna probabilidades decimales a cada clase hasta sumar 1.0. Cabe destacar que esta red hace uso de TensorFlow 1.15 ya que TensorFlow 2.2 no es soportado aún por X-CUBE-AI. 2.6 Hardware utilizado El hardware del cual se ha hecho uso en este proyecto ha sido facilitado por el Dr. Antonio Luque Estepa y consite en una placa de desarrollo Núcleo-64 STM32F401RE y una tarjeta de expansión MEMS con sensores ambientales y de movimiento X-NUCLEO-IKS01A1. 2.6.1 Placa de desarrollo: Núcleo-64 STM32F401RE La placa de desarrollo Núcleo-64 STM32F401RE proporciona una forma fácil y asequible de desarrollar nuevos conceptos y construir prototipos aprovechando las características de rendimiento
20 Capítulo 2. Desarrollos existentes Figura 2.19 Conexión entre la placa Núcleo-64 STM32F401RE y la tarjeta X-NUCLEO-IKS01A1.
3 Diseño e implementación Los científicos estudian el mundo tal como es; los ingenieros crean el mundo que nunca ha sido - Theodore Von Karman - En este capítulo se presentarán los diseños realizados, las implementaciones llevadas a cabo y se realizará un estudio de la problemática enfrentada como presentación de la necesidad de los desarrollos que se abarcan en este TFG. 3.1 Consideraciones previas Para utilizar los diseños, primero se debe configurar correctamente el entorno. Ésto implica configurar el I2C , la USART, instalar el paquete de expansión de Inteligencia Artificial X-CUBE-AI [ 25 ] y cargar una red-preentranada para generar los archivos sobre los cuales se apoya la programación. Si resulta de interés al lector, todo ello será detalladamente tratado en el anexo A. 3.2 Diseño del driver para el acelerómetro LSM6DS0 Si bien es cierto que STM32CubeMX ofrece un paquete de expansión que incluye drivers para reconocer sensores de temperatura, humedad, presión y movimiento (el paquete X-CUBE-MEMS1 [ 27 ]), éste solo ofrece drivers para las tarjetas X-NUCLEO-IKS01A2, X-NUCLEO-IKS01A3 y X-NUCLEO-IKS02A1, pero no para la tarjeta X-NUCLEO-IKS01A1. Si se busca dicha tarjeta en la página de ST [ 33 ], ésta aparece marcada como "NRND" (Not Recommended for New Dessings) y se indica explícitamente que se encuentra sin soporte. Realizando una búsquedad dentro de la página de ST, se puede comprobar que la última versión del paquete X-CUBE-MEMS1 que soportaba la placa X-NUCLEO-IKS01A1 era la versión 4.4.1. A la hora de trabajar en este TFG, la versión actual de STM32CubeMX es la 6.1.0, mientras que en el caso del paquete de expansión X-CUBE-MEMS1 la versión es la 8.2.0. Desde ST no se permite la instalación de software legacy por lo que, en STM32CubeMX, no se puede instalar ninguna versión anterior de ningún paquete de expansión ni se da acceso a descargas de versiones anteriores de dicho programa. Como consecuencia, hacer uso de los drivers propios de ST para la placa X-NUCLEO-IKS01A1 en las versiones actuales resulta imposible. Como solución, se propuso la creación de proyectos para las tarjetas X-NUCLEO-IKS01A2, X-NUCLEO-IKS01A3 y X-NUCLEO-IKS02A1, con ligeras modificaciones, aunque no se obtuvieron resultados positivos ya que cada una de estas placas cuenta con un acelerómetro distinto 1 , con 1LSM6DSL, LSM6DSO y LSM330DHCX, respectivamente 21
22 Capítulo 3. Diseño e implementación distintos registros y distintas funciones. Nótese aquí la importancia de distinguir entre los acelerómetros LSM6DS0 (terminado en el número 0, el cual se utiliza en este proyecto) y el LSM6DSO (terminado en la letra O), pues poseen características diferentes y operan de forma distinta, lo que puede inducir a error de forma sencilla. De hecho, también se trató de utilizar de forma aislada el acelerómetro LSM6DSO (terminado en la letra O), el cual sí tiene soporte dentro de STM32CubeMX, tal y como se puede ver en la Figura 3.1. En este camino también se invirtió bastante tiempo y resultó igualmente infructuoso, pues se tratan de acelerómetros diferentes pero, en un principio, se pensó que este acelerómetro era el buscado y no otro distinto debido al gran parecido entre ambos nombres. Figura 3.1 Selección del acelerómetro LSM6DSO (letra O), de forma aislada, tal y como aparece en STM32CubeMX. Si bien es cierto, desde la propia página de ST se puede descargar el paquete X-CUBE-MEMS1- V4 [ 29 ], también NRND, específico para la tarjeta X-NUCLEO-IKS01A1. El paquete en cuestión contiene ejemplos de uso de dicha tarjeta pero, y aunque pueden importarse a STM32CubeIDE, no pueden modificarse ya que este paquete no puede ser instalado en STM32CubeMX y no existe por tanto, en el directorio del proyecto, el archivo .io necesario para instalar el paquete de X-CUBE-AI, cargar la red neuronal y poder realizar aplicaciones de Inteligencia Artificial. Por otra parte, también se intentó trabajar desde un proyecto vacío e importar los archivos necesarios para hacer funcionar únicamente el acelerómetro lo cual tampoco acabo resultando factible principalmente por tres motivos: 1. La gran cantidad de dependencias entre los ficheros, las cuales hacían imposible compilar el proyecto. 2. Debido al bug controlado de STM32CubeIDE que impide importar dependencias al importar proyectos al workbench. 3. El cambio en el sistema de funciones BSP a partir de la versión 4.4.1. BSP es una capa de abstracción que se compone de controladores específicos para la placa de desarrollo y que, en su mayoría, contiene drivers para memorias externas (como SDRAM o Flash QSPI), soporte
3.2 Diseño del driver para el acelerómetro LSM6DS0 23 para pantallas lcd, pantallas táctiles, acelerómetros, etc. Este cambio en el ecosistema de funciones aumentó la complejidad a la hora de trazar las similitudes entre versiones. Por último y como opción desarrollada en este TFG, se pensó que la mejor forma de solventar el problema era creando directamente drivers propios que permitieran hacer uso de los sensores necesarios de la placa X-NUCLEO-IKS01A1 de forma sencilla y que, una vez desarrollados, permitieran al usuario una utilización rápida y eficaz, sin invertir tiempo arreglando dependencias ni tocando valores de registros internos. Por tanto, para abordar el diseño del driver, en primer lugar se creó un programa destinado a establecer una comunicación sencilla, con manjeador de errores, a través de I2C entre el acelerómetro y el microcontrolador. Dicho código se presenta a continuación: Código 3.1 Comunicación a través de I2C. I2C_HandleTypeDef hi2c1; HAL_StatusTypeDef ret; uint8_t buf[13]; while(1){ buf[0]=REG_ACC; ret = HAL_I2C_Master_Transmit(&hi2c1, LSM6DS0_ADDR, buf, 1, HAL_MAX_DELAY); if(ret != HAL_OK){ strcpy((char*)buf,"Error SCL!!\r\n"); } else { ret = HAL_I2C_Master_Receive(&hi2c1, LSM6DS0_ADDR, buf, 2, HAL_MAX_DELAY); if(ret != HAL_OK){ strcpy((char*)buf,"Error SDA!!\r\n"); } else { strcpy((char*)buf,"Todo OK!!\r\n"); } HAL_UART_Transmit(&huart2, buf,strlen((char *)buf), HAL_MAX_DELAY); } Donde se hace uso de la función: Código 3.2 Función HAL_StatusTypeDef HAL_I2C_Master_Transmit. HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout) Donde: •hi2c : Puntero a una instancia de la estructura I2C_HandleTypeDef , definida en stm32f4xx_- hal_i2c.h. Contiene la información de configuración para el I2Cespecificado. •DevAddress : Dirección del dispositivo esclavo. Tal y como se especifica en su datasheet [ 26 ], la dirección del acelerómetro es 0xD6. •pData: Puntero al array buffer que contiene la secuencia de bytes a transmitir. •Size: Cantidad de datos a enviar.
24 Capítulo 3. Diseño e implementación •Timeout: Indica el tiempo máximo, en milisegundos, antes de finalizar la transmisión. El resultado de esta función es de tipo HAL_StatusTypeDef y puede ser: HAL_OK, HAL_ERROR, HAL_BUSY, HAL_TIMEOUT 2 . Si el resultado es distinto de HAL_OK, se copia un mensaje de error en el buffer. En caso contrario, se realiza la llamada a la función: Código 3.3 Función HAL_StatusTypeDef HAL_I2C_Master_Receive. HAL_StatusTypeDef HAL_I2C_Master_Receiven(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout ) Donde: •hi2c : Puntero a una instancia de la estructura I2C_HandleTypeDef , definida en stm32f4xx_- hal_i2c.h. Contiene la información de configuración para el I2Cespecificado. •DevAddress : Dirección del dispositivo. Tal y como se especifica en su datasheet [ 26 ], la dirección del acelerómetro es 0xD6. •pData: Puntero al array buffer que contiene la secuencia de bytes a transmitir. •Size: Cantidad de datos a enviar. •Timeout: Indica el tiempo máximo, en milisegundos, antes de finalizar la transmisión. Nuevamente, si el resultado de la función HAL_StatusTypeDef HAL_I2C_Master_Receive es distinto de HAL_OK, se copia un mensaje de error al buffer. En caso contrario, se copia el mensaje Todo OK!! para indicar que la transmisión se ha realizado sin inconvenientes. Una vez el resultado fue el esperado, se procedió a la lectura del acelerómetro. Debido a la escasa documentación y al hecho, ya comentado, de que el acelerómetro LSM6DS0 no tuviera soporte dentro de STM32CubeMX hicieron de esta parte una de las más complejas del proyecto. En primer lugar, se debe acudir al datasheet del acelerómetro LSM6DS0 [ 26 ] para conocer cómo se deben leer los datos. El módulo inercial LSM0 posee dos modos de operación: acelerómetro activo y giróscopo apagado y ambos, acelerómetro y giróscopo, encendidos operando al mismo ODR 3 . Cambiar de un modo a otro requiere una operación de escritura: para la operación en modo normal, en el que únicamente el acelerómetro está activo, se debe escribir sobre CTRL_REG6_XL (20h), mientras que en el modo en el que ambos están activos, se debe escribir sobre CTRL_REG1_G (10h). En este proyecto no se utilizan las medidas del giróscopo, por lo que el modo de operación será el modo normal. Una vez establecido el modo de operación, se debe buscar cómo realizar las lecturas del mismo. Según su datasheet, para realizar múltiples lecturas, se debe leer desde los registros OUT_X_XL (28h - 29h) hasta los registros OUT_Z_XL (2Ch - 2Dh) y, una vez leídos éstos, el sistema empieza nuevamente desde OUT_X_XL (28h - 29h). Estas operaciones se pueden observar en la Figura 3.2, extraída del datasheet. En primer lugar se debe configurar el ODR (Output Data Rate). Mirando el datasheet del acelerómetro, las únicas frecuencias posibles son 10 Hz, 50 Hz, 119 Hz, 238 Hz, 476 Hz y 952 Hz. Si el modo de operación configurado es el del acelerómetro funcionando y el giróscopo apagado (el modo de operación establecido por defecto) se debe modificar el registro CTRL_REG6_XL. En este primer código, el valor del ODR será el máximo, 952 Hz, aunque más adelante se realizarán modificaciones para que dicho valor pueda ser seleccionado por el usuario de entre los posibles. 2Definición de la estructura en stm32f4xx_hal_def.h 3Output Data Rate
3.2 Diseño del driver para el acelerómetro LSM6DS0 25 Figura 3.2 Múltiples lecturas del acelerómetro del módulo inercial LSM6DS0. Imagen extraída del datasheet. En segundo lugar, se debe seleccionar la escala de trabajo del acelerómetro, a saber: ± 2/ ± 4/ ± 8/ ± 16 g. Dependiendo de la escala elegida, la variable sensitivity tomará diferentes valores de aquéllos definidos en LSM6DS0_CUSTOM.h. Por último, se debe atender a la representación de los valores. Según el datasheet, el valor leído en estos registros es expresado en una palabra de 16-bits en complemento a dos. Esto es, para formar la palabra de 16-bits de OUT_X_XL, se deben leer los registros OUT_X_L_XL yOUT_X_H_XL, cada uno de 8-bits, y concatenarlos. Se expone a continuación el código que lleva a cabo dichas operaciones: Código 3.4 Configuracion del ODR y lectura de los ejes del acelerómetro LSM6DS0. static char dataOut[256]; int16_t dataRaw[3]; float sensitivity = 0; uint8_t regValue[6] = {0, 0, 0, 0, 0, 0}; uint8_t i, j, k; uint8_t numberOfByteForDimension; uint8_t value; //Configuracion del ODR HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); value &= ~LSM6DS0_ACC_GYRO_ODR_XL_MASK; value |= LSM6DS0_ACC_GYRO_ODR_XL_952Hz; HAL_I2C_Mem_Write(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); //Lectura del acelerometro numberOfByteForDimension = 6 / 3;
26 Capítulo 3. Diseño e implementación k = 0; for (i = 0; i < 3; i++ ) { for (j = 0; j < numberOfByteForDimension; j++ ) { HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR, LSM6DS0_ACC_GYRO_OUT_X_L_XL + k, I2C_MEMADD_SIZE_8BIT, ®Value[k], 1, I2C_EXPBD_Timeout); k++; } } dataRaw[0] = ((((int16_t)regValue[1]) << 8) + (int16_t)regValue[0]); dataRaw[1] = ((((int16_t)regValue[3]) << 8) + (int16_t)regValue[2]); dataRaw[2] = ((((int16_t)regValue[5]) << 8) + (int16_t)regValue[4]); /* Seleccionar Escala */ float sensitivity = 0; switch( fullScale ) { case FS_LOW: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_2G; break; case FS_MID: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_4G; break; case FS_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_8G; break; case FS_EXTRA_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_16G; break; default: sensitivity = -1.0f; } /* Calculate the data. */ acceleration.AXIS_X = ( int32_t )( dataRaw[0] * sensitivity ); acceleration.AXIS_Y = ( int32_t )( dataRaw[1] * sensitivity ); acceleration.AXIS_Z = ( int32_t )( dataRaw[2] * sensitivity ); snprintf(dataOut, 256, "%d,%d,%d\r\n", (int)acceleration.AXIS_X, (int) acceleration.AXIS_Y, (int)acceleration.AXIS_Z); HAL_UART_Transmit(&huart2, dataOut, strlen(dataOut), HAL_MAX_DELAY); Cabe destacar el uso de la función HAL_I2C_Mem_Read en lugar de HAL_I2C_Master_Receive. Ésto es debido a que esta última función no soporta el tipo de transferencia I2C requerido para leer
3.2 Diseño del driver para el acelerómetro LSM6DS0 27 datos de una sub-dirección. El término sub-direccionamiento (del inglés sub-addressing) se refiere al uso, por parte de los dispositivos basados en I2C, de direcciones internas que son independientes de la dirección del dispositivo, tal y como se define en la especificación del protocolo I2C . Para admitir este esquema de direccionamiento interno, el fabricante del dispositivo define un protocolo de comunicaciones que utiliza bytes dentro del patrón de datos para definir estas direcciones. En el caso del módulo inercial LSM6DS0, el protocolo I2C implementado se comporta como un dispositivo esclavo, y se debe cumplir para la lectura de datos: 1. La transacción comienza cuando el dispositivo maestro envía una condición de inicio ST (START condition). Una ST es definida como una transición de nivel alto a bajo en la línea de datos SDA (Serial Data Line) mientras que la línea SCL (Serial Clock Line) se mantiene en nivel alto. 2. Después de la ST, el dispositivo maestro envía 8 bits. Los 7 primeros indican la dirección del esclavo, SAD (Slave Address), y el último se corresponde con un de bit de escritura, W. 3. El dispositivo esclavo confirma la recepción de los datos anteriormente enviados devolviendo un bit de reconocimiento, SAK (Slave Acknowledge). 4. El dispositivo maestro envía la sub-dirección al dispositivo esclavo SUB (Sub-address) . 5. El dispositivo esclavo confirma la recepción de estos datos con un SAK. 6. El dispositivo maestro envía una condición repetida de inicio, RS (Repeated Start). 7. El dispositivo esclavo responde con un SAK. 8. Después, el dispositivo esclavo transmite los datos. Cada transferencia de datos contiene 8 bits. 9. Si el dispositivo maestro espera más datos, éste responde con una confirmación de recepción de datos MAK (Master Acknowledge). Si por el caso contrario no espera ningún dato más, responde con un NMAK (No Master Acknowledge) indicando al dispositivo esclavo que detenga las comunicaciones y libere el bus. 10. Cuando la transferencia de datos finalice, el dispositivo maestro enviará una una condición de parada SP (STOP condition). La condición de parada es generada por una transición de nivel bajo a nivel alto en la línea SDA, mientras que la línea SCL se mantiene en nivel alto. Información específica acerca de este protocolo puede ser consultada en el datasheet del acelerómetro LSM6DS0. Así mismo, en las Figuras 3.3 y 3.4 se puede observar de manera más gráfica este proceso para la lectura de un único byte y para la lectura de múltiples, respectivamente. Figura 3.3 Proceso de transferencia cuando el dispositivo maestro está recibiendo (leyendo) un byte de datos del esclavo.. Una vez se pueden leer los datos del acelerómetro correctamente, lo siguiente fue crear dos archivos, LSM6DS0_CUSTOM.h yLSM6DS0_CUSTOM.c, con el objetivo de que, con sólo copiarlos en el proyecto en el que se esté trabajando, el usuario pudiera hacer uso de las funcionalidades del acelerómetro LSM6DS0 de forma rápida y sencilla. Por una parte, el archivo LSM6DS0_CUSTOM.h contiene las direcciones de memoria, registros y definiciones de las estructuras necesarias para configurar correctamente el acceso al sensor.
28 Capítulo 3. Diseño e implementación Figura 3.4 Proceso de transferencia cuando el dispositivo maestro está recibiendo (leyendo) múltiples bytes de datos del esclavo.. Por otra parte, el archivo LSM6DS0_CUSTOM.c contiene la definición de las funciones que puede utilizar el usuario. El objetivo que se ha seguido a la hora de diseñar las funciones que conforman este fichero ha sido que éstas se mantuvieran simples a la par que prácticas, brindándole al usuario la posibilidad de modificar ciertos parámetros propios del acelerómetro de forma ágil.Con este objetivo en mente, se han creado dos funciones; Inicializar_LSM6DS0 yObtener_Ejes_LSM6DS0. La función Inicializar_LSM6DS0 se debe usar, como su propio nombre indica, para la inicialización del LSM6DS0. Se encarga de configurar el modo de operación y el ODR. Su prototipo es el siguiente: Código 3.5 Prototipo de la función Inicializar_LSM6DS0. void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr) Donde: 1. I2C_handler : Puntero a una instancia de una estructura de tipo I2C_HandleTypeDef , definida en stm32f4xx_hal_i2c.h. Contiene la información de configuración para el I2Cespecificado. 2. odr : Valor para el ODR. El valor introducido se aproximará a uno de los valores de ODR admisibles por el acelerómetro. Dichos valores aparecen en LSM6DS0_CUSTOM.h o pueden consultarse directamente en el datasheet [26]. El código completo de esta función es el que se muestra a continuación: Código 3.6 Función Inicializar_LSM6DS0. void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr){ LSM6DS0_ACC_GYRO_ODR_XL_t new_odr; uint8_t value; new_odr = ( odr <= 10.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_10Hz : ( odr <= 50.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_50Hz : ( odr <= 119.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_119Hz : ( odr <= 238.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_238Hz : ( odr <= 476.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_476Hz : LSM6DS0_ACC_GYRO_ODR_XL_952Hz; HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); value &= ~LSM6DS0_ACC_GYRO_ODR_XL_MASK; value |= new_odr;
3.2 Diseño del driver para el acelerómetro LSM6DS0 29 HAL_I2C_Mem_Write(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); } La función Obtener_Ejes_LSM6DS0 es la que se utiliza para obtener los valores del acelerómetro. Su prototipo es el siguiente: Código 3.7 Prototipo de la función Obtener_Ejes_LSM6DS0. void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale); Donde: 1. I2C_handler : Puntero a una instancia de una estructura de tipo I2C_HandleTypeDef , definida en stm32f4xx_hal_i2c.h. Contiene la información de configuración para el I2Cespecificado. 2. acceleration : Puntero a una instancia de una estructura de tipo SensorAxesFloat_t, definida en LSM6DS0_CUSTOM.h. Almacenará los datos de lectura del acelerómetro cada ciclo. 3. fullScale : Instancia de una estructura de tipo SensorAxesFloat_t, definida en LSM6DS0_- CUSTOM.h. Indica cual es la escala del acelerómetro, pudiendo elegir entre FS_LOW, FS_- MID, FS_HIGH, FS_EXTRA_HIGH, los cuales se corresponden con ± 2g, ± 4g, ± 8g y ± 16g, respectivamente. El código completo de esta función queda de la siguiente forma: Código 3.8 Función Obtener_Ejes_LSM6DS0. void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale) { HAL_StatusTypeDef ret; uint8_t buf[13]; SensorAxes_t aux_acceleration; buf[0]=REG_ACC; ret = HAL_I2C_Master_Transmit(I2C_handler, LSM6DS0_ADDR, buf, 1, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SCL!!\r\n"); } else { ret = HAL_I2C_Master_Receive(I2C_handler, LSM6DS0_ADDR, buf, 2, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SDA!!\r\n"); } else { int16_t dataRaw[3];
36 Capítulo 3. Diseño e implementación printf("Running inference\r\n"); AI_Run(aiInData, aiOutData); /* Output results */ for (uint32_t i = 0; i < AI_ACC_NEURAL_MODEL_OUT_1_SIZE; i++) { printf("%8.6f ", aiOutData[i]); } uint32_t class = argmax(aiOutData, AI_ACC_NEURAL_MODEL_OUT_1_SIZE); printf(": %d - %s\r\n", (int) class, activities[class]); printf("Esperando 4 segundos para nuevo movimiento...\r\n"); HAL_Delay(4000); printf("Haz un movimiento!\r\n"); } } La primera operación en este bucle es obtener las lecturas del acelerómetro mediante la llamada Obtener_Ejes_LSM6DS0 y almacenar dicho valor en accelerationFloat. A continuación, se guarda el valor de cada lectura en el búfer aiInData, dividiéndolo antes por la escala que hemos seleccionado, 2g en este caso, para obtener valores entre -1 y 1. Después, se incrementa la variable usada como índice del búfer para la escritura de datos hasta que el valor alcanza AI_ACC_NEURAL_MODEL_IN_1_SIZE, momento en el que se resetea dicha variable y se llama a la función AI_Run para efectuar la inferencia a partir de los datos almacenados en el búfer aiInData y guardar el resultado en el búfer de salida aiOutData. Por último, se recorre dicho búfer con la función argmax para obtener el índice del valor de salida más probable para dicha inferencia y se imprime el resultado por pantalla. 3.4 Sistema completo En este apartado se incluye el código del sistema completo así como los desarrollos aún no comentados y necesarios para su implementación. Utilizar el driver del acelerómetro es tan sencillo como incluir los ficheros LSM6DS0_CUSTOM.h yLSM6DS0_CUSTOM.c en Core −→ Inc yCore −→ Src, respectivamente. El código de estos ficheros se detalla a continuación: Código 3.16 Fichero LSM6DS0_CUSTOM.h. //INCLUIR LIBRERIAS ESTANDAR #include <string.h> #include <stdint.h> //DEFINES ACELEROMETRO LSM6DS0 #define LSM6DS0_ADDR 0xD6 #define LSM6DS0_ACC_GYRO_OUT_X_L_XL 0X28 #define I2C_MEMADD_SIZE_8BIT 0x00000001U #define I2C_EXPBD_Timeout 0x1000 #define LSM6DS0_ACC_GYRO_CTRL_REG6_XL 0X20
3.4 Sistema completo 37 #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_2G 0.061 /**< Sensitivity value for 2 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_4G 0.122 /**< Sensitivity value for 4 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_8G 0.244 /**< Sensitivity value for 8 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_16G 0.732 /**< Sensitivity value for 16 g full scale [mg/LSB] */ #define LSM6DS0_ACC_GYRO_FS_XL_MASK 0x18 #define CONVERT_MG_TO_MS2 0.00980665f //ESTRUCTURAS typedef struct { int32_t AXIS_X; int32_t AXIS_Y; int32_t AXIS_Z; } SensorAxes_t; typedef struct { float AXIS_X; float AXIS_Y; float AXIS_Z; } SensorAxesFloat_t; typedef enum { LSM6DS0_ACC_GYRO_FS_XL_2g = 0x00, LSM6DS0_ACC_GYRO_FS_XL_16g = 0x08, LSM6DS0_ACC_GYRO_FS_XL_4g = 0x10, LSM6DS0_ACC_GYRO_FS_XL_8g = 0x18, } LSM6DS0_ACC_GYRO_FS_XL_t; //SENSIBILIDAD typedef enum { FS_LOW, FS_MID, FS_HIGH, FS_EXTRA_HIGH } SensorFs_t; Código 3.17 Fichero LSM6DS0_CUSTOM.c. #include "LSM6DS0_CUSTOM.h" #include "main.h"
38 Capítulo 3. Diseño e implementación #include "stm32f4xx_hal_i2c.h" //DECLARACIONES DE FUNCIONES void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale); //FUNCIONES /** * @brief Get the LSM6DS0 accelerometer sensor axes * @param I2C_handler the device handle * @param acceleration pointer where the values of the axes are written * @param fullScale set the full scale value */ void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale) { HAL_StatusTypeDef ret; uint8_t buf[13]; SensorAxes_t aux_acceleration; buf[0]=REG_ACC; ret = HAL_I2C_Master_Transmit(I2C_handler, LSM6DS0_ADDR, buf, 1, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SCL!!\r\n"); } else { ret = HAL_I2C_Master_Receive(I2C_handler, LSM6DS0_ADDR, buf, 2, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SDA!!\r\n"); } else { int16_t dataRaw[3]; uint8_t regValue[6] = {0, 0, 0, 0, 0, 0}; uint8_t i, j, k; uint8_t numberOfByteForDimension; numberOfByteForDimension = 6 / 3; k = 0; for (i = 0; i < 3; i++ ) { for (j = 0; j < numberOfByteForDimension; j++ ) { HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR, LSM6DS0_ACC_GYRO_OUT_X_L_XL + k, I2C_MEMADD_SIZE_8BIT, & regValue[k], 1, I2C_EXPBD_Timeout); k++;
3.4 Sistema completo 39 } } dataRaw[0] = ((((int16_t)regValue[1]) << 8) + (int16_t)regValue[0]); dataRaw[1] = ((((int16_t)regValue[3]) << 8) + (int16_t)regValue[2]); dataRaw[2] = ((((int16_t)regValue[5]) << 8) + (int16_t)regValue[4]); /* Seleccionar Escala */ float sensitivity = 0; switch( fullScale ) { case FS_LOW: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_2G; break; case FS_MID: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_4G; break; case FS_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_8G; break; case FS_EXTRA_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_16G; break; default: sensitivity = -1.0f; } /* Calcular valores de salida */ aux_acceleration.AXIS_X = ( int32_t )( dataRaw[0] * sensitivity ) ; aux_acceleration.AXIS_Y = ( int32_t )( dataRaw[1] * sensitivity ) ; aux_acceleration.AXIS_Z = ( int32_t )( dataRaw[2] * sensitivity ) ; acceleration->AXIS_X = ( float ) aux_acceleration.AXIS_X ; acceleration->AXIS_Y = ( float ) aux_acceleration.AXIS_Y ; acceleration->AXIS_Z = ( float ) aux_acceleration.AXIS_Z ; } } } Tal y como se menciona en apartados posteriores, en la sección 4.1.2, los datos se capturan a una frecuencia de 26Hz. Para asegurarlo, se hace uso de las características de la placa de desarrollo y se configura un timer, abriendo el fichero del proyecto .io. Para elegir un timer, se debe hacer click sobre la pestaña Timers en el menu lateral izquierdo y seleccionar uno que no tenga el símbolo triangular amarillo, símbolo que indica que ese timer en concreto ha sido "parcialmente deshabilitado" debido a que alguno o todos sus canales han sido deshabilitados por entrar en conflicto con alguna configuración previa realizada. En concreto y para la
40 Capítulo 3. Diseño e implementación placa de desarrollo STM32F401RE, los timers parcialmente deshabilitados son el TIM2,TIM4,TIM5 yTIM9, debido a que éstos entran en conflicto con la USART2 y el I2C1. Por ello, en este desarrollo se ha hecho uso del timer TIM1. Para conocer las características del timer seleccionado, se debe acudir al datasheet de la placa STM32F401RE [ 24 ]. Según la figura 3.6, dicho timer posee una resolución de 16-bits, lo que permite contar hasta un valor máximo de 65535. Figura 3.6 Características del timer TIM1. Además resulta de utilidad consultar, en este mismo datasheet, el diagrama de bloques de la placa STM32F401RE, donde se puede observar a que preescalador está conectado dicho timer. En la Figura 3.7 se puede comprobar que TIM1 está conectado al preescalador APB2. Figura 3.7 Fragmento del diagrama de bloques de la placa STM32F401RE. Una vez conocidos estos datos, antes de proceder a la configuración del timer seleccionado, se debe examinar la configuración actual del reloj principal de la CPU, haciendo click sobre la pestaña Clock Configuration situada en la parte superior. En la Figura 3.8 se puede observar que el reloj principal de la CPU, denominado HCLK, está operando a 84 MHz, su frecuencia máxima, y que el preescalador al cual está conectado TIM1, APB2 está puesto a 1. Ahora, se van a comentar los valores de configuración del timer. Puesto que se busca una frecuencia de muestreo de 26Hz, esto quiere decir que la subrutina de interrupción debe activarse cada 38,46 ms aproximadamente. Dado que el reloj principal está operando a 84 Mhz, si se configura el preescalador a 84 se puede obtener 1 Mhz en la frecuencia del timer. Hay que tener en cuenta que con estos valores, solo se podrán fijar períodos de hasta un máximo de 65,535 ms.
3.4 Sistema completo 41 Figura 3.8 Configuración del reloj interno de la placa STM32F401RE. 84Mhz 84 =1Mhz −→ 1tick µs Donde el tiempo máximo según la Figura 3.6 es: Tiempo máximo: 65535 ·1µs=65,535 ms Sin embargo, esto no supone problema alguno pues, como se ha comentado más arriba, únicamente se busca fijar el timer con un período de 38,46 ms. Para obtener el valor del contador, se debe realizar la siguiente operación: 1 26 sec · 84Mhz 84 ≈38462 ticks Una vez obtenidos los valores, se pasa a introducir dichos parámetros en STM32CubeMX. Dentro de la ventana de configuración del timer TIM1, se debe seleccionar Clock Source: Internal Clock y dejar las demás opciones por defecto (canales deshabilitados). Bajo la ventana Configuration −→ Parameter Settings −→ Counter Settings se deben introducir los parámetros previamente calculados con ciertos matices: • Prescaler (PSC - 16 bits value): 83. Se debe introducir 83 y no 84 debido a que si se introduce un valor de 0, el preescalador será configurado a 1, si se introduce un valor de 1, el preescalador será configurado a 2 y así sucesivamente. •Counter mode:Up. • Counter Period (AutoReload Register - 16 bits value) 38461. Nótese que se introduce un tick menos del calculado pues si se introdujera 38462, la interrupción saltaría en el tick 386463. Estas configuraciones se pueden ver en la Figura 3.9 A continuación, se añade un fragmento del código del archivo main.c, con las modificaciones realizadas para incluir el timer.
42 Capítulo 3. Diseño e implementación Figura 3.9 Configuración del timer TIM1. Código 3.18 Fragmento del archivo main.c. int main(void) { /* MCU Configuration --------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); MX_I2C1_Init(); MX_CRC_Init(); MX_TIM1_Init(); /* USER CODE BEGIN 2 */ RetargetInit(&huart2); printf("\r\n Iniciando Aplicacion de Reconocimiento...\r\n"); AI_Init(ai_acc_neural_model_data_weights_get(), activations2); printf("Esperando 4 segundos para comenzar...\r\n"); HAL_Delay(4000);
3.4 Sistema completo 43 printf("Haz un movimiento!\r\n"); HAL_Delay(1000); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ //Iniciamos el timer HAL_TIM_Base_Start_IT(&htim1); while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(flagInterrupcion==1){ flagInterrupcion=0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_MID); aiInData[write_index + 0] = (float) accelerationFloat.AXIS_X/ 4000.0f; aiInData[write_index + 1] = (float) accelerationFloat.AXIS_Y/ 4000.0f; aiInData[write_index + 2] = (float) accelerationFloat.AXIS_Z/ 4000.0f; printf("%.2f,%.2f,%.2f\r\n", aiInData[write_index + 0], aiInData[write_index + 1], aiInData[write_index + 2]); write_index += 3; if (write_index == AI_ACC_NEURAL_MODEL_IN_1_SIZE) { write_index = 0; printf("Realizando inferencia...\r\n"); AI_Run(aiInData, aiOutData); /* Output results */ for (uint32_t i = 0; i < AI_ACC_NEURAL_MODEL_OUT_1_SIZE; i++) { printf("%8.6f ", aiOutData[i]); }
44 Capítulo 3. Diseño e implementación uint32_t class = argmax(aiOutData, AI_ACC_NEURAL_MODEL_OUT_1_SIZE); printf(": %d - %s\r\n", (int) class, activities[class]); printf("Esperando 4 segundos para nuevo movimiento...\r\n"); HAL_TIM_Base_Stop_IT(&htim1); HAL_Delay(4000); HAL_TIM_Base_Start_IT(&htim1); printf("Haz un movimiento!\r\n"); } } } /* USER CODE END 3 */ } Tal y como se aprecia en el código 3.18, la llamada HAL_TIM_Base_Start_IT inicia el timer. Como se puede leer en el manual de usuario del driver HAL para microcontroladores de la familia STM32F4 [ 21 ], se debe utilizar HAL_TIM_Base_Start_IT en lugar de HAL_TIM_Base_Start debido a que esta última función es la encargada de iniciar el timer en modo interrupción. Cada vez que se genera una interrupción, se llama a la función HAL_TIM_PeriodElapsedCallback. Esta función aparece definida en el fichero stm32f4xx_hal_tim.c, bajo el tipo __weak, donde se informa de que para hacer uso de ella, debe ser implementada en los archivos de usuario. Por tanto, es definida en el fichero main.c y se enmarca entre las regiones /* USER CODE BEGIN 4 */ y/* USER CODE END 4 */ . Su código es el siguiente: Código 3.19 Función HAL_TIM_PeriodElapsedCallback. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ //Verificamos que sea el timer 11 el que ha hecho saltar la interrupcion if(htim==&htim1){ flagInterrupcion=1; } Dicha función recibe como argumento un puntero a una estructura de tipo TIM_HandleTypeDef , la cual contiene la identificación del timer. Esta función puede ser llamada por cualquier timer que haya generado una interrupción por lo que, para crear un código más robusto con vistas a introducir un mayor número de redes neuronales y de timers, se pregunta qué timer ha hecho saltar la interrupción en cuestión. Si ese timer es el configurado anteriormente, htim1, se pone la variable flagInterrupcion a nivel alto. En el código 3.18, cada vez que la variable flagInterrupcion esté a nivel alto, se realiza la lectura del acelerómetro. El comportamiento del timer se puede observar de forma gráfica en la Figura 3.10.
3.4 Sistema completo 45 Figura 3.10 Funcionamiento del timer TIM1. Cabe destacar que los timers entran en conflicto con la función HAL_Delay. Ésto es debido a que HAL_Delay usa la interrupción SysTick y, si la prioridad de SysTick es menor que la prioridad de la interrupción a la que se llama, se terminará en un bucle infinito, ya que SysTick nunca se invocará. Puesto que se quiere utilizar HAL_Delay con el propósito de que el usuario pueda ver por pantalla el resultado de la inferencia así como para introducir un tiempo de espera entre un movimiento y otro, en primer lugar, se para el timer con la función HAL_TIM_Base_Stop_IT, tal y como se puede ver en el código 3.18. Después, se realizan las llamadas HAL_Delay y por último se vuelve a iniciar el timer con HAL_TIM_Base_Start_IT. Ésto resuelve los conflictos y permite generar el comportamiento deseado.
52 Capítulo 4. Pruebas /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* Configure the system clock */ SystemClock_Config(); /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); MX_I2C1_Init(); MX_CRC_Init(); MX_TIM1_Init(); /* USER CODE BEGIN 2 */ RetargetInit(&huart2); printf("\r\n Iniciando programa de captura de datos...\r\n"); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ int mode=1; if(mode==1){ HAL_TIM_Base_Start_IT(&htim1); while(1){ if(flagInterrupcion==1){ HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_MID); accelerationAux.AXIS_X = (float) accelerationFloat.AXIS_X/4000.0f; accelerationAux.AXIS_Y = (float) accelerationFloat.AXIS_Y/4000.0f; accelerationAux.AXIS_Z = (float) accelerationFloat.AXIS_Z/4000.0f; printf("%.2f,%.2f,%.2f\r\n", accelerationAux.AXIS_X, accelerationAux.AXIS_Y, accelerationAux.AXIS_Z); flagInterrupcion=0; } }
4.1 Pruebas parciales 53 } if(mode==2){ int mediciones=0; printf("Esperando 6 segundos para comenzar"); HAL_Delay(6000); HAL_TIM_Base_Start_IT(&htim1); while(mediciones<1000){ if(flagInterrupcion==1){ flagInterrupcion=0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_MID); accelerationAux.AXIS_X = (float) accelerationFloat.AXIS_X; accelerationAux.AXIS_Y = (float) accelerationFloat.AXIS_Y; accelerationAux.AXIS_Z = (float) accelerationFloat.AXIS_Z; printf("%.2f,%.2f,%.2f\r\n", accelerationAux.AXIS_X, accelerationAux.AXIS_Y, accelerationAux.AXIS_Z); mediciones++; } } /* USER CODE END WHILE */ } } Este programa captura datos de dos modos diferentes: si la variable mode está configurada a 1, el programa captura datos de forma continua y muestra por pantalla los valores capturados en un rango de -1 a 1 mientras que, si está configurada a 2, capturará datos puros hasta completar 1000 mediciones. Nótese como el acelerómetro LSM6DS0 ha sido configurado a una escala de 4g mediante el uso del argumento FS_MID. Los datos que se van a tomar están capturados a una frecuencia de muestreo de 26Hz, para lo cual se hace uso de un timer. Lo relativo a la configuración de dicho timer fue comentado previamente en el apartado 3.4. Además, la posición de la placa en este caso será en la mano derecha. Para visualizar y guardar los datos se ha hecho uso del emulador de terminal Putty [ 30 ]. Para configurar el emulador para guardar la sesión, se debe pichar sobre Session −→ Logging, seleccionar Printable Output y elegir un nombre y ubicación para el archivo. Un ejemplo de un archivo saltando se puede visualizar en la gráfica 4.7
54 Capítulo 4. Pruebas Figura 4.7 Gráfica del estado saltando. Cambios en el código de la red Con respecto al código, el primer paso es cargar la biblioteca de código abierto para aprendizaje automático TensorFlow [ 5 ]. Tal y como se avisa en [ 20 ], se debe usar la versión 1.15, ya que la versión 2.2 aún no está soportada en X-CUBE-AI. Para ello se ejecuta el siguiente comando: Código 4.4 Biblioteca TensorFlow. %tensorflow_version 1.x import tensorflow as tf En concreto, la versión utilizada para crear esta red neuronal fue la 1.15.2. Después, se debe descargar el dataset que se va a utilizar, extraer los datos y cargarlos en memoria. El dataset a utilizar se encuentra subido en un repositorio de github creado para este proyecto 1 y es de acceso público para todo aquel que desee utilizarlo. Estos pasos se traducen en el siguiente código: Código 4.5 Cargar el dataset en memoria. #Descargar el dataset !wget -nc https://github.com/juacuegut/TFG_Gesture_Recognition/raw/main/ dataset.zip #Descomprimirlo !unzip -n dataset.zip #Cargar los datos en memoria import glob import numpy as np 1https://github.com/juacuegut/TFG_Gesture_Recognition
4.1 Pruebas parciales 55 # Load data into memory labels = [’stationary’, ’walking’, ’running’, ’jumping’] x = [] y = [] y_filenames = [] for i, label in enumerate(labels): filenames = glob.glob(’dataset/’ + label + ’/*.csv’) for filename in filenames: data = np.loadtxt(filename, delimiter=’,’) x.append(data) y.append(i) y_filenames.append(filename) x2 = np.array(x).reshape(len(x), 1000, 3) y2 = np.array(y) Enventanado de los datos Cada captura de datos se divide ahora en ventanas, cuyo tamaño será la dimensión de entrada de la red neuronal. En concreto, y tal y como se ha comentado anteriormente, se ha dividido el dataset en ventanas de 1 segundo (26 muestras a 26Hz). Se ha aplicado además un 50% de overlap para aumentar la cantidad de puntos en nuestros datos para training ytest. Código 4.6 Enventanado de datos. import numpy as np def frame(x, frame_len, hop_len): assert(x.shape == (len(x), 3)) assert(x.shape[0] >= frame_len) assert(hop_len >= 1) n_frames = 1 + (x.shape[0] - frame_len) // hop_len shape = (n_frames, frame_len, x.shape[1]) strides = ((hop_len * x.strides[0],) + x.strides) return np.lib.stride_tricks.as_strided(x, shape=shape, strides= strides) x3 = [] y3 = [] for i in range(x2.shape[0]): # frames = frame(x2[i], 26, 26) # no overlap frames = frame(x2[i], 26, 16) # 50% overlap x3.append(frames) y3.append(np.full(frames.shape[0], y[i])) x3 = np.concatenate(x3) y3 = np.concatenate(y3)
56 Capítulo 4. Pruebas Preprocesamiento del dataset Para esta red, se usará un simple escalado de los datos. Este escalado consiste en dividir los datos capturados entre el rango de la escala del sensor (4g) para obtener valores en un rango de entre -1 y 1: Código 4.7 Escalado de los datos. # Normalizar los datos de entrada entre [-1;1] x4 = x3 / 4000 Después, se divide el dataset entre dos conjuntos: "entrenamiento" (training) y "pruebas" (test). Para ésto, los datos se mezclan y se reservan un 25% de ellos para la evaluación del modelo y las pruebas del mismo. Con respecto al código, se hará uso en este caso de la librería Scikit-learn [ 3 ] y en concreto de la función train_test_split que lleva a cabo esta operación. Código 4.8 División del dataset en training ytest. from sklearn.model_selection import train_test_split x_train, x_test, y_train, y_test = train_test_split(x4, y3, test_size =0.25) print("Trainning samples:", x_train.shape) print("Testing samples:", x_test.shape) El resultado son 5032 muestras para la fase de training y1678 muestras para la fase de test. Creación del modelo Para la creación de la red neuronal se ha utilizado el siguiente código: Código 4.9 Creación del modelo. ## Conv1D based model model = tf.keras.models.Sequential([ tf.keras.layers.Conv1D(filters=16, kernel_size=4, activation=’relu’, input_shape=(26, 3)), tf.keras.layers.Conv1D(filters=8, kernel_size=4, activation=’relu’), tf.keras.layers.Dropout(0.5), tf.keras.layers.Flatten(), tf.keras.layers.Dense(64, activation=’relu’), tf.keras.layers.Dense(4, activation=’softmax’) ]) model.compile(optimizer=’adam’, loss=’sparse_categorical_crossentropy’, metrics=[’accuracy’]) model.fit(x_train, y_train, epochs=30) test_loss, test_acc = model.evaluate(x_test, y_test, verbose=2) print("Test loss:", test_loss) print("Test acc:", test_acc) model.summary()
4.1 Pruebas parciales 57 Para incluir el nuevo estado, se ha cambiado el tamaño del kernel, mediante la variable kernel_size, de 3 a 4. Además, se ha modificado la capa de salida de la red neuronal para contemplar 4 clases en lugar de 3, cambiando la capa en la que se aplica la función softmax, la cual asigna probabilidades decimales a cada clase sumando entre todas ellas 1.0. Entrenamiento A continuación, se presentan los epochs ejecutados durante la fase de "entrenamiento" utilizando las 5032 muestras seleccionadas para ello. Como se puede comprobar, durante esta fase se alcanzó un 98.93% de precisión. Código 4.10 Fase de entrenamiento. Train on 5032 samples Epoch 1/30 5032/5032 [==============================] - 1s 105us/sample - loss: 1.0054 - acc: 0.5636 Epoch 2/30 5032/5032 [==============================] - 0s 90us/sample - loss: 0.4769 - acc: 0.8168 Epoch 3/30 5032/5032 [==============================] - 0s 97us/sample - loss: 0.2505 - acc: 0.9050 Epoch 4/30 5032/5032 [==============================] - 0s 85us/sample - loss: 0.1666 - acc: 0.9412 Epoch 5/30 5032/5032 [==============================] - 0s 79us/sample - loss: 0.1350 - acc: 0.9519 Epoch 6/30 5032/5032 [==============================] - 0s 91us/sample - loss: 0.1131 - acc: 0.9595 Epoch 7/30 5032/5032 [==============================] - 0s 87us/sample - loss: 0.1025 - acc: 0.9622 Epoch 8/30 5032/5032 [==============================] - 0s 86us/sample - loss: 0.1019 - acc: 0.9652 Epoch 9/30 5032/5032 [==============================] - 0s 92us/sample - loss: 0.0884 - acc: 0.9710 Epoch 10/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0973 - acc: 0.9660 Epoch 11/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0887 - acc: 0.9700 Epoch 12/30 5032/5032 [==============================] - 0s 90us/sample - loss: 0.0842 - acc: 0.9694 Epoch 13/30
58 Capítulo 4. Pruebas 5032/5032 [==============================] - 0s 86us/sample - loss: 0.0724 - acc: 0.9764 Epoch 14/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0674 - acc: 0.9756 Epoch 15/30 5032/5032 [==============================] - 0s 81us/sample - loss: 0.0724 - acc: 0.9752 Epoch 16/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0627 - acc: 0.9783 Epoch 17/30 5032/5032 [==============================] - 0s 84us/sample - loss: 0.0639 - acc: 0.9795 Epoch 18/30 5032/5032 [==============================] - 0s 86us/sample - loss: 0.0586 - acc: 0.9785 Epoch 19/30 5032/5032 [==============================] - 0s 87us/sample - loss: 0.0572 - acc: 0.9817 Epoch 20/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0582 - acc: 0.9799 Epoch 21/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0516 - acc: 0.9827 Epoch 22/30 5032/5032 [==============================] - 0s 90us/sample - loss: 0.0580 - acc: 0.9791 Epoch 23/30 5032/5032 [==============================] - 0s 85us/sample - loss: 0.0533 - acc: 0.9815 Epoch 24/30 5032/5032 [==============================] - 0s 86us/sample - loss: 0.0474 - acc: 0.9849 Epoch 25/30 5032/5032 [==============================] - 0s 84us/sample - loss: 0.0491 - acc: 0.9839 Epoch 26/30 5032/5032 [==============================] - 0s 83us/sample - loss: 0.0508 - acc: 0.9819 Epoch 27/30 5032/5032 [==============================] - 0s 91us/sample - loss: 0.0420 - acc: 0.9855 Epoch 28/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0457 - acc: 0.9849 Epoch 29/30 5032/5032 [==============================] - 0s 88us/sample - loss: 0.0537 - acc: 0.9821
4.1 Pruebas parciales 59 Epoch 30/30 5032/5032 [==============================] - 0s 89us/sample - loss: 0.0428 - acc: 0.9863 1678/1678 - 0s - loss: 0.0284 - acc: 0.9893 Test loss: 0.028437232520959657 Test acc: 0.98927295 Model: "sequential_6" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv1d_12 (Conv1D) (None, 23, 16) 208 _________________________________________________________________ conv1d_13 (Conv1D) (None, 20, 8) 520 _________________________________________________________________ dropout_6 (Dropout) (None, 20, 8) 0 _________________________________________________________________ flatten_6 (Flatten) (None, 160) 0 _________________________________________________________________ dense_12 (Dense) (None, 64) 10304 _________________________________________________________________ dense_13 (Dense) (None, 4) 260 ================================================================= Total params: 11,292 Trainable params: 11,292 Non-trainable params: 0 Una vez finalizado el entrenamiento, podemos construir la matriz de confusión, la cual permitirá evaluar el modelo obteniendo de forma visual el desempeño del algoritmo empleado. Para ello, utilizamos el siguiente código: Código 4.11 Obtención de la matriz de confusión. import matplotlib.pyplot as plt import seaborn as sns %matplotlib inline y_pred = model.predict_classes(x_test) confusion_matrix = tf.math.confusion_matrix(y_test, y_pred) sess = tf.compat.v1.Session() plt.figure() sns.heatmap(sess.run(confusion_matrix), annot=True, xticklabels=labels, yticklabels=labels, cmap=plt.cm.Blues, fmt=’d’, cbar=False) plt.tight_layout() plt.ylabel(’True label’)
60 Capítulo 4. Pruebas plt.xlabel(’Predicted label’) plt.show() El resultado de la ejecución de estos comandos se puede observar en la Figura 4.8: Figura 4.8 Matriz de confusión. Guardado del modelo Para poder utilizar el modelo en STM32CubeMX, se debe guardar en formato .h5 y descargarlo del notebook. Esto se puede conseguir con el siguiente código: Código 4.12 Guardado y descarga del modelo. # Guardar el modelo en formato ".h5" model.save(’model_jumping_inc.h5’) # Descargar modelo from google.colab import files files.download(’model_jumping_inc.h5’) En este caso, al nuevo modelo se le ha puesto del nombre de model_jumping_inc.h5, pero puede ser modificado por el usuario. 4.1.3 Mediciones Adicionalmente, y aprovechando las capacidades de la placa de desarrollo, se ha programado un timer para medir el tiempo de inferencia de la red operando en el microcontrolador. En concreto, se ha programado el timer TIM3, el cual no entra en conflicto con ningún otro componente del sistema (lo cual se debe verificar, tal y como se comenta en la sección 3.4). Dado que el reloj principal está operando a 84 Mhz, si se configura el preescalador a 84 se puede obtener 1 Mhz en la frecuencia del timer. Nótese que, tal y como se ha comentado en la sección 3.4,
4.1 Pruebas parciales 61 bajo estos valores solo se podrán fijar períodos de hasta un máximo de 65,535 ms. Sin embargo, puesto que se pretenden medir tiempos muy pequeños, debido a que la inferencia se realiza de forma muy rápida, este valor será más que suficiente. El resultado de la configuración del timer se muestra en la Figura 4.9. Figura 4.9 Configuración del timer para medir el tiempo de inferencia, TIM3. Para utilizar el timer, se añadió el siguiente código: Código 4.13 Código de inclusión del timer TIM3. while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(flagInterrupcion==1){ flagInterrupcion=0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_LOW); aiInData[write_index + 0] = (float) accelerationFloat.AXIS_X/ 4000.0f; aiInData[write_index + 1] = (float) accelerationFloat.AXIS_Y/ 4000.0f; aiInData[write_index + 2] = (float) accelerationFloat.AXIS_Z/ 4000.0f; write_index += 3;
68 Capítulo 4. Pruebas Figura 4.15 Captura del sistema completo realizando inferencias.
5 Conclusiones y desarrollos futuros El futuro mostrará los resultados y juzgará a cada uno de acuerdo a sus logros. - Nikola Tesla - En este capítulo se presentarán las conclusiones obtenidas como fruto del desarrollo de este proyecto, así como los estudios realizados y las guías para una futura evolución del mismo. 5.1 Conclusión de objetivos En este proyecto se ha demostrado como resulta posible introducir una red neuronal en un dispositivo embebido utilizando las herramientas adecuadas. Además, durante la realización de este proyecto, se han enfrentado problemáticas reales propias de un entorno laboral como es el hecho de tener que dar uso a un dispositivo obsoleto y que carece de soporte, ya sea por tiempo, por el ahorro de dinero que ésto supone o porque el dispositivo es nuevo y se debe crear un driver desde cero. Adicionalmente, se ha diseñado un framework que potencia las herramientas existentes y que cualquier interesado puede utilizar como base sobre la cual desarrollar cualquier tipo de aplicación que requiera del uso de una o varias redes neuronales. Por último, se introducido en el microcontrolador una red neuronal modificada y entrenada con un dataset propio haciendo uso del framework para exponer la versatilidad, escalabilidad y correcto funcionamiento del mismo. Por todo lo expuesto anteriormente, se puede afirmar que los objetivos de este tfg han sido cumplidos, abriéndo además con él la puerta a un desarrollo más ágil de aplicaciones basadas en aprendizaje automatico sobre dispositivos embebidos. No obstante, el proyecto admite mejoras y desarrollos futuros, los cuales serán comentados a continuación. 5.2 Desarrollos futuros El framework desarrollado, aunque funcional, permite una serie de mejoras que sin duda incrementarían su valor potencial haciéndo más sencillo su uso. Las guias de desarrollo de este proyecto son variadas. En primer lugar, para hacer uso del driver del acelerómetro, el usuario debe copiar manualmente los ficheros LSM6DS0_CUSTOM.h yLSM6DS0_CUSTOM.c. Adicionalmente, el usuario debe introducir código a mano en el fichero main.c, con el fin de poder incluir una o varias redes neuronales en el dispositivo embebido deseado. Aunque éstas son las recomendaciones de STMicroelectronics, como bien se presenta en la guía publicada po el mismo en Noviembre del año pasado [ 23 ], sería de gran interés generar dicho código de forma dinámica minimizando así la intervención del usuario. Como bien es sabido, no es posible asignar un nombre dinámico a una variable en tiempo de ejecución, por lo que se exploraron diferentes posibilidades. Para comenzar, sería de interés crear un 69
70 Capítulo 5. Conclusiones y desarrollos futuros template utilizando el motor Apache FreeMarker ™ [ 7 ], una biblioteca de Java para la generación de texto basada en plantillas y datos cambiantes, haciéndo uso del lenguaje FTL (FreeMarker Template Language), con el cual se pueden crear archivos (.flt) que pueden ser usados por STM32CubeMX para generar código específico. Más interesante todavía, sería hacer uso de la herramienta STM32 Pack Creator [ 22 ], una herramienta desarrollada por STMicroelectronics cuyo propósito, como su nombre indica, es la creación de paquetes de software. Un paquete de software es una colección de archivos que se envían en formato .zip (después renombrados a .pack) y que cumplen con las especificaciones Arm ® CMSIS-Pack [ 14 ], las cuales definen una forma estandarizada de creación de componentes software. Dentro de un paquete se incluyen, entre otros: •Código fuente, archivos de encabezado y bibliotecas de software •Plantillas de documentación y código fuente •Proyectos de ejemplo • Archivo en formato .pdsc que describe el contenido del paquete y contexto de uso para los archivos suministrados en su interior Con esta herramienta se podría crear un archivo .pack instalable desde STM32CubeMX y que permitiera al usuario añadir el driver a su proyecto, así como elegir entre varios ejemplos de uso de la red neuronal, con una o varias redes en función de las necesidades del mismo. Esto brindaría sin duda más funcionalidad para el usuario. Como una idea de inclusión de dos redes neuronales coexistentes en el microcontrolador, y haciéndo uso del driver creado para el acelerómetro, sería posible el desarrollo de una aplicación para detectar caídas. Por ejemplo se podría añadir una red que detectara si existe o no caída y otra que detectase la naturaleza de la misma (si se ha producido de forma lateral, frontal o hacia atrás), lo cual brindaría la capacidad de una primera evaluación de los posibles daños causados por el impacto y su posterior acción (por ejemplo, llamar a una ambulancia) lo que resultaría de utilidad sobre todo en el caso de personas de avanzada edad. Desde un punto de vista más técnico, resultaría de interés explorar los límites de este software como, por ejemplo, comprobar cuántas redes se pueden cargar en el microcontrolador o cómo afecta a la performance la inclusión de una red adicional hasta alcalzar el límite de tamaño. Asímismo, también resultaría de interés realizar un estudio acerca de la relación entre la cantidad de movimientos que puede reconocer una red y el tamaño que ésta ocupa en el microcontrolador.
Apéndice A Configuración del entorno y posibles problemas Si le hubiera preguntado a la gente qué querían, habrían dicho caballos más rápidos. - Henry Ford - En esta sección se detallarán los pasos a seguir para configurar el entorno y se añadirá una explicación de los archivos y funciones generadas por STM32CubeMX.Estos pasos implican configurar las comunicaciones entre la placa de desarrollo y la tarjeta de expansión, las comunicaciones entre la placa y el PC y la instalación y configuración del paquete de Inteligencia Artificial X-CUBE-AI. Además, se incluye una sección de posibles problemas que se pueden encontrar al hacer uso de este software. A.1 Configuración de STM32CubeMX El primer paso, después de la instalación del programa, es empezar un nuevo proyecto (Figura A.1). En el caso concreto de este trabajo de fin de grado, se usará la placa Núcleo-64 STM32F401RE presentada en la sección 2.6.1, por lo que se puede elegir la opción ACCESS TO BOARD SELECTOR. Ésto abrirá una nueva pestaña donde, una vez encontrada la placa, aparecerá información sobre la misma tal y como se muestra en la Figura A.2. Al hacer click sobre START PROJECT, se abrirá una ventana preguntando al usuario si desea inicializar los pines (Figura A.3). Después, el usuario se encuentra en la ventana de configuración del microcontrolador (Figura A.4). A.1.1 I2C y USART En primer lugar, después de seleccionar la placa de desarrollo STM32F401RE en STM32CubeMX, algunos pines aparecen configurados por defecto. Para este proyecto, se deben marcar además los pines PB8 yPB9 como I2C1_SCL yI2C1_SDA, respectivamente. Una vez realizado este paso, en la pestaña Conectivity se debe marcar I2C1 y configurarlo tal y como se ve en la Figura A.5. En la parte derecha de la Figura A.5 se pueden ver el conjunto de pines configurados. Cabe prestar especial atención a los pines PA2 yPA3 que aparecen configurados automáticamente como USART_TX yUSART_RX, los cuales se utilizarán en la configuración del puerto serie implementando el protocolo USART. 71
72 Capítulo A. Configuración del entorno y posibles problemas Figura A.1 Página principal de STM32CubeMX. Figura A.2 Nuevo proyecto y selección de la tarjeta Núcleo-F401RE en STM32CubeMX. Todos estos pines deben ser consultados en el datasheet de la placa de desarrollo [24]. Una vez realizadas las configuraciones, elegido un nombre apropiado para el proyecto y seleccionado como IDE el software STM32CubeIDE, se debe clickar sobre GENERATE CODE, lo cual permitirá importar el proyecto al espacio de trabajo dentro de STM32CubeIDE. Hecho esto, se crearán de forma automática los ficheros que contienen las configuraciones indicadas anteriormente en STM32CubeMX.
A.1 Configuración de STM32CubeMX 73 Figura A.3 Inicialización de los periféricos en modo por defecto en STM32CubeMX. Figura A.4 Ventana de configuración del proyecto creado en STM32CubeMX. Figura A.5 Configuración del I2Cpara el driver del acelerómetro LSM6DS0.
74 Capítulo A. Configuración del entorno y posibles problemas En concreto, el fichero main.c es el destinado a ser modificado por parte del usuario. Este fichero será generado cada vez que se realicen modificaciones sobre el archivo ".io" asociado al proyecto por lo que el usuario deberá escribir dentro de las regiones encapsuladas para ello, es decir, aquéllas enmarcas entre USER CODE BEGIN yUSER CODE END. Todo código de usuario no escrito entre estas regiones se perderá en caso de modificación del archivo ".io" y no podrá ser recuperado. En la figura A.6 se puede observar a la izquierda, parte de los archivos generados por STM32CubeMX y, a la derecha, la plantilla del fichero main.c a modificar. Figura A.6 Sistema de ficheros generados por STM32CubeMX y platilla del fichero main.c. Resulta de particular interés indicar que en este fichero ya existen ciertas funciones creadas con el fin de inicializar el I2Cy la UART. La primera llamada es HAL_Init(). Esta función está definida en stm32f4xx_hal.c y es la encargada de inicializar la librería HAL, a saber: resetear los periféricos, inicializar la interfaz de la Flash y configurar el temporizador de 24 bits de cuenta descendente Systick para generar interrupciones cada milisegundo. Cabe destacar que en este punto, el reloj no está aún configurado con lo que se utiliza como fuente de reloj el oscilador RC interno de 16MHz, HSI (High Speed Internal Oscillator). Código A.1 Función HAL_Init(). HAL_StatusTypeDef HAL_Init(void) { /* Configure Flash prefetch, Instruction cache, Data cache */ #if (INSTRUCTION_CACHE_ENABLE != 0U) __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); #endif /* INSTRUCTION_CACHE_ENABLE */ #if (DATA_CACHE_ENABLE != 0U) __HAL_FLASH_DATA_CACHE_ENABLE(); #endif /* DATA_CACHE_ENABLE */ #if (PREFETCH_ENABLE != 0U) __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
A.1 Configuración de STM32CubeMX 75 #endif /* PREFETCH_ENABLE */ /* Set Interrupt Group Priority */ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */ HAL_InitTick(TICK_INT_PRIORITY); /* Init the low level hardware */ HAL_MspInit(); /* Return function status */ return HAL_OK; } La siguiente función es SystemClock_Config(), encargada de realizar, como su propio nombre indica, la configuración del reloj en base a los datos introducidos por el usuario en STM32CubeMX. Los datos de configuración del reloj se pueden observar en la Figura A.7 mientras que el código generado se puede consultar en el cuadro A.2 Figura A.7 Configuración del Reloj desde la herramienta STM32CubeMX. Código A.2 Función SystemClock_Config(). void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Configure the main internal regulator output voltage */
76 Capítulo A. Configuración del entorno y posibles problemas __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2); /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLM = 16; RCC_OscInitStruct.PLL.PLLN = 336; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; RCC_OscInitStruct.PLL.PLLQ = 7; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK ) { Error_Handler(); } } Después, se encuentra la función de inicialización de la USART mediante la llamada MX_- USART2_UART_Init(), y la correspondiente a la inicialización del I2C mediante la función MX_- I2C1_Init(), tal y como se muestra en los códigos A.3 y A.4, respectivamente. Estos códigos, al igual que el correspondiente a la configuración del reloj, también han sido generados por la herramienta de configuración gráfica STM32CubeMX en base a los parámetros introducidos por el usuario. Código A.3 Función MX_USART2_UART_Init. static void MX_USART2_UART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1;
A.1 Configuración de STM32CubeMX 77 huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart2) != HAL_OK) { Error_Handler(); } } Código A.4 Función MX_I2C1_Init(). static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK { Error_Handler(); } } A.1.2 Red neuronal Para introducir la red neuronal, desde STM32CubeMX, se debe seleccionar "Software Packs −→ Select Components" o bien pulsar las teclas "Alt+o", tal y como se ve en la Figura A.8 Seguidamente, se abrirá la ventana "Software Packs Component Selector" donde el usuario tiene la posibilidad de seleccionar software adicional que puede instalar en su proyecto (Figura A.9). Para el desarrollo de este trabajo se ha instalado el pack STMicroelectronics.X-CUBE-AI. Una vez instalado, desde esta misma ventana, se deberá hacer click sobre el paquete anterior, luego sobre X-CUBE-AI y después, marcar la casilla Core. El desplegable de Application se dejará tal y como aparece por defecto a "Not selected". Estas configuraciones aparecen en la Figura A.9. Después, en el menú de la izquierda de la ventana del proyecto se debe seleccionar "Software Packs" y aparecerá un desplegable en el cual se debe hacer click sobre STMicroelectronics.X- CUBE-AI.5.2.01 . Aparecerá ahora a la derecha, bajo la pestaña Mode , la casilla Artificial Intelligence X-CUBE-AI, la cual deberá ser seleccionada, apareciendo ahora bajo Configuration , toda la configuración relativa a la red neuronal, tal y como se muestra en la Figura A.10 En este punto, el usuario debe dar un nombre a la red 2 , seleccionar el tipo de fichero y cargar el modelo del cual se quiere hacer uso. 1La versión puede haber cambiado en función de cuando esté el lector revisando este documento. 2En este proyecto, el nombre dado a la red es acc_neural_model
84 Capítulo A. Configuración del entorno y posibles problemas Código A.6 Archivo retarget.c. // All credit to Carmine Noviello for this code // https://github.com/cnoviello/mastering-stm32/blob/master/nucleof030R8/system/src/retarget/retarget.c #include <_ansi.h> #include <_syslist.h> #include <errno.h> #include <sys/time.h> #include <sys/times.h> #include <limits.h> #include <signal.h> #include <../Inc/retarget.h> #include <stdint.h> #include <stdio.h> #if !defined(OS_USE_SEMIHOSTING) #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 UART_HandleTypeDef *gHuart; void RetargetInit(UART_HandleTypeDef *huart) { gHuart = huart; /* Disable I/O buffering for STDOUT stream, so that * chars are sent out as soon as they are printed. */ setvbuf(stdout, NULL, _IONBF, 0); } int _isatty(int fd) { if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) return 1; errno = EBADF; return 0; } int _write(int fd, char* ptr, int len) { HAL_StatusTypeDef hstatus; if (fd == STDOUT_FILENO || fd == STDERR_FILENO) { hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY); if (hstatus == HAL_OK)
A.2 Posibles problemas 85 return len; else return EIO; } errno = EBADF; return -1; } int _close(int fd) { if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) return 0; errno = EBADF; return -1; } int _lseek(int fd, int ptr, int dir) { (void) fd; (void) ptr; (void) dir; errno = EBADF; return -1; } int _read(int fd, char* ptr, int len) { HAL_StatusTypeDef hstatus; if (fd == STDIN_FILENO) { hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY ); if (hstatus == HAL_OK) return 1; else return EIO; } errno = EBADF; return -1; } int _fstat(int fd, struct stat* st) { if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) { st->st_mode = S_IFCHR; return 0; } errno = EBADF; return 0; } #endif //#if !defined(OS_USE_SEMIHOSTING)
Apéndice B Códigos principales En este apéndice, se añade el código completo de los programas principales desarrollados en este proyecto y no comentados en apartados anteriores B.1 main.c Código B.1 Código completo del archivo main.c. /* Includes ------------------------------------------------------------------*/ #include "main.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ //Para la red neuronal //SI SE INVIERTE DE LUGAR CON LSM6DS0_CUSTOM.h FALLA #include "ai_platform.h" #include "acc_neural_model.h" #include "acc_neural_model_data.h" //Para el accelerometro #include "LSM6DS0_CUSTOM.h" //Includes Varios #include "retarget.h" //To use Printf #include<stdio.h> /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ 87
88 Capítulo B. Códigos principales /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ CRC_HandleTypeDef hcrc; I2C_HandleTypeDef hi2c1; TIM_HandleTypeDef htim1; TIM_HandleTypeDef htim3; UART_HandleTypeDef huart2; /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART2_UART_Init(void); static void MX_I2C1_Init(void); static void MX_CRC_Init(void); static void MX_TIM1_Init(void); static void MX_TIM3_Init(void); /* USER CODE BEGIN PFP */ void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr); void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale); void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim); static void AI_Init(ai_handle w_addr, ai_handle act_addr); static void AI_Run(float *pIn, float *pOut); static uint32_t argmax(const float * values, uint32_t len); /* USER CODE END PFP */
B.1 main.c 89 /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ //Parametros Red Neuronal ai_handle acc_neural_model2; float aiInData[AI_ACC_NEURAL_MODEL_IN_1_SIZE]; float aiOutData[AI_ACC_NEURAL_MODEL_OUT_1_SIZE]; uint8_t activations2[AI_ACC_NEURAL_MODEL_DATA_ACTIVATIONS_SIZE]; const char* activities[AI_ACC_NEURAL_MODEL_OUT_1_SIZE] = { "Parado", "Caminando", "Corriendo", "Saltando" }; int flagInterrupcion=0; /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ uint32_t write_index = 0; uint16_t timer_value = 0; SensorAxesFloat_t accelerationFloat; /* USER CODE END 1 */ /* MCU Configuration --------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */
90 Capítulo B. Códigos principales MX_GPIO_Init(); MX_USART2_UART_Init(); MX_I2C1_Init(); MX_CRC_Init(); MX_TIM1_Init(); MX_TIM3_Init(); /* USER CODE BEGIN 2 */ Inicializar_LSM6DS0(&hi2c1,952); RetargetInit(&huart2); printf("\r\n Iniciando Aplicacion de Reconocimiento...\r\n"); AI_Init(ai_acc_neural_model_data_weights_get(), activations2); printf("Esperando 4 segundos para comenzar...\r\n"); HAL_Delay(4000); printf("Haz un movimiento!\r\n"); HAL_Delay(1000); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ //Iniciamos el timer HAL_TIM_Base_Start_IT(&htim1); while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(flagInterrupcion==1){ flagInterrupcion=0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_MID); aiInData[write_index + 0] = (float) accelerationFloat.AXIS_X/ 4000.0f; aiInData[write_index + 1] = (float) accelerationFloat.AXIS_Y/ 4000.0f; aiInData[write_index + 2] = (float) accelerationFloat.AXIS_Z/ 4000.0f; /* Para imprimir el los datos del acelerometro */ // printf("%.2f,%.2f,%.2f\r\n", aiInData[write_index + 0],
B.1 main.c 91 // aiInData[write_index + 1], aiInData[write_index + 2]); write_index += 3; if (write_index == AI_ACC_NEURAL_MODEL_IN_1_SIZE) { write_index = 0; printf("Realizando inferencia...\r\n"); HAL_TIM_Base_Start(&htim3); timer_value = __HAL_TIM_GET_COUNTER(&htim3); AI_Run(aiInData, aiOutData); timer_value = __HAL_TIM_GET_COUNTER(&htim3) - timer_value; printf("Tiempo realizando inferencia: %u us\r\n", timer_value) ; /* Output results */ for (uint32_t i = 0; i < AI_ACC_NEURAL_MODEL_OUT_1_SIZE; i++) { printf("%8.6f ", aiOutData[i]); } uint32_t class = argmax(aiOutData, AI_ACC_NEURAL_MODEL_OUT_1_SIZE); printf(": %d - %s\r\n", (int) class, activities[class]); printf("\r\n"); printf("Esperando 4 segundos para nuevo movimiento...\r\n"); HAL_TIM_Base_Stop_IT(&htim1); HAL_Delay(4000); HAL_TIM_Base_Start_IT(&htim1); printf("Haz un movimiento!\r\n"); } } } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
92 Capítulo B. Códigos principales /** Configure the main internal regulator output voltage */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2); /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLM = 16; RCC_OscInitStruct.PLL.PLLN = 336; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; RCC_OscInitStruct.PLL.PLLQ = 7; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK ) { Error_Handler(); } } /** * @brief CRC Initialization Function * @param None * @retval None */ static void MX_CRC_Init(void) { /* USER CODE BEGIN CRC_Init 0 */ /* USER CODE END CRC_Init 0 */ /* USER CODE BEGIN CRC_Init 1 */
B.1 main.c 93 /* USER CODE END CRC_Init 1 */ hcrc.Instance = CRC; if (HAL_CRC_Init(&hcrc) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN CRC_Init 2 */ /* USER CODE END CRC_Init 2 */ } /** * @brief I2C1 Initialization Function * @param None * @retval None */ static void MX_I2C1_Init(void) { /* USER CODE BEGIN I2C1_Init 0 */ /* USER CODE END I2C1_Init 0 */ /* USER CODE BEGIN I2C1_Init 1 */ /* USER CODE END I2C1_Init 1 */ hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN I2C1_Init 2 */ /* USER CODE END I2C1_Init 2 */ } /** * @brief TIM1 Initialization Function * @param None
100 Capítulo B. Códigos principales #define I2C_EXPBD_Timeout 0x1000 #define LSM6DS0_ACC_GYRO_CTRL_REG6_XL 0X20 #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_2G 0.061 /**< Sensitivity value for 2 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_4G 0.122 /**< Sensitivity value for 4 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_8G 0.244 /**< Sensitivity value for 8 g full scale [mg/LSB] */ #define LSM6DS0_ACC_SENSITIVITY_FOR_FS_16G 0.732 /**< Sensitivity value for 16 g full scale [mg/LSB] */ #define LSM6DS0_ACC_GYRO_FS_XL_MASK 0x18 #define LSM6DS0_ACC_GYRO_IF_ADD_INC_ENABLE 0x04 #define LSM6DS0_ACC_GYRO_CTRL_REG8 0X22 #define LSM6DS0_ACC_GYRO_IF_ADD_INC_MASK 0x04 #define CONVERT_MG_TO_MS2 0.00980665f //Mascara para el ODR #define LSM6DS0_ACC_GYRO_ODR_XL_MASK 0xE0 //ESTRUCTURAS typedef struct { int32_t AXIS_X; int32_t AXIS_Y; int32_t AXIS_Z; } SensorAxes_t; typedef struct { float AXIS_X; float AXIS_Y; float AXIS_Z; } SensorAxesFloat_t; typedef enum { LSM6DS0_ACC_GYRO_FS_XL_2g = 0x00, LSM6DS0_ACC_GYRO_FS_XL_16g = 0x08, LSM6DS0_ACC_GYRO_FS_XL_4g = 0x10, LSM6DS0_ACC_GYRO_FS_XL_8g = 0x18, } LSM6DS0_ACC_GYRO_FS_XL_t; //SENSIBILIDAD typedef enum { FS_LOW, FS_MID, FS_HIGH, FS_EXTRA_HIGH
B.3 LSM6DS0_CUSTOM.c 101 } SensorFs_t; typedef enum { LSM6DS0_ACC_GYRO_ODR_XL_10Hz = 0x20, LSM6DS0_ACC_GYRO_ODR_XL_50Hz = 0x40, LSM6DS0_ACC_GYRO_ODR_XL_119Hz = 0x60, LSM6DS0_ACC_GYRO_ODR_XL_238Hz = 0x80, LSM6DS0_ACC_GYRO_ODR_XL_476Hz = 0xA0, LSM6DS0_ACC_GYRO_ODR_XL_952Hz = 0xC0, } LSM6DS0_ACC_GYRO_ODR_XL_t; B.3 LSM6DS0_CUSTOM.c Código B.3 Código completo del archivo LSM6DS0_CUSTOM.c. /** *********************************************************************** * @file : LSM6DS0_CUSTOM.c * @brief : Driver for the accelerometer LSM6DS0. * This file contains the functions to use * the accelerometer LSM6DS0. ********************************************************************** **/ #include "LSM6DS0_CUSTOM.h" #include "main.h" #include "stm32f4xx_hal_i2c.h" //DECLARACIONES DE FUNCIONES void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale); void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr); //FUNCIONES /** * @brief Get the LSM6DS0 accelerometer sensor axes * @param I2C_handler the device handle * @param acceleration pointer where the values of the axes are written * @param fullScale set the full scale value */ void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale) { HAL_StatusTypeDef ret; uint8_t buf[13];
102 Capítulo B. Códigos principales SensorAxes_t aux_acceleration; buf[0]=REG_ACC; ret = HAL_I2C_Master_Transmit(I2C_handler, LSM6DS0_ADDR, buf, 1, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SCL!!\r\n"); } else { ret = HAL_I2C_Master_Receive(I2C_handler, LSM6DS0_ADDR, buf, 2, HAL_MAX_DELAY); if(ret != HAL_OK){ printf("Error SDA!!\r\n"); } else { int16_t dataRaw[3]; uint8_t regValue[6] = {0, 0, 0, 0, 0, 0}; uint8_t i, j, k; uint8_t numberOfByteForDimension; numberOfByteForDimension = 6 / 3; k = 0; for (i = 0; i < 3; i++ ) { for (j = 0; j < numberOfByteForDimension; j++ ) { HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR, LSM6DS0_ACC_GYRO_OUT_X_L_XL + k, I2C_MEMADD_SIZE_8BIT, & regValue[k], 1, I2C_EXPBD_Timeout); k++; } } dataRaw[0] = ((((int16_t)regValue[1]) << 8) + (int16_t)regValue[0]); dataRaw[1] = ((((int16_t)regValue[3]) << 8) + (int16_t)regValue[2]); dataRaw[2] = ((((int16_t)regValue[5]) << 8) + (int16_t)regValue[4]); /* Seleccionar Escala */ float sensitivity = 0; switch( fullScale ) { case FS_LOW: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_2G; break; case FS_MID: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_4G; break;
B.3 LSM6DS0_CUSTOM.c 103 case FS_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_8G; break; case FS_EXTRA_HIGH: sensitivity = LSM6DS0_ACC_SENSITIVITY_FOR_FS_16G; break; default: sensitivity = -1.0f; } /* Calcular valores de salida */ aux_acceleration.AXIS_X = ( int32_t )( dataRaw[0] * sensitivity ) ; aux_acceleration.AXIS_Y = ( int32_t )( dataRaw[1] * sensitivity ) ; aux_acceleration.AXIS_Z = ( int32_t )( dataRaw[2] * sensitivity ) ; acceleration->AXIS_X = ( float ) aux_acceleration.AXIS_X ; acceleration->AXIS_Y = ( float ) aux_acceleration.AXIS_Y ; acceleration->AXIS_Z = ( float ) aux_acceleration.AXIS_Z ; } } } /** * @brief Initialize LSM6DS0 accelerometer sensor axes * @param I2C_handler the device handle * @param odr Number of the output data rate to be configured */ void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr){ LSM6DS0_ACC_GYRO_ODR_XL_t new_odr; uint8_t value; new_odr = ( odr <= 10.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_10Hz : ( odr <= 50.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_50Hz : ( odr <= 119.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_119Hz : ( odr <= 238.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_238Hz : ( odr <= 476.0f ) ? LSM6DS0_ACC_GYRO_ODR_XL_476Hz : LSM6DS0_ACC_GYRO_ODR_XL_952Hz; HAL_I2C_Mem_Read(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); value &= ~LSM6DS0_ACC_GYRO_ODR_XL_MASK; value |= new_odr;
104 Capítulo B. Códigos principales HAL_I2C_Mem_Write(I2C_handler, LSM6DS0_ADDR , LSM6DS0_ACC_GYRO_CTRL_REG6_XL, I2C_MEMADD_SIZE_8BIT, &value, 1, I2C_EXPBD_Timeout); } B.4 CapturarDatos.c Código B.4 Código completo del archivo CapturarDatos.c. /* Includes ------------------------------------------------------------------*/ #include "main.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ //Para el accelerometro #include "LSM6DS0_CUSTOM.h" //Includes Varios #include "retarget.h" //To use Printf #include<stdio.h> //To use FILE /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ CRC_HandleTypeDef hcrc;
B.4 CapturarDatos.c 105 I2C_HandleTypeDef hi2c1; TIM_HandleTypeDef htim1; UART_HandleTypeDef huart2; /* USER CODE BEGIN PV */ /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART2_UART_Init(void); static void MX_I2C1_Init(void); static void MX_CRC_Init(void); static void MX_TIM1_Init(void); /* USER CODE BEGIN PFP */ void Obtener_Ejes_LSM6DS0(I2C_HandleTypeDef *I2C_handler, SensorAxesFloat_t *acceleration, SensorFs_t fullScale); void Inicializar_LSM6DS0(I2C_HandleTypeDef *I2C_handler, float odr); void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim); /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ int flagInterrupcion=0; SensorAxesFloat_t accelerationFloat; SensorAxesFloat_t accelerationAux; /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main (void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration --------------------------------------------------------*/
106 Capítulo B. Códigos principales /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); MX_I2C1_Init(); MX_CRC_Init(); MX_TIM1_Init(); /* USER CODE BEGIN 2 */ Inicializar_LSM6DS0(&hi2c1,952); RetargetInit(&huart2); printf("\r\n Iniciando programa de captura de datos...\r\n"); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ int mode=1; if(mode==1){ HAL_TIM_Base_Start_IT(&htim1); while(1){ if(flagInterrupcion==1){ Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_LOW); accelerationAux.AXIS_X = (float) accelerationFloat.AXIS_X/4000.0f; accelerationAux.AXIS_Y = (float) accelerationFloat.AXIS_Y/4000.0f; accelerationAux.AXIS_Z = (float) accelerationFloat.AXIS_Z/4000.0f; printf("%.2f,%.2f,%.2f\r\n", accelerationAux.AXIS_X, accelerationAux.AXIS_Y, accelerationAux.AXIS_Z); flagInterrupcion=0; } } } if(mode==2){ int mediciones=0; printf("Esperando 6 segundos para comenzar"); HAL_Delay(6000);
B.4 CapturarDatos.c 107 HAL_TIM_Base_Start_IT(&htim1); while(mediciones<1000){ if(flagInterrupcion==1){ flagInterrupcion=0; HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); Obtener_Ejes_LSM6DS0(&hi2c1,&accelerationFloat,FS_LOW); accelerationAux.AXIS_X = (float) accelerationFloat.AXIS_X;/// 4000.0f; accelerationAux.AXIS_Y = (float) accelerationFloat.AXIS_Y;/// 4000.0 f; accelerationAux.AXIS_Z = (float) accelerationFloat.AXIS_Z;/// 4000.0f; printf("%.2f,%.2f,%.2f\r\n", accelerationAux.AXIS_X, accelerationAux.AXIS_Y, accelerationAux.AXIS_Z); mediciones++; } } } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Configure the main internal regulator output voltage */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2); /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; RCC_OscInitStruct.PLL.PLLM = 16; RCC_OscInitStruct.PLL.PLLN = 336;
108 Capítulo B. Códigos principales RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV4; RCC_OscInitStruct.PLL.PLLQ = 7; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } /** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK ) { Error_Handler(); } } /** * @brief CRC Initialization Function * @param None * @retval None */ static void MX_CRC_Init(void) { /* USER CODE BEGIN CRC_Init 0 */ /* USER CODE END CRC_Init 0 */ /* USER CODE BEGIN CRC_Init 1 */ /* USER CODE END CRC_Init 1 */ hcrc.Instance = CRC; if (HAL_CRC_Init(&hcrc) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN CRC_Init 2 */ /* USER CODE END CRC_Init 2 */ } /** * @brief I2C1 Initialization Function
B.4 CapturarDatos.c 109 * @param None * @retval None */ static void MX_I2C1_Init(void) { /* USER CODE BEGIN I2C1_Init 0 */ /* USER CODE END I2C1_Init 0 */ /* USER CODE BEGIN I2C1_Init 1 */ /* USER CODE END I2C1_Init 1 */ hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN I2C1_Init 2 */ /* USER CODE END I2C1_Init 2 */ } /** * @brief TIM1 Initialization Function * @param None * @retval None */ static void MX_TIM1_Init(void) { /* USER CODE BEGIN TIM1_Init 0 */ /* USER CODE END TIM1_Init 0 */ TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; /* USER CODE BEGIN TIM1_Init 1 */ /* USER CODE END TIM1_Init 1 */
116 Índice de Códigos A.2 Función SystemClock_Config() 75 A.3 Función MX_USART2_UART_Init 76 A.4 Función MX_I2C1_Init() 77 A.5 Archivo retarget.h 83 A.6 Archivo retarget.c 84 B.1 Código completo del archivo main.c 87 B.2 Código completo del archivo LSM6DS0_CUSTOM.h 99 B.3 Código completo del archivo LSM6DS0_CUSTOM.c 101 B.4 Código completo del archivo CapturarDatos.c 104
Bibliografía [1] Github stmicroelectronics,https:// github.com/ STMicroelectronics/ stm32ai/ raw/ master/ AI_ resources/ HAR, Diciembre 2020. [2] Google colaboratory,https:// colab.research.google.com/ notebooks/ intro.ipynb, Diciembre 2020. [3] Scikit-learn: Machine learning in python,https:// scikit-learn.org/ stable/ , Diciembre 2020. [4] Stm32l4 discovery kit iot node (b-l475eiot01a),https:// www.st.com/ en/ evaluation-tools/ bl475e-iot01a.html, Diciembre 2020. [5] Tensorflow: An end-to-end open source machine learning platform,https:// www.tensorflow. org/ , Octubre 2020. [6] Crunchbase, Discover innovative companies and the people behind them,https://www. crunchbase.com/ organization/ edge-impulse/ company_financials, Enero 2021. [7] The Apache Software Foundation, Apache freemarker,https:// freemarker.apache.org/ , Enero 2021. [8] Eric Hamilton, What is edge computing: The network edge explained,https:// www.cloudwards. net/ what-is-edge-computing/ , 2018. [9] Shawn Hymel, How to use printf on stm32,https:// shawnhymel.com/ 1873/ how-to-use-printf- on-stm32/ , Noviembre 2020. [10] Edge Impulse, Edge impulse: Making things smarter,https://www.edgeimpulse.com/ , Noviembre 2020. [11] IC Insights Inc., Mcus expected to make modest comeback after 2020 drop, https:// www.icinsights.com/ news/ bulletins/ MCUs-Expected-To-Make-Modest-Comeback- After-2020-Drop--/ , 2020. [12] Keras, The python deep learning api,https:// keras.io/ , Diciembre 2020. [13] Ji Lin, Wei-Ming Chen, John Cohn, Chuang Gan, and Song Han, Mcunet: Tiny deep learning on iot devices, Annual Conference on Neural Information Processing Systems (NeurIPS), 2020. [14] Arm Ltd., Cmsis-pack documentation,https:// www.keil.com/ pack/ doc/ CMSIS/ Pack/ html/ index.html, Enero 2021. 117
118 Bibliografía [15] NUCLEO-F401RE, Stm32 nucleo-64 development board with stm32f401re mcu,https:// www. st.com/ en/ evaluation-tools/ nucleo-f401re.html, Octubre 2020. [16] José María Rodríguez, El ecosistema de tensorflow para programadores principiantes y expertos en machine learning: cursos, lenguajes y edge computing, https:// www.genbeta.com/ desarrollo/ ecosistema-tensorflow-para-programadores- principiantes-expertos-machine-learning-cursos-lenguajes-edge-computing, Enero 2021. [17] VentureBeat SF, Why tinyml is a giant opportunity,https:// venturebeat.com/ 2020/ 01/ 11/ why-tinyml-is-a-giant-opportunity/ , 2021. [18] Software STM32CubeIDE, Integrated development environment for stm32,https://www.st. com/ en/ development-tools/ stm32cubeide.html, Octubre 2020. [19] Software STM32CubeMX, Stm32cube initialization code generator,https:// www.st.com/ en/ development-tools/ stm32cubemx.html, Octubre 2020. [20] STMicroelectronics, Human activity recognition,https:// colab.research.google.com/ github/ STMicroelectronics/ stm32ai/ blob/ master/ AI_resources/ HAR/ Human_Activity_ Recognition.ipynb, Diciembre 2020. [21] ,User manual um1725 - description of stm32f4 hal and low-layer drivers,https:// www.st.com/ resource/ en/ user_manual/ dm00105879-description-of-stm32f4- hal-and-ll-drivers-stmicroelectronics.pdf , Diciembre 2020. [22] ,User manual um2739 - how to create a software pack enhanced for stm32cubemx using stm32 pack creator tool,https:// www.st.com/ resource/ en/ user_manual/ dm00716911- how-to-create-a-software-pack-enhanced-for-stm32cubemx-using-stm32-pack-creator- tool-stmicroelectronics.pdf , Enero 2021. [23] ,Getting started with stm32cube.ai and motion sensing on the stm32l4 iot node,https:// wiki.st.com/ stm32mcu/ wiki/ Getting_started_with_STM32Cube.AI_and_motion_ sensing_on_the_STM32L4_IoT_node, Noviembre 2020. [24] ,Stm32f401xd stm32f401xe arm ® cortex ® -m4 32b mcu+fpu, 105 dmips, 512kb flash/96kb ram, 11 tims, 1 adc, 11 comm. interfaces,https://www.st.com/ resource/ en/ datasheet/ stm32f401re.pdf , Noviembre 2020. [25] ,Ai expansion pack for stm32cubemx,https:// www.st.com/ en/ embedded-software/ xcube-ai.html, Octubre 2020. [26] ,Lsm6ds0 - inemo inertial module: 3d accelerometer and 3d gyroscope,https:// www. st.com/ resource/ en/ datasheet/ lsm6ds0.pdf , Octubre 2020. [27] ,Sensor and motion algorithm software expansion for stm32cube,https:// www.st.com/ en/ embedded-software/ x-cube-mems1.html, Octubre 2020. [28] ,User manual um2526 - getting started with x-cube-ai expansion package for artificial intelligence (ai),https:// www.st.com/ resource/ en/ user_manual/ dm00570145-getting-started- with-xcubeai-expansion-package-for-artificial-intelligence-ai-stmicroelectronics.pdf , Octubre 2020. [29] ,Version 4 legacy sensor and motion algorithm software expansion for stm32cube, https:// www.st.com/ en/ embedded-software/ x-cube-mems1-v4.html, Octubre 2020.
Bibliografía 119 [30] Simon Tatham, Putty software,https:// www.putty.org/ , Octubre 2020. [31] tinyML ® Asia, Big opportunities for tinyml applications: Everywhere and always-on,https: // www.tinyml.org/ asia2020/ slides/ tinyMLAsia2020d1p3-Gousev.pdf , 2020. [32] Harvard University, Tiny machine learning (tinyml),https:// www.edx.org/ professionalcertificate/ harvardx-tiny-machine-learning, 2020. [33] X-NUCLEO-IKS01A1, Motion mems and environmental sensor expansion board for stm32 nucleo,https:// www.st.com/ en/ ecosystems/ x-nucleo-iks01a1.html, Octubre 2020.