ifree 发表于 2024-3-5 09:12:19

任意GPIO使用TIM+DMA+EXTI模拟UART

任意GPIO用TIM+DMA+EXTI模拟串口
原理
先从一个最简单的需求开始,用任意一个GPIO来模拟串口的TXD引脚,发送数据,数据格式选择8N1,波特率9600。
串口数据帧串口的数据帧格式如图所示:
blob:https://www.armbbs.cn/653615bf-09ed-42ef-9b21-83d836cd3209要用GPIO来模拟TXD引脚,就是按照数据帧的格式要求,按照指定的波特率改变引脚电平。比如以9600波特率,发送数据0x55,就是要在TXD引脚上产生如下波形。blob:https://www.armbbs.cn/29cc448b-cbe5-4b2c-bc02-7d22c64d8de4
GPIO的BSRR寄存器
改变引脚电平,通过GPIO的BSRR寄存器可以方便的对实现。比如:
GPIOD->BSRR= (GPIO_PIN_5) << 16;                // PD5引脚置低GPIOD->BSRR= GPIO_PIN5;                                // PD5引脚置高

按照指定的波特率改变引脚电平
定时中断的方式
9600的波特率,就需要以1/9600的频率改变引脚电平。用定时中断来完成的示意代码如下:
typedef enum {        TX_STATE_IDLE,        TX_STATE_PREP,        TX_STATE_START,        TX_STATE_DATA,        TX_STATE_STOP,}enum_tx_state_t;

uint8_t tx_data;                        // 要发送的数据void putchar(uint8_t x){        tx_data = x;        enable_timer_isr();}void timer_isr(void){        static enum_tx_state_t tx_state = TX_STATE_IDLE;        static uint8_t tx_count = 0;        static uint8_t tx_data;                       
        switch(tx_state)        {                case TX_STATE_IDLE:                        tx_count = 0;                        tx_state = TX_STATE_START;                        break;                case TX_STATE_START:                        TX_PIN_LOW();                        tx_data = get_send_data();                        break;                case TX_STATE_DATA:                        if(tx_data & (1<<tx_count)){                                TX_PIN_HIGH();                        }else{                                TX_PIN_LOW();                        }                        if(++tx_count > 8){                                tx_state = TX_STATE_STOP;                        }                        break;                case TX_STATE_STOP:                default:                        TX_PIN_HIGH();                        tx_state = TX_STATE_IDLE;                        disable_timer_isr();                        break;                }
}
采用中断来模拟的缺点在于,如果要以较高的波特率来传输数据时,timer_isr中断频率太高,9600bps就要求0.1ms中断频率了,如果波特率再提高的话,对系统的造成非常高的负载。
采用TIM+DMA的方式为了降低系统的负载,可以采用的一个方法是,准备一个TxBuffer,使用定时器触发DMA,由DMA将TxBuffer的数据传输到GPIO的BSRR寄存器。比如,传输0x55这个数据,采用8N1的格式,就可以准备这样一个TxBuffer(假设使用的PD3引脚)
uint32_t TxBuffer = {        0x00080000,                                        // 起始位, PD5 = 0        0x00080000,                                        // D0:    PD5 = 0        0x00000008,                                        // D1:    PD5 = 1        0x00080000,                                        // D2:    PD5 = 0        0x00000008,                                        // D3:    PD5 = 1        0x00080000,                                        // D3:    PD5 = 0        0x00000008,                                        // D3:    PD5 = 1        0x00080000,                                        // D3:    PD5 = 0        0x00000008,                                        // D3:    PD5 = 1        0x00000008,                                        // 停止位:PD5 = 1};
传输不同的数据,就是在TxBuffer当中,准备不同的数据。采用DMA的方式后,只需要在整个TxBuffer发送完成后,再响应一次DMA完成中断,这就可以使得中断的频率降低10倍,大大减轻系统的负载。



eric2013 发表于 2024-3-5 11:37:41

谢谢楼主分享,实际批量数据测试,稳定性怎么样。

ifree 发表于 2024-3-5 12:25:40

初次尝试


ST官方有一个示范,用F4写的,我手上时H7的板子,先改到H7来尝试一下。几个代码如下





这个代码目前还不太完善,先看看效果怎么样,后续再尝试更改。

在主任务中执行以下测试代码:
uint8_t aTxBuffer[] = {0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa};
ecomSendBuf(ECOM1, aTxBuffer, 5);

测试效果如下:


可以选择115200的波特率,我的程序中还有较多的其他任务,不知道是否有干扰。定时器时钟是240M,计算115200,波特率有误差,目前不知道是否波特率误差导致误码。
但每次发送的第一个字符都是正确的。

后续再尝试改进,看看是否代码还有问题。

PS:更改ST官方的代码中发现他的代码中TxBuffer是这样定义的
/* UART Emulation Handle */
static UART_Emul_HandleTypeDef      *huart_emul;

/* First Buffer for format data in reception mode */
static uint32_t *pFirstBuffer_Rx;

/* Second Buffer for format data in reception mode */
static uint32_t *pSecondBuffer_Rx;

/* First Buffer for format data in transmission mode */
static uint32_t *pFirstBuffer_Tx;

/* Second Buffer for format data in transmission mode */
static uint32_t *pSecondBuffer_Tx;
这个应该是错的,不应该定义为指针数组,所以我的改过来了,但是官方这个代码没有人测试过吗,测试应该是通不过的才对吧。我改成了:
static UART_Emul_HandleTypeDef      *huart_emul;

/* First Buffer for format data in reception mode */
static uint32_t pFirstBuffer_Rx;

/* Second Buffer for format data in reception mode */
static uint32_t pSecondBuffer_Rx;

/* First Buffer for format data in transmission mode */
static uint32_t pFirstBuffer_Tx;

/* Second Buffer for format data in transmission mode */
static uint32_t pSecondBuffer_Tx;



ifree 发表于 2024-3-5 14:09:29

误码分析


采用115200波特率发送数据时,每一个bit的时间应该1/115200=8.68us。定时器时钟240000000,240000000/115200=2083.3333,无法得到整数结果,
使用2083时得到的实际波特率为115218。波特率误差18/115200 = 0.015%,这个误差应该在可接受的范围内,不至于导致那么大的误码率。


通过逻辑分析仪抓取误码,得到如下结果:



分析其中的错误帧,发现都是一个起始位持续时间有误,怀疑是定时器在触发第一次传输时定时不准确导致的。


修改代码,在HAL_UART_Emul_Transmit_DMA函数中,使能TIM_C1中断之前,加入一句清零定时器计数值的代码
    /* Format first Frame to be sent */
    if (huart->TxXferCount == FIRST_BYTE)
    {
      /* Format Frame to be sent */
      UART_Emul_TransmitFormatFrame(huart, *(pData), (uint32_t*)pFirstBuffer_Tx);

          TimHandle.Instance->CNT = 0;
      /* Enable the Capture compare channel */
      TIM_CCxChannelCmd(TIM1, TIM_CHANNEL_1, TIM_CCx_ENABLE);

      /* Send Frames */
      UART_Emul_TransmitFrame(huart);
    }

现在没有误码了:

ifree 发表于 2024-3-5 16:14:31

TX发送执行流程
TX功能成功后,整理一下Tx有关代码,和代码执行流程:
[*]
用户层代码调用HAL_UART_Emul_Transmit_DMA,本函数,启动一包数据发送。


[*]
[*]调用UART_Emul_TransmitFormatFrame,根据要发送的字符,准备DMA传输需要的TxBuffer
[*]调用UART_Emul_TransmitFrame,设置DMA,启动模拟串口数据收发
[*]DMA在定时器的触发下,将TxBuffer的数据搬运到BSRR寄存器,发送一帧数据,传输完成后,产生中断请求
[*]硬件调用DMA中断函数:UART_EMUL_TX_DMA_IRQHandler
[*]中断调用UART_Emul_DMATransmitCplt函数,若数据包没有发送完毕,启动下一帧数据发送,否则调用数据包完成回调函数
[*]
数据包发送完成,执行回调 HAL_UART_Emul_TxCpltCallback,这是一个weak定义的函数,用户定义该函数,实现数据包发送完成处理逻辑。

发送有关代码如下:
/**
* @briefThis function formats one Frame
* @paramUART Emulation Handle
* @parampdata pinteur in data
* @retval None
*/
static void UART_Emul_TransmitFormatFrame(UART_Emul_HandleTypeDef *huart , uint8_t Data, uint32_t *pBuffer_Tx)
{
    uint32_t counter = 0;
    uint32_t bitmask = 0;
    uint32_t length = 0;
    uint32_t cntparity = 0;

    length = huart->Init.WordLength;                // 待传输的字符帧长度:5,6,7,8,9

    /* Get the Pin Number */
    bitmask = (uint32_t)huart->Init.TxPinNumber;    // 要传输到BSRR寄存器中设置引脚的数据
    /* with no parity */
    if(huart->Init.Parity == 0x00)
    {               
      for (counter = 0; counter < length; counter++)
      {
            if (((Data >> counter)&BitMask) != 0)
            {
                pBuffer_Tx = bitmask;            // PIN = 1
            }
            else
            {
                pBuffer_Tx = (bitmask << 16);    // PIN = 0
            }
      }
    }
    /* with parity */
    else
    {
      for (counter = 0; counter < length-1; counter++)
      {
            if (((Data >> counter)&BitMask) != 0)
            {
                pBuffer_Tx = bitmask;
                cntparity ++;
            }
            else
            {
                pBuffer_Tx = (bitmask << 16);
            }
      }       
    }       
    // 根据设置的校验类型,计算校验需要的bit值
    switch(huart->Init.Parity)
    {
      case UART_EMUL_PARITY_ODD:
      {
            /* Initialize Parity Bit */
            if ((cntparity % 2) != SET)
            {
            pBuffer_Tx = bitmask;
            }
            else
            {
            pBuffer_Tx = (bitmask << 16);
            }
      }
      break;
      case UART_EMUL_PARITY_EVEN:
      {
            /* Initialize Parity Bit */
            if ((cntparity % 2) != SET)
            {
            pBuffer_Tx = (bitmask << 16);
            }
            else
            {
            pBuffer_Tx = bitmask;
            }
      }
      break;
      default:
            break;
    }
    /* Initialize Bit Start 设置起始位 */
    pBuffer_Tx = (bitmask << 16);

    /* Initialize Bit Stop 设置停止位,1位或2位 */
    if (huart->Init.StopBits == UART_EMUL_STOPBITS_1)
    {
      pBuffer_Tx = bitmask;
    }
    else
    {
      pBuffer_Tx = bitmask;
      pBuffer_Tx = bitmask;
    }
    /* Reset counter parity */
    cntparity = 0;
}

/**
* @briefSends an amount of Frames
* @paramhuart: UART Emulation handle
* @parampData: Frame to be sent
* @retval None
*/
static void UART_Emul_TransmitFrame(UART_Emul_HandleTypeDef *huart)
{
    uint32_t tmp_sr = 0;
    uint32_t tmp_ds = 0;
    uint32_t tmp_size = 0;

    if ((huart_emul->TxXferCount % 2 ) != 0)
    {
      tmp_sr = (uint32_t)pFirstBuffer_Tx;
    }
    else
    {
      tmp_sr = (uint32_t)pSecondBuffer_Tx;
    }

    tmp_ds = (uint32_t) & ((huart->TxPortName)->BSRR);
    tmp_size = __HAL_UART_EMUL_FRAME_LENGTH(huart);

    /* Configure DMA Stream data length */
    ((DMA_Stream_TypeDef*)hdma_tx.Instance)->NDTR = tmp_size;
    /* Configure DMA Stream destination address */
    ((DMA_Stream_TypeDef*)hdma_tx.Instance)->PAR = tmp_ds;
    /* Configure DMA Stream source address */
    ((DMA_Stream_TypeDef*)hdma_tx.Instance)->M0AR = tmp_sr;

    /* Enable the transfer complete interrupt */
    __HAL_DMA_ENABLE_IT(&hdma_tx, DMA_IT_TC);
    /* Enable the transfer Error interrupt */
    __HAL_DMA_ENABLE_IT(&hdma_tx, DMA_IT_TE);

    /* Enable the Peripheral */
    __HAL_DMA_ENABLE(&hdma_tx);
    /* Enable the TIM Update DMA request */
    __HAL_TIM_ENABLE_DMA(&TimHandle, TIM_DMA_CC1);
    /* Enable the Peripheral */
    __HAL_TIM_ENABLE(&TimHandle);
}

/**
* @briefSends an amount of data
* @paramhuart: UART Emulation handle
* @parampData: Pointer to data buffer
* @paramSize: Amount of data to be sent
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Emul_Transmit_DMA(UART_Emul_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    uint32_t tmp = 0;

    tmp = huart->State;

    if ((tmp == HAL_UART_EMUL_STATE_READY) || (tmp == HAL_UART_EMUL_STATE_BUSY_RX))
    {
      if ((pData == NULL ) || (Size == 0))
      {
            return HAL_ERROR;
      }

      huart->TxXferSize = Size;
      huart->pTxBuffPtr = pData;
      huart->TxXferCount = 1;
      huart->ErrorCode = HAL_UART_EMUL_ERROR_NONE;

      /* Check if a receive process is ongoing or not */
      if (huart->State == HAL_UART_EMUL_STATE_BUSY_RX)
      {
            huart->State = HAL_UART_EMUL_STATE_BUSY_TX_RX;
      }
      else
      {
            huart->State = HAL_UART_EMUL_STATE_BUSY_TX;
      }
      /* 设置DMA传输完成回调函数 */
      TimHandle.hdma->XferCpltCallback = UART_Emul_DMATransmitCplt;
      /* Set the DMA error callback */
      TimHandle.hdma->XferErrorCallback = UART_Emul_DMAError;

      /* Format first Frame to be sent */
      if (huart->TxXferCount == FIRST_BYTE)
      {
            /* Format Frame to be sent */
            UART_Emul_TransmitFormatFrame(huart, *(pData), (uint32_t*)pFirstBuffer_Tx);

            /* 由于定时器一直处于计数过程中,可能导致第一次DMA触发时间不正确,所以使能TIM1_CC1前先清零定时器 */
            /* 这个方法太粗暴了,可能影响正在进行中的Rx */
            TimHandle.Instance->CNT = 0;

            /* Enable the Capture compare channel */
            TIM_CCxChannelCmd(TIM1, TIM_CHANNEL_1, TIM_CCx_ENABLE);
            /* Send Frames */
            UART_Emul_TransmitFrame(huart);
      }

      if ((huart->TxXferCount == FIRST_BYTE) && (huart->TxXferCount < Size))
      {
            /* Format Second Frame to be sent */
            UART_Emul_TransmitFormatFrame(huart, *(pData + huart->TxXferCount), (uint32_t*)pSecondBuffer_Tx);
      }
      return HAL_OK;         
    }
    else
    {
      return HAL_BUSY;
    }
}

/**
* @briefThis function handles DMA interrupt request for TC.
* @paramNone
* @retval None
*/
void UART_EMUL_TX_DMA_IRQHandler(void)
{
    if (__HAL_DMA_GET_FLAG(TimHandle.hdma, __HAL_DMA_GET_TE_FLAG_INDEX(TimHandle.hdma)) != RESET)
    {
      UART_Emul_DMAError(&hdma_tx);
    }

    /* Clear the transfer complete flag */
    __HAL_DMA_CLEAR_FLAG(TimHandle.hdma, __HAL_DMA_GET_TC_FLAG_INDEX(TimHandle.hdma));

    /* Transfer complete callback */
    TimHandle.hdma->XferCpltCallback(TimHandle.hdma);
}

/**
* @briefThis function is executed in case of Transfer Complete of a Frame.
*         每传输完一个字符帧,调用本函数。
*         若没有传输完全部数据,调用UART_Emul_TransmitFrame启动下一个数据帧传输,
*                            然后调用UART_Emul_TransmitFormatFrame,格式化下一个待传输的帧
*         若传输完全部数据,关闭TIM_CC1,设置TC标志,更新状态,调用传输完成回调函数
* @paramNone
* @retval None
*/
static void UART_Emul_DMATransmitCplt(DMA_HandleTypeDef *hdma)
{
    uint32_t tmpbuffer = 0;

    /* Incremente Counter of frame */
    huart_emul->TxXferCount++;

    if (huart_emul->TxXferCount <= huart_emul->TxXferSize)
    {
      /* Call UART Emulation Transmit frame for next Frame */
      UART_Emul_TransmitFrame(huart_emul);

      if ((huart_emul->TxXferCount % 2 ) != 0)
      {
            tmpbuffer = (uint32_t)pSecondBuffer_Tx;
      }
      else
      {
            tmpbuffer = (uint32_t)pFirstBuffer_Tx;
      }
      /* Format second Data to be sent */
      UART_Emul_TransmitFormatFrame(huart_emul, *(huart_emul->pTxBuffPtr + huart_emul->TxXferCount), (uint32_t*)tmpbuffer);
    }
    else
    {
      /* Disable the transfer complete interrupt */
      __HAL_DMA_DISABLE_IT(TimHandle.hdma, DMA_IT_TC);

      /* Set TC flag in the status register software */
      __HAL_UART_EMUL_SET_FLAG(huart_emul, UART_EMUL_FLAG_TC);

      /* De_Initialize counter frame for Tx */
      huart_emul->TxXferCount = 0;

      /* Initialize the UART Emulation state */
      huart_emul->ErrorCode = HAL_UART_EMUL_ERROR_NONE;

      /* Check if a receive process is ongoing or not */
      if (huart_emul->State == HAL_UART_EMUL_STATE_BUSY_TX_RX)
      {
            huart_emul->State = HAL_UART_EMUL_STATE_BUSY_RX;
      }
      else
      {
            huart_emul->State = HAL_UART_EMUL_STATE_READY;
      }
      /* Handle for UART Emulation Transfer Complete */
      HAL_UART_Emul_TxCpltCallback(huart_emul);                                // user callback
    }
}

/**
* @briefInitializes the UART Emulation Transfer Complete.
* @paramhuart: UART Emulation Handle
* @retval None
*/
__weak void HAL_UART_Emul_TxCpltCallback(UART_Emul_HandleTypeDef *huart)
{
/* NOTE : This function Should not be modified, when the callback is needed,
            the HAL_UART_Emul_TransferComplete could be implemented in the user file
   */
}

2859932063 发表于 2024-3-5 16:17:49

这个通过TIM中断让DMA来输出高低电平来做模拟串口?中断的频率还是那么快啊,DMA减轻了不少CPU的时间。

ifree 发表于 2024-3-5 17:12:24

2859932063 发表于 2024-3-5 16:17
这个通过TIM中断让DMA来输出高低电平来做模拟串口?中断的频率还是那么快啊,DMA减轻了不少CPU的时间。

TIM只是触发DMA传输,在DMA传输完成后触发DMA中断,中断频率比用TIM中断降低了10倍,TIM每一个bit中断1次,采用DMA后,1+8+1总共10个bit中断1次,还是大大减轻负载了的。

ifree 发表于 2024-3-5 17:15:55

如果想进一步降低中断频率,可以做到一个数据包DMA中断1次。比如数据包长度为N个字节要发送,可以分配N*10的TxBuffer,由DMA完成后,才产生一个中断,当然代价是需要更大的内存。

用这个方法,应该可以模拟任何时序信号。

庄永 发表于 2024-3-5 19:40:11

ifree 发表于 2024-3-5 17:15
如果想进一步降低中断频率,可以做到一个数据包DMA中断1次。比如数据包长度为N个字节要发送,可以分配N*10 ...

很不错的思路,就是数据优点占内存

2859932063 发表于 2024-3-6 09:18:18

ifree 发表于 2024-3-5 17:12
TIM只是触发DMA传输,在DMA传输完成后触发DMA中断,中断频率比用TIM中断降低了10倍,TIM每一个bit中断1次 ...

明白了{:10:}TIM触发DMA传输,但是TIM不产生中断,DMA传完一次才产生一次中断{:6:}

honami520 发表于 2024-3-6 09:44:50

挺不错的思路,不过接收就不好搞了吧

2859932063 发表于 2024-3-6 10:38:50

honami520 发表于 2024-3-6 09:44
挺不错的思路,不过接收就不好搞了吧

接收也简单,外部中断之后开始接收起始位,把TIM的时间改成原来的时间的一半就可以了,接收到起始位之后,把TIM调回来就可以了。不过确实会比发送麻烦一些

ifree 发表于 2024-3-6 10:38:54

这个方法从理论上讲和用中断驱动的硬件串口效率差不多,都是一个字符中断一次。不过st官方给的代码好像问题蛮多,调试起来还有不少问题。

2859932063 发表于 2024-3-6 14:28:35

ifree 发表于 2024-3-6 10:38
这个方法从理论上讲和用中断驱动的硬件串口效率差不多,都是一个字符中断一次。不过st官方给的代码好像问题 ...

我看了一下,你上面好像是对整组GPIO操作,这样不会对其他GPIO造成影响吗?

麦克斯韦Maxwell 发表于 2024-3-22 11:23:50

ifree 发表于 2024-3-5 17:12
TIM只是触发DMA传输,在DMA传输完成后触发DMA中断,中断频率比用TIM中断降低了10倍,TIM每一个bit中断1次 ...

楼主你好,看了文章,可不可以这样理解思路,Tx_buffer准备好要传输的10个数据位,然后启动发送,DMA开始把各个位的数据,依次存到BSRR寄存器,但是,我不明白,DMA的传输速度是不可控的,你是怎么通过DMA控制波特率的,假如起始位刚发送到BSRR,然后第一个数据位立刻来了,岂不是,等不到足够的时间,起始位的保持电平时间不够长,数据就被覆盖掉?

2859932063 发表于 2024-3-22 11:43:40

麦克斯韦Maxwell 发表于 2024-3-22 11:23
楼主你好,看了文章,可不可以这样理解思路,Tx_buffer准备好要传输的10个数据位,然后启动发送,DMA开始 ...

定时器触发的DMA,时间当然是固定的

麦克斯韦Maxwell 发表于 2024-3-22 14:23:20

2859932063 发表于 2024-3-22 11:43
定时器触发的DMA,时间当然是固定的

定时器可以控制DMA的开始和终止时间,但是并不能控制DMA的数据传输速度啊,我看代码,定时器触发DMA后,一次发送一帧数据,一帧有10BIT,但是我们怎么保证每一个BIT的电平保持时间符合UART要求呢?比如说,我们设置定时器10S,UPDATE一次,一帧传10BIT,那一个BIT的电平可以保持1S的时间,但是如果DMA仅需要5S就完成的一帧的传输呢,那岂不是一个BIT仅有0.5S的保持时间,接收方不就解码错误,而剩下的5S则是直接进入空闲状态?

ifree 发表于 2024-3-23 07:43:58

麦克斯韦Maxwell 发表于 2024-3-22 14:23
定时器可以控制DMA的开始和终止时间,但是并不能控制DMA的数据传输速度啊,我看代码,定时器触发DMA后, ...

定时器触发DMA传输,意思是每次产生定时器事件,触发一次DMA传输,时间就是固定的。
不是你理解的定时器触发DMA传输了,DMA一下子把配置的数据全部传完。

ifree 发表于 2024-3-23 07:45:23

2859932063 发表于 2024-3-6 14:28
我看了一下,你上面好像是对整组GPIO操作,这样不会对其他GPIO造成影响吗?

操作BSRR寄存器,是可以只对GPIO的某一个引脚进行操作,而不影响同一组下的其他引脚的。

麦克斯韦Maxwell 发表于 2024-3-27 10:27:20

ifree 发表于 2024-3-23 07:43
定时器触发DMA传输,意思是每次产生定时器事件,触发一次DMA传输,时间就是固定的。
不是你理解的定时器 ...

好的,谢谢你,我明白了
页: [1]
查看完整版本: 任意GPIO使用TIM+DMA+EXTI模拟UART