RTThread与CubeMX – (2)修改CubeMX生成的代码


在CubeMX中配置好后,自动生成的代码只需要再添加少量的代码就可以运行了。比如上一篇的点灯,只需要在main函数的大循环中添加几个控制引脚的代码就可以了。HAL库通过systick帮我们实现了delay的功能,只需要调用就行了。

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

CubeMX生成的代码,像上面这样有很多USER CODE BEGIN,USER CODE END的代码,据传这样的代码是为了让CubeMX在原来工程的基础上生成新代码的时候,可以保留这样的注释对中的代码。对于这个功能我没实践过,我只是用CubeMX来生成初始化的代码,还没有试过在代码已经有修改的情况下再次使用CubeMX生成代码。因为CubeMX重新生成代码的时候会将所有的库都重新复制一次,并且修改工程文件,导致所有代码都得重新编译,不是很方便。

为了让后续的rtthread跑起来,还得加入对串口的支持。

CubeMX这样的设计方式是瀑布型的,需要先对整个工程有个全局的考虑,设置好管脚之后生成代码。当你需要调整引脚或是功能的时候,需要回到CubeMX中,重新生成代码,重新走一次之前的流程。

/* USART3 init function */
static void MX_USART3_UART_Init(void)
{

  huart3.Instance = USART3;
  huart3.Init.BaudRate = 115200;
  huart3.Init.WordLength = UART_WORDLENGTH_7B;
  huart3.Init.StopBits = UART_STOPBITS_1;
  huart3.Init.Parity = UART_PARITY_NONE;
  huart3.Init.Mode = UART_MODE_TX_RX;
  huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart3.Init.OverSampling = UART_OVERSAMPLING_16;
  huart3.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
  huart3.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
  if (HAL_UART_Init(&huart3) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

}

比如上面这段代码中,我配置错了UART的比特长度,为了改成正确的8b,按照CubeMX的方式,我需要回到图形化界面,重新生成代码,如果是在一个工程中做到了中期,发现了这个问题,还需要重新生成代码的话,可能会造成意想不到的结果。所以这里我决定直接在源代码中修改,不去理CubeMX了。

在做了上述的修改之后,我们的CubeMX工程和源代码工程就“失步”了。如何能把代码的修改反馈到CubeMX中,我觉得这是CubeMX从一个初始化玩具进化到配置管理工具要解决的问题。图形UI与代码交互式的设计基本上是现代程序设计的主流风格,因为很难有项目能够一开始就把所有的细节都考虑好,交互式的开发风格更符合实际的开发流程。CubeMX可以设计一个编辑器来管理初始化相关的代码,在这个编辑器中分析出用户的修改并反馈到图形界面中。或者是直接用外设初始化的源代码来做为CubeMX的配置管理文件,通过分析源代码来展示UI元素。

前面我们用CubeMX生成了基本的初始化代码,只添加了几行代码就完成了闪灯的功能。后续我们为其添加串口输入输出的功能。串口作为单片机上最为常见的接口,基本上玩不出什么特别的花样了,HAL也是如此。

学习一个新的库最好是先去看他的头文件,通过头文件我们能够知道这个库能提供哪些功能给我们用。通过观察HAL串口库的头文件,前面是一堆的类型和宏定义,这个先不管他。直接看后面对外提供的函数声明。HAL写得还是比较科学,帮我们把这些函数归了类。

/** @addtogroup UART_Exported_Functions_Group1 Initialization and de-initialization functions
  * @{
  */

/* Initialization and de-initialization functions  ****************************/
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_HalfDuplex_Init(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_LIN_Init(UART_HandleTypeDef *huart, uint32_t BreakDetectLength);
HAL_StatusTypeDef HAL_MultiProcessor_Init(UART_HandleTypeDef *huart, uint8_t Address, uint32_t WakeUpMethod);
HAL_StatusTypeDef HAL_RS485Ex_Init(UART_HandleTypeDef *huart, uint32_t Polarity, uint32_t AssertionTime, uint32_t DeassertionTime);
HAL_StatusTypeDef HAL_UART_DeInit (UART_HandleTypeDef *huart);
void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_MspDeInit(UART_HandleTypeDef *huart);

最开始就是initialization和de-initialization类的函数声明,这些函数CubeMX应该会帮我们做好,不然对不起他那么大的名头,这样初始化和资源回收的函数就暂时跳过不管。再看下面,是IO类的函数。

/** @addtogroup UART_Exported_Functions_Group2 IO operation functions
  * @{
  */

/* IO operation functions *****************************************************/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);

前面是各种后缀不同的发送和接收,然后是irq中断处理函数,最后是一堆的callback。收发类的函数有三种,不带后缀的,IT后缀,DMA后缀。如果没猜错分别对应的是:普通收发,阻塞式的;中断的收发,调用完可以不管,结果会通过中断来返回;DMA就是用DMA收发的,不用mcu参与数据搬移。这些收发类函数的数据都是一个指针加长度的形式,ST的库也延续了他一贯的坚决不用const修饰的风格,如果想发字符串常量,继续cast(强制转换)吧。普通形式的收发多了个timeout参数,看来的确是阻塞式的。后面的callback函数看起来不像是被别人调用的,倒像是什么事情被调用了,我的用这个函数来处理的意思。一般设计的callback函数都有一个注册的过程,这里的callback直接声明成一个函数是几个意思。看来光看头文件有点理解不了st得设计风格了,那就去看看c文件吧。虽然我们一般情况下只看头文件来分析别人提供的代码,对于不一般的代码,当然得用不一般的方法。

/**
  * @brief Tx Transfer completed callbacks
  * @param huart: uart handle
  * @retval None
  */
 __weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);

  /* NOTE : This function should not be modified, when the callback is needed,
            the HAL_UART_TxCpltCallback can be implemented in the user file
   */
}

/**
  * @brief  Tx Half Transfer completed callbacks.
  * @param  huart: UART handle
  * @retval None
  */
 __weak void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);

  /* NOTE: This function should not be modified, when the callback is needed,
           the HAL_UART_TxHalfCpltCallback can be implemented in the user file
   */
}

C文件中的callback函数很简单,全是空的,定义的前面有一个__weak标示。不同的编译器对__weak有不同的理解,我用的keil对其的理解是,如果有一个非weak的函数定义,就用这个非weak的定义来替换他。按照这样的写法,我们要用callback的时候直接写一个相应callback的非weak的定义就行了,看这里写的注释也是这么个意思。再回到头文件中,看起来我们了解的函数们已经足以让我们把串口用起来了,那就去代码中试试看。

这个例子中,我用中断方式来收发数据,准备好数据后后台会自动在中断中进行处理。

上面的代码也很简单,将接收到的数据再转发出去。

HAL库的串口和以前标准库的不大一样,标准库没有提供这样的能够收发一段数据的接口,只提供了一个单字节读写的接口,HAL库提供了一个更接近“实际”应用的接口。通常在用中断实现的发送代码中,把要需要发送的数据放到一个缓存中,用一个变量来标记当前已经发送的数据位置,在发送完成或是发送寄存器空的中断中填入新数据,移动标记变量。HAL库也应该会采用这样的思路来设计,下面进入到HAL库的代码中一探究竟。

/**
  * @brief Send an amount of data in interrupt mode.
  * @param huart: UART handle.
  * @param pData: pointer to data buffer.
  * @param Size: amount of data to be sent.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* 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;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Transmit Data Register Empty Interrupt */
    SET_BIT(huart->Instance->CR1, USART_CR1_TXEIE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

在HAL_UART_Transmit_IT函数中,代码还是挺简单的,设置好要发送数据的指针,要发送数据的数量,开启发送中断。代码中会对串口当前的状态进行判断。通常在设计代码的时候,对于可能同时发生的状态会用不同的位来表示。st的库经历了几代变迁后,把接受状态用了一个叫rxstate的变量来表示。

/**
  * @brief Receive an amount of data in interrupt mode.
  * @param huart: UART handle.
  * @param pData: pointer to data buffer.
  * @param Size: amount of data to be received.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* Check that a Rx process is not already ongoing */
  if(huart->RxState == HAL_UART_STATE_READY)
  {
    if((pData == NULL ) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr = pData;
    huart->RxXferSize = Size;
    huart->RxXferCount = Size;

    /* Computation of UART mask to apply to RDR register */
    UART_MASK_COMPUTATION(huart);

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
    SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

    /* Enable the UART Parity Error and Data Register not empty Interrupts */
    SET_BIT(huart->Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

HAL_UART_Receive_IT函数和发送函数差不多,也是把数据缓存备好后打开接收中断。既然中断收发函数都很简单,那复杂的代码一定在中断响应函数中。

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
...........
 /* If no error occurs */
 errorflags = (isrflags & (uint32_t)(USART_ISR_PE | USART_ISR_FE | USART_ISR_ORE | USART_ISR_NE));
 if (errorflags == RESET)
 {
 /* UART in mode Receiver ---------------------------------------------------*/
 if(((isrflags & USART_ISR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
 {
 UART_Receive_IT(huart);
 return;
 }
 }
...........
  /* UART in mode Transmitter ------------------------------------------------*/
  if(((isrflags & USART_ISR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
  {
    UART_Transmit_IT(huart);
    return;
  }

  /* UART in mode Transmitter (transmission end) -----------------------------*/
  if(((isrflags & USART_ISR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
  {
    UART_EndTransmit_IT(huart);
    return;
  }

}

中断响应函数就是对一堆状态的判断,然后执行对应的代码。这里没有分析HAL库对错误的处理,只关注他对接收和发送的处理。HAL库为了通用,对所有的状态都进行了判断,实际在很多的应用中只需要接收和发送中断。在中断响应函数中,发送空中断到来时,调用UART_Transmit_IT函数,接收中断到来时,调用UART_Receive_IT函数。

static HAL_StatusTypeDef UART_Transmit_IT(UART_HandleTypeDef *huart)
{
  uint16_t* tmp;

  /* Check that a Tx process is ongoing */
  if (huart->gState == HAL_UART_STATE_BUSY_TX)
  {

    if(huart->TxXferCount == 0U)
    {
      /* Disable the UART Transmit Data Register Empty Interrupt */
      CLEAR_BIT(huart->Instance->CR1, USART_CR1_TXEIE);

      /* Enable the UART Transmit Complete Interrupt */
      SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);

      return HAL_OK;
    }
    else
    {
      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
      {
        tmp = (uint16_t*) huart->pTxBuffPtr;
        huart->Instance->TDR = (*tmp & (uint16_t)0x01FFU);
        huart->pTxBuffPtr += 2U;
      }
      else
      {
        huart->Instance->TDR = (uint8_t)(*huart->pTxBuffPtr++ & (uint8_t)0xFFU);
      }

      huart->TxXferCount--;

      return HAL_OK;
    }
  }
  else
  {
    return HAL_BUSY;
  }
}
static HAL_StatusTypeDef UART_EndTransmit_IT(UART_HandleTypeDef *huart)
{
  /* Disable the UART Transmit Complete Interrupt */
  CLEAR_BIT(huart->Instance->CR1, USART_CR1_TCIE);

  /* Tx process is ended, restore huart->gState to Ready */
  huart->gState = HAL_UART_STATE_READY;

  HAL_UART_TxCpltCallback(huart);

  return HAL_OK;
}

 

UART_Transmit_IT和UART_EndTransmit_IT是发送中断的处理函数。在UART_Transmit_IT函数中,判断是否还有数据待发送,如果没有则关闭发送empty中断TXEIE,打开发送complete中断TCIE,这样当数据发送完成后会进入到UART_EndTransmit_IT 函数中。如果还有待发数据,那就继续发送。这里的处理逻辑还是挺简单的,发送时先准备好数据,然后打开发送empty中断,进入empty中断后,有数据就发数据,没数据开complete中断,后续进入complete中断后,调用HAL_UART_TxCpltCallback函数。这个cplt函数是weak的,用户可以在自己的代码中重写它,有点虚函数的意味。

static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
  uint16_t* tmp;
  uint16_t uhMask = huart->Mask;

  /* Check that a Rx process is ongoing */
  if(huart->RxState == HAL_UART_STATE_BUSY_RX)
  {

    if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
    {
      tmp = (uint16_t*) huart->pRxBuffPtr ;
      *tmp = (uint16_t)(huart->Instance->RDR & uhMask);
      huart->pRxBuffPtr +=2;
    }
    else
    {
      *huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->RDR & (uint8_t)uhMask);
    }

    if(--huart->RxXferCount == 0)
    {
      /* Disable the UART Parity Error Interrupt and RXNE interrupt*/
      CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE | USART_CR1_PEIE));

      /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
      CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);

      /* Rx process is completed, restore huart->RxState to Ready */
      huart->RxState = HAL_UART_STATE_READY;

      HAL_UART_RxCpltCallback(huart);

      return HAL_OK;
    }

    return HAL_OK;
  }
  else
  {
    /* Clear RXNE interrupt flag */
    __HAL_UART_SEND_REQ(huart, UART_RXDATA_FLUSH_REQUEST);

    return HAL_BUSY;
  }
}

UART_Receive_IT是接收中断的处理函数,逻辑与tx的类似,只不过方向是接收。这个函数接收到指定的数据长度后会关闭接收中断并调用HAL_UART_RxCpltCallback函数。这样的接收机制几乎没什么用处,一般应用场景下接收数据是不可预知的,即使协议长度固定,在线路上也会因为干扰而出现错误包,导致包长变化。如果将每次接收设置为1个字节就退化到了单字节接收模式。如果增加超时机制,可以按照多字节接收,但是会引入定时器,这个和应用密切相关,不好在库中定义。不知道st出于什么原因设计了这样的接收机制,导致串口库的接收部分代码基本上不会在应用中使用。