Criando seu próprio shell para sistemas embarcados

Neste artigo usaremos o Kit NUCLEO-F446RE da ST para demonstrar o uso da lib micro-shell. Mostraremos como criar, receber e tratar comandos vindos pela porta serial.

Usaremos o CubeMX para gerar o código de inicialização e a IDE Atollic para programar nosso Kit com o suporte ao sistema operacional FreeRTOS.

A biblioteca micro-shell usada neste artigo foi baseada na lib-shell criada originalmente pelo Felipe Neves.

O Problema

Muitas vezes durante o processo de desenvolvimento, chegamos ao instante em que precisamos enviar comandos ao nosso microcontrolador.

A primeira opção seria enviar caracteres simples para controle on/off. Se precisarmos indicar o valor de um set point acabamos combinando letras e números como “S:135\n”. Existem várias formas de fazer isso, porém, muitos desses métodos são bem simples e muitas vezes precisamos enviar várias sequências de comandos para realizar uma única configuração.

A segunda opção seria usar um protocolo industrial como o modbus. Porém não basta implementar o protocolo do lado do micro, também é preciso implementar o protocolo do lado do software. Isso pode levar muito tempo e dependendo da aplicação é inviável.

A Solução

A solução parece óbvia: por que não implementar uma lib que conseguisse replicar o que o shell do linux faz em minha aplicação embarcada?

No mundo Unix, o termo Shell é mais usualmente utilizado para se referir aos programas de sistemas do tipo Unix que podem ser utilizados como meio de interação entre interface de usuário para o acesso a serviços do kernel no sistema operacional. Este é um programa que recebe, interpreta e executa os comandos de usuário, aparecendo na tela como uma linha de comandos, representada por um interpretador de comandos, que aguarda na tela os comandos do usuário. (Fonte: wikipedia)

Conversando com o Felipe descobri a biblioteca que ele tinha montado. Acabei alterando a biblioteca original dele para usá-la com FreeRTOS, e esta é a solução que está sendo compartilhada neste artigo.

Principais Funções da Lib Micro-Shell

Abaixo descreveremos um pouco as funções da lib micro-shell.

Macro SHELL_PRINTF: esta macro é usada para indicar qual função será usada como output das respostas do micro-shell. Em nosso exemplo usaremos uma saída serial, mas nada o impede de usar outro tipo de saída, como por exemplo ethernet, usb, etc.

vTaskMicroShell_init: Função usada para criar a task que trata os comandos vindos pela serial e verificar se eles são válidos ou não.

Obs: Ocasionalmente pode acontecer Stack Overflow ao criar a task; para corrigir isso será preciso aumentar o tamanho da stack usada pela task, que é feito através do parâmetro de entrada multi_stack_size.

vTaskShell: Task da aplicação micro-shell, é usado um semáforo binário para acordar e sinalizar a task, para informar que existe um pacote e ele precisa ser tratado.

shell_parser: Função que realiza toda a mágica de parse do pacote recebido. Obs: dedique um tempo para entender bem esta função, ela pode lhe agregar bastante conhecimento.

shell_getc: Função usada para armazenar o byte recebido pela serial, e internamente controla a montagem do pacote recebido. Ao receber o caractere “\n” ou “\r” ele sinaliza a task principal que chegou um comando e que ele precisa ser tratado.


shell_isr_getc: Função parecida com a shell_getc, esta função deve ser usada somente quando a recepção do byte é feita dentro de uma interrupção.

shell_callback: Esta função deve ser implementada por sua aplicação. Após a lib micro-shell tratar e separar as sintaxes do comando recebido, esta função é chamada para você poder tratar cada dado individualmente.

Rodando o Exemplo

No código de exemplo, implementamos um caso de uso onde esta biblioteca pode ser bem útil. O exemplo consiste em informar a sua aplicação IoT, a rede, e o host que deseja se conectar.

O código abaixo mostra a implementação de nossa aplicação dentro da callback ‘shell_callback”, a variável cmd é a string com o comando principal. A variável argc contém o número de argumentos recebidos e a variável argv contém a string dos argumentos já separados na forma de vetor.

void shell_callback(uint8_t *cmd, uint16_t argc, uint8_t **argv)
{
    HAL_StatusTypeDef err = HAL_ERROR;

    if (strcmp((const char *)"help", (const char *)cmd) == 0)
    {
        SHELL_PRINTF("Supported commands:");
        SHELL_PRINTF("-> rede -w WIFI_NAME -pwd PASSWORD");
        SHELL_PRINTF("-> connect -h HOST -p PORT\n");
        err = HAL_OK;
    }
    else if (strcmp((const char *)"rede", (const char *)cmd) == 0)
    {
        if (argc == 4)
        {
            if (strcmp((const char *)"-w", (const char *)argv[0]) == 0 &&
                strcmp((const char *)"-pwd", (const char *)argv[2]) == 0)
            {
                snprintf((char *)config.wifi_name, 20, "%s", argv[1]);
                snprintf((char *)config.passwd, 20, "%s", argv[3]);
                err = HAL_OK;
            }
        }
    }
    else if (strcmp((const char *)"connect", (const char *)cmd) == 0)
    {
        if (argc == 4)
        {
            if (strcmp((const char *)"-h", (const char *)argv[0]) == 0 &&
                strcmp((const char *)"-p", (const char *)argv[2]) == 0)
            {
                snprintf((char *)config.host, 20, "%s", argv[1]);
                config.port = atoi((const char *)argv[3]);
                err = HAL_OK;
            }
        }
    }
    else if (strcmp((const char *)"show", (const char *)cmd) == 0)
    {
        if (argc == 1)
        {
            if (strcmp((const char *)"-cfg", (const char *)argv[0]) == 0)
            {
                SHELL_PRINTF("======== CONFIGS ========");
                SHELL_PRINTF("wifi name: %s", config.wifi_name);
                SHELL_PRINTF("password: %s", config.passwd);
                SHELL_PRINTF("host: %s", config.host);
                SHELL_PRINTF("port: %d", config.port);
                err = HAL_OK;
            }
        }
    }
    else
    {
        SHELL_PRINTF("(X) command not found");
    }

    if (err == HAL_ERROR)
    {
        SHELL_PRINTF("(X) Sintax error");
    }
}

A tabela abaixo mostra como o comando é recebido, armazenado e disponibilizado nas variáveis cmd e argv. Para este exemplo, o número de argumentos da variável argc é 4 (argv[0-3]).

cmd

argv[0]

argv[1]

argv[2]

argv[3]

Caractere de fim de pacote

rede

-w

@Casa

-pwd

123456789

\n ou \r

A figura abaixo mostra a troca de mensagens entre o microcontrolador e o usuário.

Troca de mensagens entre o usuário e a aplicação.

Ao enviar um comando com sintaxe errada podemos ver a aplicação informando a mensagens “(X) Sintax error”.

Conclusão

Neste artigo vimos como usar a lib micro-shell, suas principais vantagens e como usar suas principais funções para aquisitar e tratar diferentes tipos de comandos. Uma segunda opção a esta biblioteca é a lib CLI do FreeRTOS.

Se você gostou do artigo ou tem algo a acrescentar deixe seus comentários.

Código Fonte

No link abaixo segue o código fonte do exemplo mostrado em este artigo.

https://github.com/JorgeGzm/STM32-Micro-Shell

Referências

https://pt.wikipedia.org/wiki/Shell_(computa%C3%A7%C3%A3o)
https://os.mbed.com/users/uLipe/code/TEN_mbedos_simple_shell/file/1e8461adc480/shell.cpp/
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
2 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Wellington Bianchini
Wellington Bianchini
01/04/2019 08:39

Parabéns pelo artigo!

Marco
Marco
26/03/2019 00:14

Um dos problemas muito peculiares em comunicação UART no uso de microcontroladores, perda de dados. Uma boa dica, parabéns .

Home » Software » Criando seu próprio shell para sistemas embarcados

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: