STM32:Letter Shell串口移植与中断驱动实战,实现高效交互

文章目录

  • 前言
  • 一、Letter Shell源码
  • 二、使用单字节中断
  • 1.shell移植
  • 2.串口配置
  • 三.使用DMA
  • 1.cubemx配置
  • 2.代码
  • 总结

  • 前言

    letter shell是可以在单片机上运行的交互库,可以让我们像在linux下使用shell一样使用单片机。
    我个人认为在单片机上使用shell,更大的意义是通过它帮我们快速实现一个好看点的简易的上位机。

    一、Letter Shell源码

    letter shell 源码
    作者更新到了3.2,如果是使用2.X的版本,他的read和write有所不同。

    二、使用单字节中断

    使用单字节中断方式驱动shell函数,接收串口数据。
    cubemx只需开启串口中断即可。

    1.shell移植

    1.新建Shell目录。
    2.把letter-shell/src下的文件全部复制到新建的Shell目录中。
    3.新建两个文件shell_port.c,shell_port.h。


    4.在shell_port.c中完成init,read,write函数的实现。
    如果是用轮询或者rtos的,需要实现read函数,我们这里因为使用的中断,可以没有read的实现。

    shell_port.c

    #include "shell_port.h"
    #include "usart.h"
    
    Shell shell;
    char shell_buffer[512];
    
    signed short shell_write(char *data, unsigned short len)
    {
        HAL_UART_Transmit(&huart1, (uint8_t *)data, len, 1000);
        return len;
    }
    
    //可以没有
    signed short shell_read(char *data, unsigned short len)
    {
        if (HAL_UART_Receive(&huart1, (uint8_t *)data, len, 1000) == HAL_OK)
        {
            return len;
        }
        return 0;
    }
    
    void shell_port_init(void)
    {
    //可以没有
        shell.write = shell_write;
    //
        shell.read = shell_read;
        shellInit(&shell, shell_buffer, 512);
    }
    

    shell_port.h

    #pragma once
    
    #include "shell.h"
    
    extern Shell shell;
    
    void shell_port_init(void);
    

    5.shell_cfg.h
    将SHELL_TASK_WHILE关闭

    /**
     * @file shell_cfg.h
     * @author Letter (nevermindzzt@gmail.com)
     * @brief shell config
     * @version 3.0.0
     * @date 2019-12-31
     * 
     * @copyright (c) 2019 Letter
     * 
     */
    
    #ifndef __SHELL_CFG_H__
    #define __SHELL_CFG_H__
    
    #ifdef SHELL_CFG_USER
    #include SHELL_CFG_USER
    #endif
    
    #ifndef SHELL_TASK_WHILE
    /**
     * @brief 是否使用默认shell任务while循环
     *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
     *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
     */
    #define     SHELL_TASK_WHILE            0
    #endif /** SHELL_TASK_WHILE */
    
    #ifndef SHELL_USING_CMD_EXPORT
    /**
     * @brief 是否使用命令导出方式
     *        使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令
     *        定义shell命令,关闭此宏的情况下,需要使用命令表的方式
     */
    #define     SHELL_USING_CMD_EXPORT      1
    #endif /** SHELL_USING_CMD_EXPORT */
    
    #ifndef SHELL_USING_COMPANION
    /**
     * @brief 是否使用shell伴生对象
     *        一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象
     */
    #define     SHELL_USING_COMPANION       0
    #endif /** SHELL_USING_COMPANION */
    
    #ifndef SHELL_SUPPORT_END_LINE
    /**
     * @brief 支持shell尾行模式
     */
    #define     SHELL_SUPPORT_END_LINE      0
    #endif /** SHELL_SUPPORT_END_LINE */
    
    #ifndef SHELL_HELP_LIST_USER
    /**
     * @brief 是否在输出命令列表中列出用户
     */
    #define     SHELL_HELP_LIST_USER        0
    #endif /** SHELL_HELP_LIST_USER */
    
    #ifndef SHELL_HELP_LIST_VAR
    /**
     * @brief 是否在输出命令列表中列出变量
     */
    #define     SHELL_HELP_LIST_VAR         0
    #endif /** SHELL_HELP_LIST_VAR */
    
    #ifndef SHELL_HELP_LIST_KEY
    /**
     * @brief 是否在输出命令列表中列出按键
     */
    #define     SHELL_HELP_LIST_KEY         0
    #endif /** SHELL_HELP_LIST_KEY */
    
    #ifndef SHELL_HELP_SHOW_PERMISSION
    /**
     * @brief 是否在输出命令列表中展示命令权限
     */
    #define     SHELL_HELP_SHOW_PERMISSION  1
    #endif /** SHELL_HELP_SHOW_PERMISSION */
    
    #ifndef SHELL_ENTER_LF
    /**
     * @brief 使用LF作为命令行回车触发
     *        可以和SHELL_ENTER_CR同时开启
     */
    #define     SHELL_ENTER_LF              1
    #endif /** SHELL_ENTER_LF */
    
    #ifndef SHELL_ENTER_CR
    /**
     * @brief 使用CR作为命令行回车触发
     *        可以和SHELL_ENTER_LF同时开启
     */
    #define     SHELL_ENTER_CR              1
    #endif /** SHELL_ENTER_CR */
    
    #ifndef SHELL_ENTER_CRLF
    /**
     * @brief 使用CRLF作为命令行回车触发
     *        不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
     */
    #define     SHELL_ENTER_CRLF            0
    #endif /** SHELL_ENTER_CRLF */
    
    #ifndef SHELL_EXEC_UNDEF_FUNC
    /**
     * @brief 使用执行未导出函数的功能
     *        启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
     * @attention 如果地址错误,可能会直接引起程序崩溃
     */
    #define     SHELL_EXEC_UNDEF_FUNC       0
    #endif /** SHELL_EXEC_UNDEF_FUNC */
    
    #ifndef SHELL_PARAMETER_MAX_NUMBER
    /**
     * @brief shell命令参数最大数量
     *        包含命令名在内,超过16个参数并且使用了参数自动转换的情况下,需要修改源码
     */
    #define     SHELL_PARAMETER_MAX_NUMBER  8
    #endif /** SHELL_PARAMETER_MAX_NUMBER */
    
    #ifndef SHELL_HISTORY_MAX_NUMBER
    /**
     * @brief 历史命令记录数量
     */
    #define     SHELL_HISTORY_MAX_NUMBER    5
    #endif /** SHELL_HISTORY_MAX_NUMBER */
    
    #ifndef SHELL_DOUBLE_CLICK_TIME
    /**
     * @brief 双击间隔(ms)
     *        使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔
     */
    #define     SHELL_DOUBLE_CLICK_TIME     200
    #endif /** SHELL_DOUBLE_CLICK_TIME */
    
    #ifndef SHELL_QUICK_HELP
    /**
     * @brief 快速帮助
     *        作用于双击tab的场景,当使能此宏时,双击tab不会对命令进行help补全,而是直接显示对应命令的帮助信息
     */
    #define     SHELL_QUICK_HELP            1
    #endif /** SHELL_QUICK_HELP */
    
    #ifndef SHELL_KEEP_RETURN_VALUE
    /**
     * @brief 保存命令返回值
     *        开启后会默认定义一个`RETVAL`变量,会保存上一次命令执行的返回值,可以在随后的命令中进行调用
     *        如果命令的`SHELL_CMD_DISABLE_RETURN`标志被设置,则该命令不会更新`RETVAL`
     */
    #define     SHELL_KEEP_RETURN_VALUE     0
    #endif /** SHELL_KEEP_RETURN_VALUE */
    
    #ifndef SHELL_MAX_NUMBER
    /**
     * @brief 管理的最大shell数量
     */
    #define     SHELL_MAX_NUMBER            5
    #endif /** SHELL_MAX_NUMBER */
    
    #ifndef SHELL_PRINT_BUFFER
    /**
     * @brief shell格式化输出的缓冲大小
     *        为0时不使用shell格式化输出
     */
    #define     SHELL_PRINT_BUFFER          128
    #endif /** SHELL_PRINT_BUFFER */
    
    #ifndef SHELL_SCAN_BUFFER
    /**
     * @brief shell格式化输入的缓冲大小
     *        为0时不使用shell格式化输入
     * @note shell格式化输入会阻塞shellTask, 仅适用于在有操作系统的情况下使用
     */
    #define     SHELL_SCAN_BUFFER          0
    #endif /** SHELL_SCAN_BUFFER */
    
    #ifndef SHELL_GET_TICK
    /**
     * @brief 获取系统时间(ms)
     *        定义此宏为获取系统Tick,如`HAL_GetTick()`
     * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
     */
    #define     SHELL_GET_TICK()            0
    #endif /** SHELL_GET_TICK */
    
    #ifndef SHELL_USING_LOCK
    /**
     * @brief 使用锁
     * @note 使用shell锁时,需要对加锁和解锁进行实现
     */
    #define     SHELL_USING_LOCK            0
    #endif /** SHELL_USING_LOCK */
    
    #ifndef SHELL_MALLOC
    /**
     * @brief shell内存分配
     *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
     */
    #define     SHELL_MALLOC(size)          0
    #endif /** SHELL_MALLOC */
    
    #ifndef SHELL_FREE
    /**
     * @brief shell内存释放
     *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
     */
    #define     SHELL_FREE(obj)             0
    #endif /** SHELL_FREE */
    
    #ifndef SHELL_SHOW_INFO
    /**
     * @brief 是否显示shell信息
     */
    #define     SHELL_SHOW_INFO             1
    #endif /** SHELL_SHOW_INFO */
    
    #ifndef SHELL_CLS_WHEN_LOGIN
    /**
     * @brief 是否在登录后清除命令行
     */
    #define     SHELL_CLS_WHEN_LOGIN        1
    #endif /** SHELL_CLS_WHEN_LOGIN */
    
    #ifndef SHELL_DEFAULT_USER
    /**
     * @brief shell默认用户
     */
    #define     SHELL_DEFAULT_USER          "letter"
    #endif /** SHELL_DEFAULT_USER */
    
    #ifndef SHELL_DEFAULT_USER_PASSWORD
    /**
     * @brief shell默认用户密码
     *        若默认用户不需要密码,设为""
     */
    #define     SHELL_DEFAULT_USER_PASSWORD "123456"
    #endif /** SHELL_DEFAULT_USER_PASSWORD */
    
    #ifndef SHELL_LOCK_TIMEOUT
    /**
     * @brief shell自动锁定超时
     *        shell当前用户密码有效的时候生效,超时后会自动重新锁定shell
     *        设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
     * @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
     */
    #define     SHELL_LOCK_TIMEOUT          0 * 60 * 1000
    #endif /** SHELL_LOCK_TIMEOUT */
    
    #ifndef SHELL_USING_FUNC_SIGNATURE
    /**
     * @brief 使用函数签名
     *        使能后,可以在声明命令时,指定函数的签名,shell 会根据函数签名进行参数转换,
     *        而不是自动判断参数的类型,如果参数和函数签名不匹配,会停止执行命令
     */
    #define     SHELL_USING_FUNC_SIGNATURE  0
    #endif /** SHELL_USING_FUNC_SIGNATURE */
    
    #ifndef SHELL_SUPPORT_ARRAY_PARAM
    /**
     * @brief 支持数组参数
     *        使能后,可以在命令中使用数组参数,如`cmd [1,2,3]`
     *        需要使能 `SHELL_USING_FUNC_SIGNATURE` 宏,并且配置 `SHELL_MALLOC`, `SHELL_FREE`
     */
    #define     SHELL_SUPPORT_ARRAY_PARAM   0
    #endif /** SHELL_SUPPORT_ARRAY_PARAM */
    
    #endif
    

    6.在shell_ext.h中解决报错(因为使用了size_t)
    #include “stm32f4xx_hal.h”

    shell的移植就算是完成了。

    2.串口配置

    1.开始串口中断,设置串口接收字节地址。
    main.c

    int main(void)
    {
    
      /* USER CODE BEGIN 1 */
    
      /* 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 */
      MX_GPIO_Init();
      MX_USART1_UART_Init();
      MX_USB_DEVICE_Init();
      /* USER CODE BEGIN 2 */
      HAL_UART_Receive_IT(&huart1,&uart1_recv_byte,1);
      shell_port_init();
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }
    
    

    2.实现串口中断回调函数
    stm32f4xx_it.c

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
      if(huart->Instance == USART1)
    	{
        shellHandler(&shell,uart1_recv_byte);
        HAL_UART_Receive_IT(&huart1,&uart1_recv_byte,1);
    	}
    }
    

    三.使用DMA

    使用dma和idle中断的方式。
    具体原理看我的这篇
    STM32: USART + DMA + Idle 中断 + Modbus 通用解析函数
    注意,经过测试,上下左右键这种复合ascii键无法使用,和git bash的用起来差不多

    1.cubemx配置


    2.代码

    shell部分和单字节中断是一样的。
    主要是要在初始化串口时,开始dma idle中断。并且在每次串口空闲中断中调用shellhandler。
    usart.c

    void MX_USART1_UART_Init(void)
    {
    
      /* USER CODE BEGIN USART1_Init 0 */
    
      /* USER CODE END USART1_Init 0 */
    
      /* USER CODE BEGIN USART1_Init 1 */
    
      /* USER CODE END USART1_Init 1 */
      huart1.Instance = USART1;
      huart1.Init.BaudRate = 115200;
      huart1.Init.WordLength = UART_WORDLENGTH_8B;
      huart1.Init.StopBits = UART_STOPBITS_1;
      huart1.Init.Parity = UART_PARITY_NONE;
      huart1.Init.Mode = UART_MODE_TX_RX;
      huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
      huart1.Init.OverSampling = UART_OVERSAMPLING_16;
      if (HAL_UART_Init(&huart1) != HAL_OK)
      {
        Error_Handler();
      }
      /* USER CODE BEGIN USART1_Init 2 */
      Uart_Str.Analysis_function = uart_callback;
      HAL_UARTEx_ReceiveToIdle_DMA(&huart1,Uart_Str.Uart_RecvBuff,sizeof(Uart_Str.Uart_RecvBuff) - 1);
      /* USER CODE END USART1_Init 2 */
    }
    
    static void uart_callback(uint8_t len,uint8_t *data)
    {
      shellHandler(&shell, *data);
    }
    
    

    stm32f4xx_it.c

    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
    {
    	if(huart->Instance == USART1)
    	{
        Uart_Str.Analysis_function(Size,Uart_Str.Uart_RecvBuff);
    		HAL_UARTEx_ReceiveToIdle_DMA(&huart1,Uart_Str.Uart_RecvBuff,sizeof(Uart_Str.Uart_RecvBuff) - 1);
    	}
    }
    

    main函数中就不需要一直轮询shelltask了。

    int main(void)
    {
    
      /* USER CODE BEGIN 1 */
    
      /* 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 */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_USART1_UART_Init();
      MX_USB_DEVICE_Init();
      /* USER CODE BEGIN 2 */
      shell_port_init();
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
        
      }
      /* USER CODE END 3 */
    }
    

    总结

    我个人认为如果是在freertos下,也还是使用中断方式驱动比较好,但是要做好临界保护。
    其实,因为shell的数据是按一个键就会发送一个命令,所以使用单字节中断的方式比较好,使用dma就有点脱裤子放屁了。只有在复制整条命令时,dma会相对有点优势。

    作者:十一月海拒绝河

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32:Letter Shell串口移植与中断驱动实战,实现高效交互

    发表回复