Bootstrap

【stm32单片机】-Hal库-蓝桥杯嵌入式-shell开发

TIPS: 米醋工作室学习笔记,本博客仅学习交流,不提供相关资源
Date:2025.1.24

Shell的组成部分

串口上位机:

  • 普通模式

  • 终端模式

Shell实现的功能:

  • 输入回显功能:在终端中输入(发送)什么字符,就要在终端中显示对应的字符,本质上是单片机对接收到字符的储存。

  • 命令功能判断功能:

    • 单片机什么时候执行命令判断?

      • 当终端按下”回车或换行键“,对应的字符为KaTeX parse error: Expected group as argument to '\r' at end of input: \rKaTeX parse error: Undefined control sequence: \n at position 1: \̲n̲
    • PS:对命令行判断的过程需要在字符串数组末尾加上KaTeX parse error: Undefined control sequence: \0 at position 1: \̲0̲字符,C语言中的字符串为\0结尾,这样做保证了字符串的标准性

    • 函数指针的设计方法:因为我们通常情况下需要定义比较多的命令,如果一个一个去写,代码就会显得比较臃肿,可以通过定义函数指针和函数名的结构体,提高程序的接口化和可拓展性。

  • 退格键:

    • 按下Backspace键,主机给单片机发送字符\b
    • 单片机回传 ‘\b’ ‘空格’ ‘\b’ 三个字符。
      • \b:假设当前终端上显示字符串hello ,光标从o之后移动到o之前。
      • 空格:空格覆盖原来的o,此时光标向右移动一位。
      • \b:光标左移一位,这样,字符串显示hell,并且光标刚好在末尾的l之后。
  • TAB键(自动补全):

  • 输入TAB,Shell能够自动关联命令,并给出提示。

  • 第一次按下:轮询匹配

    • 对终端显示的字符串进行判断,查询匹配项
    • 如果有匹配项,则默认选择第一个命令,并向终端打印所有命令。
  • 第二次按下:进行匹配

  • 历史命令:

    • 上键:
    • 下键:

PS:使用指针数组,减小访存开销。

开发过程问题汇总

1. 调用自定义串口重定向函数(my_printf),在任务较多时,单片机发生死机/卡顿

  • 问题原因:

    我们首先看一下my_printf这个函数的内容

    int my_printf(UART_HandleTypeDef *huart, const char *format, ...) {
        char buffer[512];
        va_list arg;
        int len;
       
        va_start(arg, format);
        len = vsnprintf(buffer, sizeof(buffer), format, arg);
        va_end(arg);
        HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
        return len;
    }
    

    其中HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);

    HAL_UART_Transmit() 是 STM32 HAL 库中用于串口发送数据的函数。它用于通过 UART 发送指定长度的数据缓冲区内容。

    HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
    

    函数参数解释:

    1. UART_HandleTypeDef \*huart:
      • 类型:指向 UART_HandleTypeDef 类型的指针。
      • 作用:该参数是用于指定所使用的 UART 句柄,也就是你配置并初始化的串口。通过该句柄,函数知道要使用哪个串口进行数据传输。
    2. uint8_t \*pData:
      • 类型:指向 uint8_t 类型的指针。
      • 作用:这是一个指向数据缓冲区的指针,数据缓冲区包含要通过 UART 发送的数据。你将要发送的内容需要通过这个指针传递。
    3. uint16_t Size:
      • 类型uint16_t
      • 作用:要发送的数据的大小,以字节为单位。Size 表示你想发送的数据缓冲区的长度。
    4. uint32_t Timeout:
      • 类型uint32_t
      • 作用:指定一个超时时间(以毫秒为单位)。如果 UART 发送数据需要超过这个时间,则会触发超时错误。0xFF 是一个特殊的超时时间值,通常表示“没有超时”或“无限等待”。因此,在你的例子中,Timeout 被设置为 0xFF,表示该函数调用将一直等待,直到数据传输完成为止,不会因超时而返回。

    返回值:

    • HAL_OK:函数成功执行,数据发送完成。
    • HAL_ERROR:函数执行失败,可能是由于硬件问题或其他错误。
    • HAL_BUSY:串口忙,当前无法执行此操作。
    • HAL_TIMEOUT:数据传输超时。

    解释:

    在你的代码中:

    HAL_UART_Transmit(huart, (uint8_t *)buffer, (uint16_t)len, 0xFF);
    
    • huart:是一个指向 UART_HandleTypeDef 的指针,它表示 UART 设备的句柄,通常是在初始化时配置的。
    • (uint8_t \*)buffer:这是一个指向要发送数据的缓冲区的指针,数据会从这个缓冲区中取出并通过 UART 发送。
    • (uint16_t)len:这是一个 uint16_t 类型的参数,表示要发送的字节数。通常是缓冲区的长度。
    • 0xFF:表示无限超时,函数将一直阻塞,直到数据传输完成。

    总结:

    HAL_UART_Transmit() 函数会阻塞当前线程(直到传输完成),通过指定的 huart 对象发送 len 字节的数据,数据存储在 buffer 中。如果你希望函数在传输过程中有超时控制,可以设置合适的超时时间。如果使用 0xFF(即无限超时),则函数会一直等待,直到数据传输完成。

    根据现象分析,my_printf使用的是串口直接发送的原理,会直接阻塞程序,且全程CPU参与,对CPU资源的消耗较大。

    我们可以利用DMA+串口发送重定义的方法,优化这个函数。

    在定义函数前,不要忘记在CubeMX中更新配置

    在这里插入图片描述

    volatile uint8_t dmaTxCompleteFlag = 1;  // DMA 发送完成标志,初始值一定要为1
    
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
        dmaTxCompleteFlag = 1;  // 设置标志,表示 DMA 发送完成
    }
    
    /** @brief 串口打印(DMA)函数
     *  @param huart 串口发送句柄
     *  @param format 方法与printf一致
     *  @note 	为什么要加标志位判断?
     * 			DMA发送和CPU是异步的。
     *  		假设你调用了 my_printf 第一次,开始 DMA 发送数据。此时 DMA 在后台进行传输,
     * 			而 CPU 继续执行后续的代码。没有标志位检查时,如果立即调用 my_printf 第二次,
     * 			第二次的打印数据可能会直接覆盖在正在进行中的 DMA 传输上,导致第二次数据无法正确发送。
     *	
     *	@note 为什么用while等待不用if判断?
     *			举个例子,如果你用if判断发送标志位,1s/次调用此函数发送,没有任何问题
     *			如果你连续两句my_printf呢?这时很可能在执行玩第一句,dma还没那么快处理完,标志位没有被重置1,第二句执行是没有效果的。
     *			dma因为是和cpu并行,很快就能查询到标志位,这个while其实阻塞时间极短。
     *	
     **/
    int my_printf(UART_HandleTypeDef *huart, const char *format, ...) {
        static char buffer[512];
        va_list arg;
        int len;
    	
    	while (dmaTxCompleteFlag == 0);// 如果 DMA 还未完成,等待 DMA 完成
    	
        va_start(arg, format);
        len = vsnprintf(buffer, sizeof(buffer), format, arg);
        va_end(arg);
    
        // 清除标志,表示正在开始新的传输
        dmaTxCompleteFlag = 0;
    
        // 使用 DMA 发送数据
        if (HAL_UART_Transmit_DMA(huart, (uint8_t *)buffer, (uint16_t)len) != HAL_OK) {
            return -1;  // 发送失败
        }
    
        return len;
    }
    

    上面是最终完成版本的代码,在此之前,尝试过未完善的DMA打印函数,如果我们不关心发送中断是否完成,直接使用HAL_UART_Transmit_DMA,这个时候和之前使用HAL_UART_Transmit完全不同,因为使用DMA发送时,DMA和CPU是并行执行的,DMA和CPU也可以看成异步的,HAL_UART_Transmit_DMA不会像HAL_UART_Transmit一样阻塞等待完整的串口发送完成,它只负责清空标志位,将数据送给DMA就可以了,所以很可能会出现:DMA还没来得及处理完数,这边又调用HAL_UART_Transmit_DMA,覆盖了上一次DMA的数据,这样的话,会出现发送不完整甚至乱码的情况。如图所示:

    在这里插入图片描述

所以我们需要对串口发送中断进行检查,进行DMA和CPU的同步机制。即只有DMA发送完成时,我们手动置一个标志位,这个时候才允许我们自定义的打印函数进行打印。DMA相当于承但了数据搬运的角色。4

修改为DMA串口发送后的结果:

  • Shell运行流畅,且lcd和adc的任务不受影响

>   > 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

;