La biblioteca estándar Newlib Embedded C y cómo usarla

Al escribir código para una nueva plataforma de hardware, lo último que desea hacer es preocuparse por los detalles de las rutinas de E / S, el manejo de la cadena y otros detalles igualmente tediosos que no tienen nada que ver con el proyecto real. En sistemas más grandes, la biblioteca estándar C tradicionalmente comenzaría a reproducirse aquí.

Para pequeñas plataformas integradas como microcontroladores, los recursos suelen ser lo suficientemente duros como para que una stdlib completa no sea apropiada, razón por la cual existe Newlib: para llevar los beneficios portátiles de una biblioteca estándar a los microcontroladores.

Ya sea que esté usando C, C ++ o MicroPython para programar una MCU, Newlib probablemente esté ahí debajo del capó. Sin embargo, ¿cómo se integra exactamente con el hardware y cómo son las llamadas al sistema (llamadas) para, por ejemplo, ¿Manejo de archivos y entrada / salida?

Engranaje de tocón

La biblioteca estándar C proporciona una serie de títulos que cubren la función disponible. Con cada revisión del estándar C, se agregan nuevos títulos que cubren características adicionales. De estos títulos originales, los más utilizados incluyen:

  • Aquí ya se puede adivinar que cada uno de estos encabezados difiere en lo complicado que es el código a continuación para ingresar a una nueva plataforma, especialmente en el caso de una plataforma MCU sin un sistema operativo (SO). Sin un sistema operativo, no existe una forma obvia de proporcionar acceso fácil a funciones tales como entradas y salidas de texto estándar, o un reloj y calendario del sistema. Esto nos lleva a las muchas funciones de muñón en Newlib.

    En el caso de estamos bastante seguros, porque las cadenas de estilo C y sus operaciones se tratan básicamente de operaciones de memoria, algo para lo que no se necesitan apelaciones especiales. Esto es muy diferente de que contiene funciones relacionadas con el acceso y las operaciones de archivos, así como la entrada y salida hacia y desde la salida o entrada estándar.

    Sin algún código subyacente que conecte la implementación de libc con, por ejemplo, un terminal o almacenamiento, no puede suceder nada para estas funciones de E / S, ya que no hay una acción predeterminada prudente para, por ejemplo, printf() o fopen(). Si queremos usar printf() u otras funciones de producción de texto, el documento Newlib nos dice que necesitamos implementar una función global int _write(int handle, char* data, int size).

    Como indica el nombre "stump", la biblioteca Newlib viene con sus propias implementaciones de stump que no hacen nada, entonces, ¿qué se debe hacer? printf() escribir a algún lugar sensato? Lo más importante a tener en cuenta aquí es que esto depende completamente de la implementación y lo que tiene sentido depende del proyecto específico. A menudo, en una aplicación incorporada, las funciones de salida de texto formateado se utilizarán para generar información de depuración e información similar, en cuyo caso, por ejemplo, la salida a USART.

    En mi marco Nodate, el enfoque elegido fue permitir que el código durante el inicio seleccione un periférico USART específico para enviar la salida, como podemos ver su implementación de la función stub en el módulo IO:

bool stdout_active = false;
USART_devices IO::usart;
int _write(int handle, char* data, int size) {
	if (!stdout_active) { return 0; }
	
	int count = size;
	while (count-- > 0) {
		USART::sendUart(IO::usart, *data);
		data++;
	}
	
	return size;
}

Se proporciona una matriz de caracteres junto con su longitud, que luego transferimos en este caso al USART activo. Debido a que USART transmite bytes individuales, la matriz proporcionada se transmite un byte cada uno.

Debido a que el USART de destino puede cambiar en una plataforma, esto se vuelve personalizable, lo que permite al desarrollador configurar el dispositivo del producto de destino una vez durante el inicio y dinámicamente durante el tiempo de ejecución.

bool IO::setStdOutTarget(USART_devices device) {
	IO::usart = device;	
	stdout_active = true;
	
	return true;
}

Es importante con estas implementaciones de stump que se basen en un enlace de estilo C para encontrar reemplazos. Debido a que en lenguajes como C ++ se utilizan nombres de forma predeterminada, asegúrese de aplicar extern "C" { } bloquear en torno a la implementación completa, o la declaración de avance de la implementación del muñón.

Cuestión de tiempo

Para que la funcionalidad relacionada con el tiempo funcione como se define en , necesitamos una base de tiempo subyacente o al menos una calculadora de la que podamos obtener esta información. El uso de una calculadora del sistema, que contiene el número de milisegundos desde el inicio, no es suficiente para cubrir p. Ej. time(), que requiere la cantidad de segundos desde la era Unix.

Posible implementación de lo siguiente int _times(struct tms* buf) syscall usaría el reloj en tiempo real (RTC) del sistema. Esto también tiene la mayor ventaja de usar un sistema, ya que el RTC se puede dejar funcionar en modo de bajo consumo, lo que permite obtener resultados de sincronización precisos incluso cuando el sistema se pone regularmente en modo de suspensión o incluso se apaga por completo.

En Nodate, esta función se implementa en clock.cpp para STM32, lo que habilita el RTC si aún no se ha iniciado:

int _times(struct tms* buf) {
#if defined RTC_TR_SU
	if (!rtc_pwr_enabled) {
		if (!Rtc::enableRTC()) { return -1; }
		rtc_pwr_enabled = true;
	}
	
	// Fill tms struct from RTC registers.
	// struct tms {
	//		clock_t tms_utime;  /* user time */
	//		clock_t tms_stime;  /* system time */
	//		clock_t tms_cutime; /* user time of children */
	//		clock_t tms_cstime; /* system time of children */
	//	};
	uint32_t tTR = RTC->TR;
	uint32_t ticks = (uint8_t) RTC_Bcd2ToByte(tTR & (RTC_TR_ST | RTC_TR_SU));
	ticks = ticks * SystemCoreClock;
	buf->tms_utime = ticks;
	buf->tms_stime = ticks;
	buf->tms_cutime = ticks;
	buf->tms_cstime = ticks;
	
	return ticks; // Return clock ticks.
#else
	// No usable RTC peripheral exists. Return -1.
	return -1;
#endif 
}

En las MCU basadas en STM32 Cortex-M (excepto STM32F1), los registros de RTC contienen la línea de tiempo en formato BCD (decimal codificado en binario), que requiere que se convierta a código binario para que sea compatible con cualquier código que utilice el _times() función:

uint8_t RTC_Bcd2ToByte(uint8_t Value) {
	uint32_t tmp = 0U;
	tmp = ((uint8_t)(Value & (uint8_t)0xF0) >> (uint8_t)0x4) * 10;
	return (tmp + (Value & (uint8_t)0x0F));
}

Nuevo libro para microcontroladores

Técnicamente, hay dos versiones de Newlib: una es la biblioteca normal y completa, la otra es la versión enana baja en grasa, que fue creada por ARM explícitamente para las MCU Cortex-M en 2013. Una gran desventaja del Newlib regular es que ocupa suficiente espacio, lo que en el caso de MCU especialmente más pequeñas con almacenamiento flash limitado y SRAM probablemente significará que incluso un simple "Hola mundo" compilado con él puede ser demasiado grande para igualarlo.

Al compilar con GCC para una plataforma MCU como STM32 o SAM, se puede indicar al compilador que se vincule con este Newlib nano agregando el archivo específico para usar en el comando bind con --specs=nano.specs. Este archivo específico básicamente garantiza que el proyecto esté vinculado a la biblioteca nano de Newlib y use los archivos de encabezado apropiados.

Como se señaló en el artículo de ARM vinculado, la diferencia de tamaño entre un Newlib normal y la versión enana es bastante dramática. Para un proyecto dirigido a un MCU Cortex-M0 de gama baja, como el STM32F030F4 con un total de 16 kB de rayos y 4 kB de SRAM, usar un Newlib normal es imposible, ya que la imagen de firmware resultante llenará el flash y luego algunos . Con Newlib nano utilizado, los proyectos de demostración básicos proporcionados por Nodate (por ejemplo, Blinky, Pushy) son de alrededor de 2 kB y, por lo tanto, se adaptan cómodamente a Flash y RAM.

Aquí se pueden obtener ahorros de espacio adicionales intercambiando el valor predeterminado printf() implementación que viene con Newlib para optimización, como la implementación printf de mpaland. Esta implementación se utilizó junto con Nodate para completar printf() soporte incluso en estas pequeñas MCU Cortex M0.

Mantenlo limpio

A medida que desarrolla más microcontroladores con recursos limitados, literalmente cada byte cuenta. Debido a que la mayoría de estas MCU son sistemas de un solo núcleo, no requieren el soporte de subprocesos múltiples que sería conveniente cuando se utilizan sistemas de múltiples núcleos (por ejemplo, la familia STM32H7). Se puede observar fácilmente si el reingreso está habilitado al inspeccionar el archivo del proyecto después de construirlo.

Cuando uno ve entradas como impure_data e inscripciones simbólicas similares, a menudo contenidas en lib_a-impure.o o similar, se enlaza un código de recodificación, que puede costar kilobytes de espacio en el peor de los casos. A menudo, este código está vinculado debido a algunas funciones utilizadas en el código del proyecto, pero también puede ser de, por ejemplo, atexit() vigilante. Se puede encontrar una explicación de esta función de reintroducción en el documento Newlib.

Analizar el archivo de mapa directamente o usar una herramienta como MapViewer (solo Windows) puede ayudar a rastrear esas dependencias. Una sugerencia es agregar la bandera -fno-use-cxa-atexit a los indicadores de compilación de GCC, para que no utilice la versión de reentrada del controlador de salida.

Envolvente

Todo esto trata solo con los conceptos básicos de lo que se necesita para integrar y usar Newlib, obviamente. Hay muchos más stumps de syscall que aún no se han manejado, y el manejo de archivos API vale un artículo en sí mismo. La dinámica de Newlib también cambia a medida que pasa de un sistema bare metal, como se explica en este artículo por uno en el que está presente el sistema operativo.

La gestión de procesos es otro tema abordado por el getpid(), fork() y otras funciones del muñón. Aunque parece un poco complicado tener que implementar el propio código aquí incluso para funciones básicas como printf(), también destaca la fuerza de este método, ya que es extremadamente flexible y se adapta con bastante facilidad a cualquier plataforma. Es por eso que Newlib funciona en gran medida sin problemas desde MCU Cortex M-MCU de un solo núcleo con recursos limitados hasta grandes sistemas de múltiples núcleos, incluidas las consolas de juegos.

Imagen del libro: "Bibliotecas públicas en Gales / Llyfrgelloedd Cyhoeddus yng Nghymru" por la Asamblea Nacional de Gales / Cynulliad Cymru tiene licencia CC BY 2.0

  • Navegando gratis dice:

    Es bueno ver una biblioteca con la mayoría de los andamios en su lugar, también puede elegir subconjuntos de características. Muchos de los bloques básicos también estaban en el libro original de k&r, aunque con pocas limitaciones para comprobar si lo recuerdo.

    Ojalá tuviera esto en 85 cuando escribí la mayor parte de Stdlib y algo relacionado de bajo nivel para una mini versión de 16 bits de c basada en la c pequeña de Ron Cain, principalmente en un ensamblador para espacio y velocidad. 32 KB de RAM para tiempo de ejecución y datos. Llegaron a compilarse ante horizontes más interesantes.

  • irox dice:

    ¡Guau, sourceware.org! ¡Eso realmente me trae de vuelta! Es bueno ver que todavía se usa activamente.

    Y si otras personas piensan que "libnew, eso suena familiar", ha existido durante mucho tiempo, la versión 1.6 apareció en 1994.

Pedro Molina
Pedro Molina

Deja una respuesta

Tu dirección de correo electrónico no será publicada.