【嵌入式项目_01】letter_shell设备移植以及讲解

0.项目

项目传送门

项目概述:

使用STM32F103ZET裸机移植letter_shell,并且通过STM32cubeMX使用HAL库来完成DMA的传输,并且通过shell来进行测试,同时使用git分别将代码上传到码云中

项目目的:

学习STM32中letter_shell移植、对shell有基本理解、对STM32串口方式(DMA)有所了解、强化使用git技能.


1、项目准备

1.1使用CubeMX创建

打开工程,在主界面->直接点击ACCEE TO MCU SELECTOR
出现芯片型号选择 一般我们直接搜索自己芯片的型号即可 (本项目使用F103ZET)
1.设置RCC:
设置RCC晶振时钟,一般选用外部时钟。
设置高速外部时钟HSE 选择外部时钟源
设置低速外部时钟LSE 选择外部时钟源
2.设置串口:
1.点击USATR1
2.设置MODE为异步通信(Asynchronous)
3.基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能
4.GPIO引脚自动设置 USART1_RX/USART_TX
5.NVIC Settings 一栏使能接收中断
3.设置DMA:
在这里插入图片描述

  1. 点击DMASettings 点击 Add 添加通道
  2. 选择USART_RX USART_TX 传输速率设置为中速
  3. DMA传输模式为正常模式
  4. DMA内存地址自增,每次增加一个Byte(字节)

4.设置时钟:
F103的外部晶振为8MHz

1选择外部时钟HSE 8MHz
2PLL锁相环倍频9倍
3系统时钟来源选择为PLL
4设置APB1分频器为 /2
5 使能CSS监视时钟

5.创建文件:
生成工程代码

1.2裸机移植letter_shell

具体参考《letter-shell | 一个功能强大的嵌入式shell
letter_shell源码

1.3Git管理代码

在项目建立成功之后,用git进行管理,并将代码上传至gitee上.在编译过程中会产生许多,过程文件,这时需要在项目区中增加.gitignore来讲过程文件置入到忽略区中。这样边能将整个项目(不包含中间文件)上传至服务器上。
git具体管理步骤如下:

git init	
touch .gitignore	//在.gitignore中写入忽略项
git add .
git commit -m "MCU雏形"
git remote add origin [url]  //url是需要关联github上面那个仓库的地址

2、学习阶段

2.1DMA学习

DMA:全称Direct Memory Access,直接存储器访问。通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。
DMA传输: DMA传输有四种方式,外设到内存、内存到外设、内存到内存、外设到外设。在传输的过程中,最重要的就是需要传输参数,对应的传输参数也有四种:数据地址、目标地址、传输数据量、传输模式(Normal、Circular)

Normal模式:DMA控制器启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输
Circular模式:同上但是传输到重点之后将重新传输。

传输过程: 在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。DMA传输结束,如果有更多的请求时,外设可以启动下一个周期

一句话概括:DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动

DMA特性:

  • 在同一个DMA设备上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低)
  • 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
  • 支持循环的缓冲器管理
  • 每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个事件标志逻辑或成为一个单独的中断请求。
  • 可编程的数据传输数目:最大为65535

DMA库函数配置:
注:因为使用CubeMX生成代码,其实已经将DMA配置完成,但是还是具体分析一下更好。

下来以CubeMX生成的HAL库中DMA配置来学习:

  1. 初始化DMA:使能时钟、开启中断
void MX_DMA_Init(void)
{
    
    
	__HAL_RCC_DMA1_CLK_ENABLE();	//RCC使能DMA1
  	HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
 	 HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
  	/* DMA1_Channel5_IRQn interrupt configuration */
  	HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
  	HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn)
}
  1. 使能外设DMA通道:CubeMX自动生成DMA通道放在了对应的串口初始化中,进行统一管理
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
    
    
		..........
	/* USART1 DMA Init */
    /* USART1_RX Init */
    hdma_usart1_rx.Instance = DMA1_Channel5;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
    {
    
    
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);	//将DMA与USART1联系起来(接收DMA)

    /* USART1_TX Init */
    hdma_usart1_tx.Instance = DMA1_Channel4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
    
    
      Error_Handler();
    }

    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);	//将DMA与USART1联系起来(发送DMA)

    /* 
    开启串口中断:
    如果不开启串口中断,则程序只能发送一次数据,程序不能判断DMA传输是否完成,
    */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
	..........
}
  1. 初始化DMA通道:设置通道;传输地址;传输方向;传输数据的数目
    STM32中HAL库其实已经封装好了DMA的通道,并且设置了相关传输,具体可以查看HAL库
    发送:HAL_DMA_Start_IT(…)->HAL_UART_Transmit_DMA(…)。
    接收: HAL_DMA_Start_IT(…)->HAL_UART_Transmit_DMA(…)。

如下代码为HAL_UART_Transmit_DMA(),调用HAL_DMA_Start_IT(…)节选(接收同理)

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    
    
  uint32_t *tmp;

  /* Check that a Tx process is not already ongoing */
  if (huart->gState == HAL_UART_STATE_READY)
  {
    
    
    if ((pData == NULL) || (Size == 0U))
    {
    
    
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pTxBuffPtr = pData;
    huart->TxXferSize = Size;
    huart->TxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->gState = HAL_UART_STATE_BUSY_TX;

    /* Set the UART DMA transfer complete callback */
    huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;

    /* Set the UART DMA Half transfer complete callback */
    huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;

    /* Set the DMA error callback */
    huart->hdmatx->XferErrorCallback = UART_DMAError;

    /* Set the DMA abort callback */
    huart->hdmatx->XferAbortCallback = NULL;

    /* Enable the UART transmit DMA channel */
    tmp = (uint32_t *)&pData;
    HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);

    /* Clear the TC flag in the SR register by writing 0 to it */
    __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC);

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the DMA transfer for transmit request by setting the DMAT bit
       in the UART CR3 register */
    SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);

    return HAL_OK;
  }
  else
  {
    
    
    return HAL_BUSY;
  }
}

  1. 其他DMA的API:
__HAL_DMA_GET_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TC4)    //传输完成返回1,否则返回0
__HAL_DMA_CLEAR_FLAG(&UART1TxDMA_Handler,DMA_FLAG_TC4); //清除DMA1通道4传输完成标志
HAL_UART_DMAStop(&UART1_Handler);      					//传输完成以后关闭串口DMA
__HAL_DMA_GET_COUNTER(&UART1TxDMA_Handler);				//得到当前还剩余多少个数据

2.2了解lsh流程

从GitHub上拉下来letter_shell之后一头雾水呢/(ㄒoㄒ)/~~ ,那我们来一步步分析这个letter_shell到底是怎么完成的~

2.2.1接收用户输入

在使用shell的时候,当然第一件事情就是shell要能够,接收到用户的输入,如下所示为接收用户输入API
void shellHandler(Shell *shell, char data);
第一个参数不用解释,用户初始化时候注册的句柄,第二个参数就是接收到的数据。 从这个接口可以看出,lsh是一个字节一个字节送入到接口解析的,从这个接口看,大胆猜测一下。lsh内部应该需要有一个buffer缓存用户的输入,如下为API具体函数

void shellHandler(Shell *shell, char data)
{
    
    
    SHELL_ASSERT(data, return);
    SHELL_LOCK(shell);

#if SHELL_LOCK_TIMEOUT > 0
    if (shell->info.user->data.user.password
        && strlen(shell->info.user->data.user.password) != 0
        && SHELL_GET_TICK())
    {
    
    
        if (SHELL_GET_TICK() - shell->info.activeTime > SHELL_LOCK_TIMEOUT)
        {
    
    
            shell->status.isChecked = 0;
        }
    }
#endif

    /* 根据记录的按键键值计算当前字节在按键键值中的偏移 */
    char keyByteOffset = 24;
    int keyFilter = 0x00000000;
    if ((shell->parser.keyValue & 0x0000FF00) != 0x00000000)
    {
    
    
        keyByteOffset = 0;
        keyFilter = 0xFFFFFF00;
    }
    else if ((shell->parser.keyValue & 0x00FF0000) != 0x00000000)
    {
    
    
        keyByteOffset = 8;
        keyFilter = 0xFFFF0000;
    }
    else if ((shell->parser.keyValue & 0xFF000000) != 0x00000000)
    {
    
    
        keyByteOffset = 16;
        keyFilter = 0xFF000000;
    }

    /* 遍历ShellCommand列表,尝试进行按键键值匹配 */
    ShellCommand *base = (ShellCommand *)shell->commandList.base;
    for (short i = 0; i < shell->commandList.count; i++)
    {
    
    
        /* 判断是否是按键定义并验证权限 */
        if (base[i].attr.attrs.type == SHELL_TYPE_KEY
            && shellCheckPermission(shell, &(base[i])) == 0)
        {
    
    
            /* 对输入的字节同按键键值进行匹配 */
            if ((base[i].data.key.value & keyFilter) == shell->parser.keyValue
                && (base[i].data.key.value & (0xFF << keyByteOffset))
                    == (data << keyByteOffset))
            {
    
    
                shell->parser.keyValue |= data << keyByteOffset;
                data = 0x00;
                if (keyByteOffset == 0 
                    || (base[i].data.key.value & (0xFF << (keyByteOffset - 8)))
                        == 0x00000000)
                {
    
    
                    if (base[i].data.key.function)
                    {
    
    
                        base[i].data.key.function(shell);
                    }
                    shell->parser.keyValue = 0x00000000;
                    break;
                }
            }
        }
    }

    if (data != 0x00)	//接收到回车
    {
    
    
        shell->parser.keyValue = 0x00000000;
        shellNormalInput(shell, data);	//shell 常规输入
    }

    if (SHELL_GET_TICK())
    {
    
    
        shell->info.activeTime = SHELL_GET_TICK();
    }
    SHELL_UNLOCK(shell);

从上述代码大概看出来,其实shell输入函数就主要做了两件事:

  • 解析按键值
  • 将数据缓存到buffer

将数据缓存到buffer:
在接收数据接收到回车符之后,进入shellNormalInput(shell, data);将输入的数据存储到Buffer中,具体实现功能如下:

void shellInsertByte(Shell *shell, char data)
{
    
    
    /* 判断输入数据是否过长 */
    if (shell->parser.length >= shell->parser.bufferSize - 1)
    {
    
    
        shellWriteString(shell, shellText[SHELL_TEXT_CMD_TOO_LONG]);
        shellWritePrompt(shell, 1);
        shellWriteString(shell, shell->parser.buffer);
        return;
    }

    /* 插入数据 */
    if (shell->parser.cursor == shell->parser.length)
    {
    
    
        shell->parser.buffer[shell->parser.length++] = data;
        shell->parser.buffer[shell->parser.length] = 0;
        shell->parser.cursor++;
        shellWriteByte(shell, data);
    }
    else if (shell->parser.cursor < shell->parser.length)
    {
    
    
        for (short i = shell->parser.length - shell->parser.cursor; i > 0; i--)
        {
    
    
            shell->parser.buffer[shell->parser.cursor + i] = 
                shell->parser.buffer[shell->parser.cursor + i - 1];
        }
        shell->parser.buffer[shell->parser.cursor++] = data;
        shell->parser.buffer[++shell->parser.length] = 0;
        for (short i = shell->parser.cursor - 1; i < shell->parser.length; i++)
        {
    
    
            shellWriteByte(shell, shell->parser.buffer[i]);
        }
        for (short i = shell->parser.length - shell->parser.cursor; i > 0; i--)
        {
    
    
            shellWriteByte(shell, '\b');
        }
    }
}

数据缓存部分从3个角度分析:

  1. 数据异常判断:判断数据长度是否超过了buffer大小,如果是,输出错误信息通知用户,然后直接退出
  2. buffer中无待解析数据:插入数据时,当前光标位置和输入数据长度相等,这种情况直接将数据插入到buffer就行,同时更新cursor位置(光标位置)
  3. buffer中有待解析数据:插入数据时,会将当前数据向前平移一个byte,给新的数据腾出空间

上面每次收到数据。都会调用一次shellWriteByte,这里其实是一个回显功能,将用户输入的数据回显出来
### 2.1.2 解析按键值 的

2.2.2解析用户意图

从用户使用场景分析,输入完指令后,按enter按键,shell就会给用户显示想要的执行结果,lsh也是一样,那我们看看shellEnter接口具体做了些啥!

void shellEnter(Shell *shell)	//shell回车处理
{
    
    
    shellExec(shell);
    shellWritePrompt(shell, 1);
}
void shellExec(Shell *shell)	//shell运行命令
{
    
    
    
    if (shell->parser.length == 0)
    {
    
    
        return;
    }

    shell->parser.buffer[shell->parser.length] = 0;

    if (shell->status.isChecked)
    {
    
    
    #if SHELL_HISTORY_MAX_NUMBER > 0
        shellHistoryAdd(shell);
    #endif /** SHELL_HISTORY_MAX_NUMBER > 0 */
        shellParserParam(shell);
        shell->parser.length = shell->parser.cursor = 0;
        if (shell->parser.paramCount == 0)
        {
    
    
            return;
        }
        shellWriteString(shell, "\r\n");

        ShellCommand *command = shellSeekCommand(shell,
                                                 shell->parser.param[0],
                                                 shell->commandList.base,
                                                 0);
        if (command != NULL)
        {
    
    
            shellRunCommand(shell, command);
        }
        else
        {
    
    
            shellWriteString(shell, shellText[SHELL_TEXT_CMD_NOT_FOUND]);
        }
    }
    else
    {
    
    
        shellCheckPassword(shell);
    }
}

其中主要注意下面两个函数:

shellHistoryAdd(shell);
shellParserParam(shell);
shellHistoryAdd 是将指令添加到历史队列中,可以快速执行历史队列中的指令,该接口代码比较简单,就直接略过
shellParaserParam 参数解析接口,lsh支持做多8个参数,这个接口是将参数解析出来放在参数buffer中(这里注意,解析的时候,将指令也当做参数一起解析的,因此参数buffer中的第一个参数就是执行的指令)
参数解析完后,通过shellSeekCommand查询是否指令列表中有该指令,以及指令对应的执行函数

2.2.3执行用户意图

命令解析完之后,lsh匹配指令类型,并执行对应的执行函数,执行接口shellRunCommand的代码如下:

unsigned int shellRunCommand(Shell *shell, ShellCommand *command)
{
    
    
    int returnValue = 0;
    shell->status.isActive = 1;
    if (command->attr.attrs.type == SHELL_TYPE_CMD_MAIN)
    {
    
    
        shellRemoveParamQuotes(shell);
        returnValue = command->data.cmd.function(shell->parser.paramCount,
                                                 shell->parser.param);
        if (!command->attr.attrs.disableReturn)
        {
    
    
            shellWriteReturnValue(shell, returnValue);
        }
    }
    else if (command->attr.attrs.type == SHELL_TYPE_CMD_FUNC)
    {
    
    
        returnValue = shellExtRun(shell,
                                  command,
                                  shell->parser.paramCount,
                                  shell->parser.param);
        if (!command->attr.attrs.disableReturn)
        {
    
    
            shellWriteReturnValue(shell, returnValue);
        }
    }
    else if (command->attr.attrs.type >= SHELL_TYPE_VAR_INT
        && command->attr.attrs.type <= SHELL_TYPE_VAR_NODE)
    {
    
    
        shellShowVar(shell, command);
    }
    else if (command->attr.attrs.type == SHELL_TYPE_USER)
    {
    
    
        shellSetUser(shell, command);
    }
    shell->status.isActive = 0;

    return returnValue;
}

下面以SHELL_TYPE_CMD_FUNC为例,介绍下如何解析命令

int shellExtRun(Shell *shell, ShellCommand *command, int argc, char *argv[])
{
    
    
    unsigned int params[SHELL_PARAMETER_MAX_NUMBER] = {
    
    0};
    int paramNum = command->attr.attrs.paramNum > (argc - 1) ? 
        command->attr.attrs.paramNum : (argc - 1);
    for (int i = 0; i < argc - 1; i++)
    {
    
    
        params[i] = shellExtParsePara(shell, argv[i + 1]);
    }
    switch (paramNum)
    {
    
    
#if SHELL_PARAMETER_MAX_NUMBER >= 1
    case 0:
        return command->data.cmd.function();
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 1 */
#if SHELL_PARAMETER_MAX_NUMBER >= 2
    case 1:
        return command->data.cmd.function(params[0]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 2 */
#if SHELL_PARAMETER_MAX_NUMBER >= 3
    case 2:
        return command->data.cmd.function(params[0], params[1]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 3 */
#if SHELL_PARAMETER_MAX_NUMBER >= 4
    case 3:
        return command->data.cmd.function(params[0], params[1],
                                          params[2]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 4 */
#if SHELL_PARAMETER_MAX_NUMBER >= 5
    case 4:
        return command->data.cmd.function(params[0], params[1],
                                          params[2], params[3]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 5 */
#if SHELL_PARAMETER_MAX_NUMBER >= 6
    case 5:
        return command->data.cmd.function(params[0], params[1],
                                          params[2], params[3],
                                          params[4]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 6 */
#if SHELL_PARAMETER_MAX_NUMBER >= 7
    case 6:
        return command->data.cmd.function(params[0], params[1],
                                          params[2], params[3],
                                          params[4], params[5]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 7 */
#if SHELL_PARAMETER_MAX_NUMBER >= 8
    case 7:
        return command->data.cmd.function(params[0], params[1],
                                          params[2], params[3],
                                          params[4], params[5],
                                          params[6]);
        // break;
#endif /** SHELL_PARAMETER_MAX_NUMBER >= 8 */
#if SHELL_PARAMETER_MAX_NUMBER >= 9
    case 8:
        return command->data.cmd.function(params[0], params[1],
                                          params[2], params[3],
                                          params[4], params[5],
                                          params[6], params[7]);
        // break;
    default:
        return -1;
        // break;
    }
}

其实就是根据参数的数量来选择接口的形式~

这里其实有一个问题:如果用户执行的时候,输入的参数过多或者过少,代码上无法检测,会出现一些不预期的异常。 这里只能通过人工确保

3、手撸项目

其实项目的主要任务就是学习DMA方式以及lsh的一些理解,如下图是项目成果图,导入shell命令,通过DMA来进行串口收发

在主函数中,写了使用DMA发送数据的测试函数,通过shell来进行导出使用。

具体main.c函数代码如下:

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "shell_port.h"

#define SEND_BUF_SIZE 512
uint8_t recv_buf = 0;

void SystemClock_Config(void);

uint8_t dat[SEND_BUF_SIZE];			//发送缓存区
const uint8_t TEXT_TO_SEND[]={
    
    "i'm BSP9 change"};

void test01()
{
    
    
	int i,t = 0;
	for(i = 0;i<sizeof(TEXT_TO_SEND);i++)
	{
    
    
		dat[i] = TEXT_TO_SEND[t];
		t++;
	}
}

int test(int i,char ch,char *str)
{
    
    
	test01();
	HAL_UART_Transmit_DMA(&huart1,(uint8_t*)dat, sizeof(dat)); /*DMA寄存器->DR*/
	return 0;
	
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), 
									DMA_UART, test, test);	//导入到shell命令


int main(void)
{
    
    
  HAL_Init();
  SystemClock_Config();
  
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();	//DMA初始化,使能时钟、开启DMA中断
  MX_USART1_UART_Init();	//串口1初始化,包括DMA配置
 
	HAL_UART_Receive_IT(&huart1, (uint8_t*)&recv_buf, 1);		//使用串口中断接收shell命令

	User_Shell_Init();	//shell初始化,注册读写
	
  while (1)
  {
    
    
 
  }
 
}


void SystemClock_Config(void)
{
    
    
  RCC_OscInitTypeDef RCC_OscInitStruct = {
    
    0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {
    
    0};

  
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    
    
    Error_Handler();
  }
  
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    
    
    Error_Handler();
  }
}


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
    /* 判断是哪个串口触发的中断 */
    if(huart ->Instance == USART1)
    {
    
    
        //调用shell处理数据的接口
			  shellHandler(&shell, recv_buf);
        //使能串口中断接收
			  HAL_UART_Receive_IT(&huart1, (uint8_t*)&recv_buf, 1); //中断接收shell的数据
		}
}
void Error_Handler(void)
{
    
    
  
  __disable_irq();
  while (1)
  {
    
    
  }
  
}

其中在串口中断回调函数中调用shell处理数据接口,并且通过串口中断方式进行接收。如下所示

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
    /* 判断是哪个串口触发的中断 */
    if(huart ->Instance == USART1)
    {
    
    
        //调用shell处理数据的接口
			  shellHandler(&shell, recv_buf);
        //使能串口中断接收
			  HAL_UART_Receive_IT(&huart1, (uint8_t*)&recv_buf, 1); //中断接收shell的数据
		}
}

下面使用串口DMA发送数据,并且导入到shell命令中

void test01()
{
    
    
	int i,t = 0;
	for(i = 0;i<sizeof(TEXT_TO_SEND);i++)
	{
    
    
		dat[i] = TEXT_TO_SEND[t];
		t++;
	}
}

int test(int i,char ch,char *str)
{
    
    
	test01();
	HAL_UART_Transmit_DMA(&huart1,(uint8_t*)dat, sizeof(dat)); /*DMA寄存器->DR*/
	return 0;
	
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), 
									DMA_UART, test, test);	//导入到shell命令

最后实验结果如下所示:
在shell终端显示界面上输入导入的DMA_UART来进行测试DMA收发实验
结果

4、总结

4.1问题总结:

4.1.1和Git打架-.-

在使用git管理版本时,因为前期在gitee库上提交了很多中间文件- -,所以后期需要将中间文件忽略,但是加入.gitignore之后发现还是会上传中间文件,进行如下处理

# 清除记录的所有文件管理信息,注意行末的点表示当前目录,一定要写
git rm -r --cached .
# 添加该仓库的所有文件,注意行末的点表示当前目录,一定要写
git add .
# 提交注释
git commit -m "....."
# 推送到仓库
git push -u origin master

4.1.2和编译器打架-.-

在使用CubeMX生成代码时候,若使用J-Link进行debug时,会发现debug错误,如下所示
在这里插入图片描述
通过询问之后,发现在stm32f1xx_hal_msp.c文件中__HAL_AFIO_REMAP_SWJ_NOJTAG();被开启(可能原因是CubeMX害怕STM32引脚不够而不能去调试)将其注释掉之后,便可以再次进行调试~

4.2心得

  1. 在做项目开始时,遇到不理解的问题截图保存,之后写下方法,这样好做问题解决和回顾
  2. git管理代码时,分布提交,这样把任务由繁化简

猜你喜欢

转载自blog.csdn.net/weixin_45281868/article/details/121610570