Firmware de bricolaje para impresora 3D LCD de fotopolímero. Parte 1

imagen



... o cómo inventé mis propias bicicletas con preferencias y geishas a mi gusto: escribí firmware para una impresora de fotopolímero desde cero. Por el momento, el firmware ya es completamente funcional.



Se tomó como base la placa MKS DLP vendida en Aliexpress, para la cual el fabricante proporciona un circuito y códigos fuente de firmware, que rechacé a favor de escribir todo desde cero.

El artículo resulta muy extenso, así que decidí dividirlo en dos partes. En esta parte, habrá un fondo y una descripción de una GUI casera para una pantalla táctil. Al final, habrá enlaces al tema del acoso en sí y a los repositorios de GitHub.





- Parte 1: 1. Interfaz de usuario.

- Parte 2: 2. Trabajar con el sistema de archivos en una unidad flash USB. 3. Control de motor paso a paso para el movimiento de la plataforma.

- Parte 3: 4. Visualización de imágenes de capas en la pantalla retroiluminada. 5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc. 6. Funciones adicionales para comodidad y conveniencia.



Para una mejor comprensión, daré una descripción muy breve del trabajo de las impresoras 3D LCD de fotopolímero para aquellos que no estén familiarizados con ellas:



Una breve explicación de cómo funcionan la mayoría de las impresoras de fotopolímeros de consumo
— LCD- ( , 5.5" 25601440 ( — 47.25 ). 405 . , FEP-. , «» . , -. , «» , . . , , . , . , .



Antecedentes



¿Cómo llegué a esto y por qué comencé a escribir mi propio firmware en lugar de simplemente ajustar el código fuente del fabricante por mí mismo?



La historia de fondo resultó ser larga, así que la quité debajo del spoiler.
5 3D-. , , . FDM-, — Anet A8. - , , . - — - , , . , . - — Anycubic Photon S. , .



, , «» — , . , .., FDM-. , , — 11565 , :) «» , , , . . «» — . , , 20-30 . , — , . .



. , , .. , , , , , . . , (), . , , . - . , 3D- — MKS DLP. : , (5.5", 25601440) (3.5", 480320). — ! , , .



, , . , - , , . . , . … -, CMSIS HAL ST ( STM32F407). -, Marlin 3D. — Marlin 3D — FDM 3D-. 6 , , , G- - . 3 . . — G- . , . , FDM- .



, GUI- , . , , - .



Entonces, lo que tenemos:



  • Kit MKS DLP, que incluye: placa base, pantalla de interfaz de 3,5 "480x320 y pantalla de retroiluminación de 5,5" 2560x1440
  • fuentes nativas del fabricante
  • diagrama de la placa base (sin nombres de valores activos y nominales de componentes pasivos)


La placa base se basa en el microcontrolador STM32F407. Para controlar la pantalla de retroiluminación , la placa contiene un FPGA del fabricante chino GW1N-LV4LQ144ES, SDRAM y dos chips de interfaz SSD2828 MIPI. El microcontrolador conduce la imagen de la capa a la FPGA, la FPGA la almacena en SDRAM y desde allí actualiza la pantalla a través del SSD2828. Por cierto, el fabricante no proporciona la configuración FPGA (firmware) en el código fuente: (Además, la placa base tiene:



  • entrada de energía 12-24 voltios
  • USB A /
  • A4988
  • Z —
  • WiFi
  • FLASH- W25Q64
  • EEPROM- AT24C16


La pantalla de interfaz con un panel táctil resistivo está conectada con un cable plano plano de 40 pines. Controlador de pantalla - ILI9488, controlador de panel táctil - HR2046 (similar a TSC2046).



Para inicializar los periféricos, utilicé el programa STM32CUBE MX. Pero no utilicé el resultado obtenido directamente, sino que inserté las piezas necesarias en mis fuentes. Cuando trabajaba con periféricos, usaba las bibliotecas HAL de ST, y donde necesitaba obtener la máxima velocidad, trabajaba con registros directamente.



Entonces, hay una tarea: este kit debería poder imprimir archivos desde una unidad flash con facilidad para el usuario. Dividí todo este problema aproximadamente en partes principales, lo que resultó en tres publicaciones.



Quiero advertirles de inmediato que ni mi código ni mi enfoque pretenden ser ideales o incluso buenos, juego como puedo. Para mí, programar es más un hobby que una profesión. Así que, por favor, no juzgues por estándares demasiado estrictos.



1. Interfaz de usuario



Primero fue la inicialización de la pantalla. Nada interesante aquí, la secuencia estándar para el controlador ILI9488. Lo arranqué de fuentes nativas, cortando el código de inicialización para otros tipos de pantallas (que, probablemente, permaneció allí desde la vida FDM de estas fuentes). Luego me metí en las fuentes.



1.1 Fuentes



Hay muchas bibliotecas de fuentes para microcontroladores en la red, pero la gran mayoría de ellas funcionan con fuentes monoespaciadas, y eso no me gusta mucho. Esto es cuando todos los caracteres tienen el mismo ancho, como la letra "z", que la letra "i". Una vez escribí una biblioteca de fuentes proporcionales para uno de mis proyectos favoritos. Utiliza dos matrices para cada fuente: una matriz con los datos de bits de los propios caracteres y una matriz con el ancho de cada carácter. Y una pequeña estructura con parámetros de fuente: punteros a matrices, altura de fuente, número de caracteres en la fuente:



typedef struct
{
	uint16_t	*width;
	uint8_t		*data;
	uint8_t		height;
	uint16_t	symcount;
} LCDUI_FONT;


Parecería que tal organización de fuentes debería ocupar más espacio en la memoria que un simple mapa de bits monoespaciado, pero esto no es del todo cierto. Primero, el propio espacio único da lugar a un exceso de datos almacenados. Por ejemplo, si en una fuente de 8 píxeles de alto y 5 píxeles de ancho, 1 byte (1 bit de ancho y 8 bits de alto) sería suficiente para la letra "i", todavía se necesitarán 5 bytes de datos (5 bits de ancho y 8 bits de alto), ya que el ancho es fijo. En segundo lugar, como regla, en tales fuentes, la alineación se realiza en los límites de bytes de cada línea o columna, dependiendo de cómo estén organizados los datos.



Por ejemplo, tome la misma fuente de 5x8. Si los datos de bits se almacenan línea por línea, entonces hay un exceso de 3 bits para cada línea. O 3 bytes por carácter:



imagen



O una fuente de 7x12 con almacenamiento de datos en columnas, entonces hay un exceso de 4 bits por columna o 3,5 bytes por carácter:



imagen



en mi biblioteca, los datos de bits son continuos para un carácter y la alineación en el límite de bytes es solo al final del carácter.



Además, hay un pequeño truco más que le permite reducir ligeramente el tamaño de fuente almacenado: un carácter puede no tener datos de bits, pero se refiere a otro carácter con el mismo estilo. Por ejemplo, las letras cirílicas "A", "B", "E", "K", etc. puede tener una referencia a letras latinas con el mismo estilo. Esto se hace especificando un valor negativo para el ancho del carácter correspondiente en la matriz de anchos de caracteres. Si hay un valor negativo allí, entonces la imagen de este personaje se toma del personaje en posición (ancho * -1).



Este es el procedimiento para encontrar un carácter en una matriz:



uint8_t*	_lcdui_GetCharData(char c)
{
	if (c < 32)
		return 0;
	if (c > 126)
		c -= 65;
	c -= 32;
	if (c >= lcdui_current_font->symcount)
		return 0;
	uint16_t c1 = lcdui_current_font->width[c];
	if (c1 & 0x8000)
		c = (c1 & 0x7FFF);
	uint16_t ch = lcdui_current_font->height;
	int32_t i = 0, ptr = 0, bits = 0, line_bits = ch;
	for (i = 0; i < c; i++)
	{
		if (lcdui_current_font->width[i] & 0x8000)
			continue;
		bits = lcdui_current_font->width[i] * line_bits;
		ptr += bits >> 3;
		if (bits & 0x07)
			ptr++;
	}

	return &(lcdui_current_font->data[ptr]);
}


Todo esto a menudo proporciona incluso una ganancia en la cantidad de datos de la fuente. Sin mencionar que el tipo proporcional parece más natural.



La velocidad de representación de dicha fuente es bastante decente debido a la salida en ventana: primero se le da a la pantalla el comando para limitar la ventana de salida al tamaño del carácter en la posición deseada, y luego se vierten los datos de todo el carácter en una secuencia. No es necesario establecer coordenadas por separado para cada píxel.



Por ejemplo, en la foto de abajo, el texto azul y la línea blanca superior fueron renderizados por mi biblioteca, y la línea inferior blanca - por la biblioteca estándar tipo arduino de las fuentes nativas:



imagen



el texto azul se renderizó varias veces más rápido que la línea blanca inferior.



Al mismo tiempo, tuve que inventar una utilidad para crear matrices de fuentes listas para usar en un programa a partir de una imagen. En Photoshop, se crea una imagen de la altura deseada con todos los caracteres de la fuente, luego se ingresan las coordenadas X de la última columna de cada carácter en el archivo de texto a mano, y luego se establece la utilidad en la imagen y este archivo de texto. Esto crea un archivo .c con las matrices necesarias. Un poco tedioso, por supuesto, pero sencillo.



El procedimiento para mostrar texto es capaz de ajustar el texto a una nueva línea al final de la pantalla o por un carácter de avance de línea encontrado, puede alinearse a la izquierda, derecha y centro, limitar el área más allá de la cual el texto no irá (se cortará). Y es capaz de mostrar símbolos con pintura de fondo con color de fondo o con preservación de fondo. La segunda opción funciona más lentamente, ya que ya no es posible completar los datos de los caracteres en la pantalla en una secuencia, pero sigue siendo lo suficientemente rápido como para que la salida de 3-4 líneas no sea visible a simple vista.



1.2 Visualización de imágenes de la interfaz



Para la interfaz de usuario, debe mostrar imágenes: fondo, iconos, botones. Al principio, decidí no molestarme demasiado y almacenar todas las imágenes en formato .bmp en la memoria flash de 8 MB disponible en la placa. E incluso escribí un procedimiento para esto. El archivo se guarda en formato de 16 bits (R5 G6 B5) con un orden de línea de extremo a extremo o de extremo a extremo, y es posible que ya se haya alimentado directamente a la rutina de renderizado. Pero el tamaño de una imagen de fondo de 480x320 supera los 300 KB. Teniendo en cuenta que parte de esta memoria flash se dedicará a actualizaciones de firmware, 30 imágenes de fondo ocuparán toda la memoria. Parece mucho, pero aún menos de lo que me gustaría tener, por si acaso. Pero también debería haber botones, iconos, etc. Por lo tanto, se decidió convertir las imágenes a algún tipo de formato comprimido.



No hay muchas opciones con compresión: todos los algoritmos que comprimen imágenes más o menos bien requieren una RAM decente (según los estándares de un microcontrolador) o una cantidad decente de tiempo para descomprimir. Las imágenes, por otro lado, deben mostrarse, abriéndose sobre la marcha, y es deseable que la imagen cuando se muestra no se parezca a una barra de progreso que se arrastra. Por lo tanto, me decidí por la compresión RLE: 1 byte codifica el número de repeticiones y las dos siguientes: el color. Para ello, también se escribió una utilidad que convierte archivos .bmp en imágenes comprimidas de esta forma. El encabezado consta de solo 4 bytes, 2 bytes para el ancho y el alto de la imagen. En promedio, las imágenes de fondo se comprimen de esta manera entre 5 y 7 veces, lo que depende en gran medida del tamaño de las áreas monocromáticas (lo que es de esperar). Por ejemplo, una imagen como esta se redujo de los 307 KB originales a 74 KB:



imagen



Pero este, hasta 23 KB del mismo 307:





Por cierto, el diseñador mío es aún más malo que un programador ...



Estaba satisfecho con este resultado. La decodificación y visualización de imágenes es muy rápida: unos 40 milisegundos por imagen de fondo completa. Así que me decidí por esta opción.



Y, por cierto, cambiar al modo DMA para enviar datos a la pantalla no dio casi ninguna aceleración de salida. La pantalla está conectada a través de un bus de datos externo de 16 bits como memoria externa, pero sus tiempos son bastante tristes, lo que casi niega las ventajas de la salida DMA sobre la salida manual píxel por píxel.



1.3 Marco de interfaz gráfica de usuario



Se muestran textos, se dibujan imágenes, ahora es el momento de pensar en cómo se organizará la base de la interfaz de usuario.



Con el panel táctil, todo es simple: el microcontrolador sondea constantemente el controlador del panel táctil mediante interrupciones y promedia los últimos 4 resultados obtenidos, traduciéndolos en coordenadas de pantalla. Por lo tanto, el estado del sensor se conoce en cualquier momento: si se presiona o no, y si se presiona, en qué lugar. Otra capa entre el panel táctil y la parte principal del programa es el procedimiento para procesar los clics de los botones, que ha estado vagando de un proyecto a otro con pequeñas adaptaciones para condiciones específicas durante bastante tiempo.



A continuación, se muestra un breve resumen de cómo funciona.
«». (100-150 ). , «». , . , , «», . , «», «». «», «». - «» «», . ( «»), - . , , .



El panel táctil sirve como único botón de la interfaz, salvo que además del propio hecho de presionarlo, también se analizan las coordenadas en las que se produjo el clic.



Ahora se debe hacer todo para que se puedan mostrar en la pantalla una variedad de elementos de la interfaz, que podrían o no reaccionar a los clics, actualizarse por eventos, tener diferentes tamaños e imágenes, etc.



Finalmente, llegué a este esquema: la interfaz consta de dos tipos principales de elementos: pantallas y botones.



Una pantalla es una especie de contenedor de pantalla completa para botones. La pantalla tiene las siguientes propiedades:



  • imagen de fondo
  • color de fondo
  • forma de dibujar el fondo: rellenar con un color de fondo o mostrar una imagen
  • texto de cabecera
  • color del texto del título
  • fuente del texto del encabezado
  • un puntero a la pantalla principal (a la que volver al cerrar esto)
  • una serie de punteros a botones
  • un puntero a un procedimiento de evento (llamado periódicamente en el ciclo principal del programa)
  • puntero a la rutina de renderizado de pantalla


Estructura de la pantalla
typedef struct
{
	void				*addparameter;

	char				*bgimagename;
	
	void				*prevscreen;
	
	LNG_STRING_ID		name;
	TG_RECT				nameposition;
	TG_TEXTOPTIONS		nameoptions;
	
	uint8_t				btns_count;
	TG_BUTTON			*buttons;
	
	LCDUI_FONT_TYPE		font;
	LCDUI_FONT_TYPE		namefont;
	uint16_t			textcolor;
	uint16_t			nametextcolor;
	uint16_t			backcolor;

	struct {
		paintfunc		_callpaint;	// repaint screen
		processfunc		_process;	// screen process handling (check for changes, touch pressed, etc)
	} funcs;
} TG_SCREEN;




De hecho, los botones pueden ser no solo botones, sino también texto, un icono, algún tipo de elemento cambiante, como un contador o un reloj. Simplemente resultó conveniente combinar todo en un tipo y establecer el comportamiento de cada botón específico a través de sus propiedades.



Propiedades del botón:



  • coordenadas en la pantalla
  • color de fondo
  • imagen de fondo para el estado libre
  • imagen de fondo para el estado presionado
  • imagen de fondo para estado discapacitado
  • imagen de fondo para el estado activo (para el elemento activo de un grupo de botones de opción, por ejemplo)
  • método de reproducción: imagen o color de fondo
  • Ya sea para volver a dibujar el botón cuando se presiona y se suelta
  • botón de texto
  • ( )
  • (, )
  • ( )
  • ,
  • ,


typedef struct
{
	void				*addparameter;
	
	uint8_t				button_id;
	

	int8_t				group_id;		// for swithed options buttons, >0 - single selection from group (select), <0 - multiple selection (switch)
	
	TG_RECT				position;
	
	void				*parentscreen;
	void				*childscreen;

	char				*bgimagename_en;
	char				*bgimagename_press;
	char				*bgimagename_dis;
	char				*bgimagename_act;	// for swithed options buttons

	LNG_STRING_ID		text;
	TG_RECT				textposition;
	LCDUI_FONT_TYPE		font;
	uint16_t			textcolor_en;
	uint16_t			textcolor_press;
	uint16_t			textcolor_dis;
	uint16_t			textcolor_act;	// for swithed options buttons
	uint16_t			backcolor_en;
	uint16_t			backcolor_press;
	uint16_t			backcolor_dis;
	uint16_t			backcolor_act;	// for swithed options buttons
	
	struct {
		uint8_t				active:1;		// for swithed options buttons
		uint8_t				needrepaint:1;
		uint8_t				pressed:1;
		uint8_t				disabled:1;
		uint8_t				repaintonpress:1;		// repaint or not when pressed - for indicate pressed state
		BGPAINT_TYPE		bgpaint:2;
	} options;
	
	TG_TEXTOPTIONS	textoptions;

	struct {
		paintfunc		_call_paint;	// repaint button
		pressfunc		_call_press;	// touch events handling
		pressfunc		_call_longpress;	// touch events handling
		processfunc		_call_process;	// periodical processing (for example text value refresh)
	} funcs;
} TG_BUTTON;




Con la ayuda de este conjunto de propiedades, fue posible crear casi cualquier cosa en la interfaz basada en dicho elemento. Si una pantalla o un botón tiene un puntero a cualquiera de los procedimientos cero, se llama al procedimiento estándar correspondiente. En lugar de un puntero de procedimiento al presionar un botón, por ejemplo, puede haber un identificador especial que indique que debe ir a la pantalla secundaria o anterior, entonces el procedimiento estándar lo hará. En general, los procedimientos estándar cubren casi todos los casos de uso de botones ordinarios y debe crear sus propios procedimientos para un botón solo en casos no estándar, por ejemplo, cuando un botón funciona como un reloj o como un elemento de una lista de archivos.



Pero lo que faltaba en las capacidades de este esquema era para ventanas modales con mensajes o preguntas (como MessageBox en la API de Windows), así que hice un tipo de pantallas por separado para ellas. Sin imágenes de fondo y un tamaño determinado por el título o el mensaje en sí. Estos mensajes se pueden crear en cuatro versiones: con los botones "Sí / No", con los botones "Aceptar / Cancelar", con un botón "Aceptar" o sin ningún botón (como "Espere, se están cargando datos ...").







Estructura del cuadro de mensaje
typedef struct
{
	MSGBOXTYPE			type;
	
	void				*prevscreen;
	
	char				caption[128];
	char				text[512];
	TG_RECT				boxpos;
	
	uint8_t				btns_count;
	TG_BUTTON			buttons[TG_BTN_CNT_MSGBOX];
	
	uint16_t			caption_height;
	
	LCDUI_FONT_TYPE		font_caption;
	LCDUI_FONT_TYPE		font_text;
	uint16_t			text_color;
	uint16_t			box_backcolor;
	uint16_t			capt_textcolor;
	uint16_t			capt_backcolor;
} TG_MSGBOX;




Fue sobre la base de estos tres tipos que se construyó toda la interfaz, resultó ser bastante flexible. Ahora, la inicialización de todos los elementos se lleva a cabo de forma rígida en el firmware, pero existe una idea para brindar a los usuarios la oportunidad de crear su propia interfaz describiendo las propiedades de todos los elementos en el archivo de configuración y agregando una serie de imágenes necesarias. En teoría, será posible cambiar hasta el contenido de diferentes pantallas: qué botones poner en la pantalla principal, qué botones para la pantalla de servicio, etc.



1.4 Multilenguaje







El multilingüismo estaba inicialmente en las tareas. Pero al principio tomé el camino estúpido: al inicializar todos los elementos, les asigné textos de la tabla de idiomas que era la actual. Cambiar el idioma significaba reinicializar todos los elementos de texto, y cuando había más de dos pantallas en la interfaz y más de 20 botones y etiquetas, me di cuenta de que era imposible vivir así por más tiempo. Luego hizo todas las referencias a los textos a través del procedimiento. El procedimiento recibe un identificador de texto como parámetro y devuelve un puntero al texto en el idioma actual:



	char *mshortname = LANG_GetString(LSTR_SHORT_JANUARY);


Al cambiar el idioma, el puntero simplemente cambia de una matriz de textos en el idioma antiguo a una matriz con textos en el nuevo idioma:



void		LANG_SetLanguage(uint8_t lang)
{
	lngCurrent = lngLanguages[lang].strings;
	
	return;
}


Todos los textos fuente están en codificación UTF-8. También tuve que jugar con estas codificaciones. Textos: en UTF-8, archivos cirílicos, en Unicode-16, algunas cadenas, en ANSI normal. No quería incluir en el firmware un conjunto completo de bibliotecas para admitir codificaciones multibyte, por lo que se escribieron varias funciones para convertir de codificación a codificación y para operaciones con textos en diferentes codificaciones, por ejemplo, agregar una cadena UTF-8 al final de una cadena Unicode16.

Agregar un nuevo idioma ahora se ha reducido a crear una tabla de textos en él y cambiar el valor de la constante LNG_LANGS_COUNT. Es cierto que sigue habiendo una pregunta con las fuentes, si el nuevo idioma utiliza símbolos distintos del cirílico y el latín ... Ahora apoyo el ruso y el inglés traducido por Google en el código fuente.



1.5 Almacenamiento de imágenes y otros recursos



Para almacenar grandes recursos, la placa tiene un flash SPI W25Q64 de 8 megabytes. Inicialmente, quería hacer como siempre: establecer un desplazamiento para cada recurso dentro de la memoria flash y guardarlos allí como solo datos binarios. Pero luego me di cuenta de que los problemas con este método me están garantizados tan pronto como el número de recursos guardados supere un par de docenas y quiero cambiar, por ejemplo, alguna imagen que se guarde en sexto lugar en orden. Si su tamaño aumenta, tendrá que cambiar las direcciones de todos los siguientes recursos y reescribirlos. O deje un espacio libre de tamaño desconocido después de cada recurso, quién sabe cómo puede cambiar cualquiera de los recursos. Sí, vi este alboroto en el ataúd :) Así que escupí y organicé un sistema de archivos en este flash.En ese momento, ya tenía un sistema de archivos USB basado en la biblioteca FatFS, por lo que fue suficiente para mí escribir funciones separadas de lectura / escritura de bajo nivel para los sectores. Solo una cosa me molestó un poco: el tamaño del sector borrado en este microcircuito ya es de hasta 4 KB. En primer lugar, esto lleva al hecho de que los archivos ocuparán espacio en porciones de 4 KB (el archivo se escribió 200 bytes; tomó 4 KB de flash), y en segundo lugar, el búfer en la estructura de cada puntero de archivo consumirá los mismos 4 KB de RAM, que en el microcontrolador no es tanto - 192 KB. Por supuesto, uno podría pervertirse y escribir funciones de bajo nivel para poder escribir y leer en porciones más pequeñas, informando sobre el tamaño del sector, por ejemplo, 512 bytes. Pero eso ralentizaría el Flash, por lo que dejó el tamaño del sector en 4 KB.Así que se puede acceder a cualquier recurso simplemente por su nombre de archivo, que resultó ser muy conveniente. En este momento, por ejemplo, la cantidad de recursos almacenados ya superó los 90. E hice su actualización lo más simple posible: los recursos actualizados (o nuevos) se escriben en una unidad flash USB en un directorio determinado, la unidad flash se inserta en la placa, la placa se reinicia en el modo de servicio (durante encienda o reinicie, presione y mantenga presionada la esquina superior derecha de la pantalla) y automáticamente copia todos los archivos que se encuentran en este directorio desde una unidad flash USB a una flash SPI.la placa se reinicia en modo de servicio (durante el encendido o el reinicio, presione y mantenga presionada la esquina superior derecha de la pantalla) y automáticamente copia todos los archivos que se encuentran en este directorio desde la unidad flash USB a la unidad flash SPI.la placa se reinicia en modo de servicio (durante el encendido o el reinicio, presione y mantenga presionada la esquina superior derecha de la pantalla) y automáticamente copia todos los archivos que se encuentran en este directorio desde la unidad flash USB a la unidad flash SPI.







Continuará...



Quizás la parte más voluminosa salió en la interfaz. Si este artículo resulta de interés para la comunidad, en la segunda parte intentaré acomodar todo lo demás.



Bueno, estaré encantado de recibir preguntas y comentarios.



- Parte 1: 1. Interfaz de usuario.

- Parte 2: 2. Trabajar con el sistema de archivos en una unidad flash USB. 3. Control de motor paso a paso para el movimiento de la plataforma.

- Parte 3: 4. Visualización de imágenes de capas en la pantalla retroiluminada. 5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc. 6. Funciones adicionales para comodidad y conveniencia.



Enlaces



Kit MKS DLP en Aliexpress

Fuentes de firmware originales del fabricante en GitHub

Schemes del fabricante de dos versiones de la placa en GitHub

Mis fuentes en GitHub



All Articles