任意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倍,大大减轻系统的负载。
谢谢楼主分享,实际批量数据测试,稳定性怎么样。 初次尝试
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;
误码分析
采用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);
}
现在没有误码了:
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
*/
}
这个通过TIM中断让DMA来输出高低电平来做模拟串口?中断的频率还是那么快啊,DMA减轻了不少CPU的时间。
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次,还是大大减轻负载了的。 如果想进一步降低中断频率,可以做到一个数据包DMA中断1次。比如数据包长度为N个字节要发送,可以分配N*10的TxBuffer,由DMA完成后,才产生一个中断,当然代价是需要更大的内存。
用这个方法,应该可以模拟任何时序信号。 ifree 发表于 2024-3-5 17:15
如果想进一步降低中断频率,可以做到一个数据包DMA中断1次。比如数据包长度为N个字节要发送,可以分配N*10 ...
很不错的思路,就是数据优点占内存 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
挺不错的思路,不过接收就不好搞了吧
接收也简单,外部中断之后开始接收起始位,把TIM的时间改成原来的时间的一半就可以了,接收到起始位之后,把TIM调回来就可以了。不过确实会比发送麻烦一些 这个方法从理论上讲和用中断驱动的硬件串口效率差不多,都是一个字符中断一次。不过st官方给的代码好像问题蛮多,调试起来还有不少问题。
ifree 发表于 2024-3-6 10:38
这个方法从理论上讲和用中断驱动的硬件串口效率差不多,都是一个字符中断一次。不过st官方给的代码好像问题 ...
我看了一下,你上面好像是对整组GPIO操作,这样不会对其他GPIO造成影响吗? ifree 发表于 2024-3-5 17:12
TIM只是触发DMA传输,在DMA传输完成后触发DMA中断,中断频率比用TIM中断降低了10倍,TIM每一个bit中断1次 ...
楼主你好,看了文章,可不可以这样理解思路,Tx_buffer准备好要传输的10个数据位,然后启动发送,DMA开始把各个位的数据,依次存到BSRR寄存器,但是,我不明白,DMA的传输速度是不可控的,你是怎么通过DMA控制波特率的,假如起始位刚发送到BSRR,然后第一个数据位立刻来了,岂不是,等不到足够的时间,起始位的保持电平时间不够长,数据就被覆盖掉? 麦克斯韦Maxwell 发表于 2024-3-22 11:23
楼主你好,看了文章,可不可以这样理解思路,Tx_buffer准备好要传输的10个数据位,然后启动发送,DMA开始 ...
定时器触发的DMA,时间当然是固定的 2859932063 发表于 2024-3-22 11:43
定时器触发的DMA,时间当然是固定的
定时器可以控制DMA的开始和终止时间,但是并不能控制DMA的数据传输速度啊,我看代码,定时器触发DMA后,一次发送一帧数据,一帧有10BIT,但是我们怎么保证每一个BIT的电平保持时间符合UART要求呢?比如说,我们设置定时器10S,UPDATE一次,一帧传10BIT,那一个BIT的电平可以保持1S的时间,但是如果DMA仅需要5S就完成的一帧的传输呢,那岂不是一个BIT仅有0.5S的保持时间,接收方不就解码错误,而剩下的5S则是直接进入空闲状态? 麦克斯韦Maxwell 发表于 2024-3-22 14:23
定时器可以控制DMA的开始和终止时间,但是并不能控制DMA的数据传输速度啊,我看代码,定时器触发DMA后, ...
定时器触发DMA传输,意思是每次产生定时器事件,触发一次DMA传输,时间就是固定的。
不是你理解的定时器触发DMA传输了,DMA一下子把配置的数据全部传完。 2859932063 发表于 2024-3-6 14:28
我看了一下,你上面好像是对整组GPIO操作,这样不会对其他GPIO造成影响吗?
操作BSRR寄存器,是可以只对GPIO的某一个引脚进行操作,而不影响同一组下的其他引脚的。 ifree 发表于 2024-3-23 07:43
定时器触发DMA传输,意思是每次产生定时器事件,触发一次DMA传输,时间就是固定的。
不是你理解的定时器 ...
好的,谢谢你,我明白了
页:
[1]