zephyr-herramientas-shell

Shell de Zephyr: consola de comandos interactiva

  • 4 min

La Shell de Zephyr es una consola interactiva para inspeccionar y controlar el firmware en ejecución.

Hasta ahora, cuando compilamos el firmware, es una “caja negra”. Lo flasheamos y esperamos que funcione. Si algo falla, ponemos printk y rezamos.

Pero, ¿y si pudierais “entrar” dentro del microcontrolador mientras está funcionando? ¿Y si pudierais preguntarle: “¿Cuánta memoria libre queda?”, “¿Qué hilos están corriendo?” o “Enciende el LED 2” escribiendo comandos en una terminal?

Este subsistema convierte vuestra conexión UART (o USB) en una línea de comandos muy parecida a la de Linux o DOS.

Activar la shell

La Shell es un módulo opcional. Para activarla, solo necesitamos añadir un par de líneas a nuestro prj.conf.

# Activamos el subsistema Shell
CONFIG_SHELL=y

# Elegimos el backend (por dónde sale). Normalmente UART.
CONFIG_SHELL_BACKEND_SERIAL=y

# Comandos de kernel y dispositivos usados en este artículo
CONFIG_KERNEL_SHELL=y
CONFIG_DEVICE_SHELL=y
CONFIG_THREAD_MONITOR=y

# Historial navegable con las flechas del teclado
CONFIG_SHELL_HISTORY=y

# Opcional: Aumentar el buffer si vamos a escribir comandos largos
CONFIG_SHELL_CMD_BUFF_SIZE=128
Copied!

Al recompilar y flashear (west build -p always ...), abre el monitor serie (PuTTY, Minicom o la extensión de VSCode) y pulsa Enter. Verás un prompt interactivo:

uart:~$
Copied!

¡Ya estamos dentro!

Comandos integrados

Los comandos disponibles dependen de las opciones Kconfig activadas. Escribe help para ver los que incluye tu compilación.

kernel version y kernel uptime

Lo básico. Nos dice qué versión del OS corre y cuánto tiempo lleva encendido.

kernel threads

Este es el comando más importante. Muestra una tabla en tiempo real con todos los hilos del sistema, su uso de pila (Stack) y su prioridad.

uart:~$ kernel threads
Scheduler: 335 since last call
 Thread       Pri   Stack   %   Of   State
*idle          48     128  33  384   running
 main           0     420  20 2048   suspend
 logging       14     200  19 1024   suspend
 shell_uart    14     512  25 2048   ready
Copied!

Si ves que el porcentaje de stack (%) de algún hilo se acerca al 90 %, estás a punto de sufrir un desbordamiento. Aumenta el stack en el código.

device list

Muestra los dispositivos registrados y su estado. Las opciones necesarias ya están incluidas en el prj.conf anterior: CONFIG_KERNEL_SHELL=y, CONFIG_DEVICE_SHELL=y y CONFIG_THREAD_MONITOR=y.

Historial

Con CONFIG_SHELL_HISTORY=y, puedes usar las flechas arriba y abajo para recuperar comandos anteriores.

Shells de periféricos

Además de los comandos del Kernel, podemos activar comandos específicos para probar hardware sin escribir código C. Esto es utilísimo para validar una PCB recién fabricada.

En prj.conf:

CONFIG_GPIO_SHELL=y
CONFIG_I2C_SHELL=y
Copied!

Ahora puedes hacer cosas como:

  • Leer un pin: gpio get gpio@50000000 13 (Leemos el pin 13 del puerto 0).
  • Escanear I2C: i2c scan i2c@40003000 (Busca dispositivos en el bus).

Los nombres extraños como gpio@50000000 son las etiquetas internas de los dispositivos. Puedes ver los nombres correctos usando device list.

Crear comandos propios

Lo mejor de la Shell es que puedes añadir tus propios comandos personalizados.

Vamos a crear un comando app status para imprimir variables de la aplicación y otro app say_hello que reciba un argumento.

El código necesario en tu main.c es sorprendentemente sencillo:

#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>

/* Función que se ejecuta con el comando 'status' */
static int cmd_status(const struct shell *sh, size_t argc, char **argv)
{
    /* Usamos shell_print en lugar de printk para que salga por la shell */
    shell_print(sh, "Estado del sistema: OK. Temperatura: 24.5C");
    return 0;
}

/* Función que se ejecuta con el comando 'say_hello' */
/* argc es el número de argumentos, argv es el array de argumentos */
static int cmd_say_hello(const struct shell *sh, size_t argc, char **argv)
{
    /* argv[0] es el nombre del comando. argv[1] es el primer argumento. */
    shell_print(sh, "Hola %s!", argv[1]);
    return 0;
}

/* Definimos los subcomandos (el nivel más bajo) */
SHELL_STATIC_SUBCMD_SET_CREATE(sub_app,
    SHELL_CMD(status, NULL, "Muestra el estado", cmd_status),
    SHELL_CMD_ARG(say_hello, NULL, "Saluda a alguien. Uso: say_hello <nombre>", cmd_say_hello, 2, 0),
    SHELL_SUBCMD_SET_END /* Marca el final de la lista */
);

/* Registramos el comando raíz 'app' */
SHELL_CMD_REGISTER(app, &sub_app, "Comandos de mi aplicación", NULL);

int main(void) {
    /* ... resto del código ... */
    return 0;
}
Copied!

Ahora, al compilar y ejecutar, puedes escribir:

uart:~$ app status
Estado del sistema: OK. Temperatura: 24.5C

uart:~$ app say_hello Luis
Hola Luis!
Copied!

Colores y formato

La Shell soporta códigos de escape VT100. Esto significa que puedes imprimir en colores.

shell_info(sh, "Esto es información (Verde)");
shell_warn(sh, "Esto es un warning (Amarillo)");
shell_error(sh, "Esto es un error (Rojo)");
Copied!