STM32之模拟串口设计
一、设计用途:
公司PCB制成板降成本,选择的MCU比项目需求少一个串口,为满足制成板成本和项目对串口需求,选择模拟一路串口。
二、硬件电路:
三、设计实现:
工具&软件:STM32F030R8 KEIL5 STM32CubeMX
1、 串口通信
串口是一种很常用的通信接口,按位(bit)发送和接收字节,串口通信是异步传输,端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是比特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配,在我们单片机设计中发送和接收一个字节的位格式一般由起始信号位,数据位,停止位组成,很少有校验位,但是可以有。
2、 串口STM32串口硬件实现原理
既然想要模拟串口通信,那就需要设计得最接近于硬件串口,那么硬件串口是怎样实现数据的采集的呢?
以设置过采样率为8时进行分析:
在硬件串口接收线空闲时,串口一般会以一定频率去采集接收线上电平信号,如果辨认出一个特殊序列,1110X0X0X0X0X0X0 则确认接收到起始帧,触发接收中断。在前面判断三个1后如果后面采集到的位中有1则会降噪声位置1,所以在模拟串口时也需要加入噪声处理。
硬件串口的噪声处理:
简单来说,噪声处理就是将采集到的正中间的三位值进行判定,取三个中至少两个相同值作为最终采集值。
3、 实现方式选择
在网上大概看了一些前辈做的模拟串口,大致分为以下几种:
1、 阻塞式:接收使用中断,发送直接在主函数中进行,接收时以波特率的位时间进行定时采集,尽量的将采集点位于位中间。
2、 非阻塞式:使用一个定时器或者两个定时器,将接收和发送作于中断中,但接收方式基本一致,都只采集一次,未将噪声考虑进入。
也许每个行业对噪声的关注度都不太一样,但是在我这项目中噪声确是一个不得不面对的一个问题,只为通信更可靠。
自写第一种实现方式:
以波特率位时间的1/6进行中断采集,将每一位均分六等份,采集五次,然后取中间三次进行值判断。
复制代码
#include "UartSet.h"
#include "string.h"
uint8_t ucRecvData = 0; //每次接收的字节
uint8_t ucAcquCx = 0; //三次滤波计数
uint8_t ucRecvBitCx = 0; //接收位计数
uint8_t ucSendBitCx = 0; //发送位计数
uint8_t ucSendLengCx = 0; //发送长度计数
uint8_t ucRecvLastBit = 0; //上次接收位记录
uint8_t ucRecvNowBit = 0; //当前位记录
uint8_t ucRecvTrueCx = 0; //正确计数
uint32_t Prescaler = 0;
uint32_t Period = 0;
UART gsUARTBuff; //定义串口
TIM_HandleTypeDef htim6;
void MX_TIM6_Init(void)
{
__HAL_RCC_TIM6_CLK_ENABLE();
htim6.Instance = TIM6;
htim6.Init.Prescaler = Prescaler;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = Period;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_IRQn);
HAL_TIM_Base_Init(&htim6);
}
void UART_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = COM_TX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(COM_TX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = COM_RX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(COM_RX_GPIO_Port, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI4_15_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);
}
/*******************************************************************************
* @FunctionName : UART_Init.
* @Description : 模拟串口结构体初始化.
* @Input : None.
* @Output : None.
* @Return : None.
* @Author&Data : MrShuCai 2019.4.11.
*******************************************************************************/
void UART_Init(void)
{
gsUARTBuff.CheckType = NONE;
gsUARTBuff.UartMaxLength = Uartlength;
gsUARTBuff.UartStat = COM_NONE_BIT_DEAL;
UART_GPIO_Init();
if(BaudRate == 1200)
{
Prescaler = 15;
Period = 623;
}
else if(BaudRate == 2400)
{
Prescaler = 15;
Period = 207;
}
else if(BaudRate == 4800)
{
// Prescaler = 15;
// Period = 50; //9600
Prescaler = 15;
Period = 103;
}
MX_TIM6_Init();
}
/*******************************************************************************
* @FunctionName : UART_Send_Data.
* @Description : 模拟串口发送数据接口.
* @Input : None.
* @Output : None.
* @Return : None.
* @Author&Data : MrShuCai 2019.4.11.
*******************************************************************************/
void UART_Send_Data(uint8_t * data, uint8_t size)
{
gsUARTBuff.Sendlength = size;
memcpy(gsUARTBuff.UART_Send_buf, data, size);
gsUARTBuff.TxEn = 1;
gsUARTBuff.RxEn = 0;
gsUARTBuff.UartStat = COM_START_BIT_DEAL;
HAL_TIM_Base_Start_IT(&htim6);
}
/*******************************************************************************
* @FunctionName : EXTI4_15_IRQHandler.
* @Description : 接收引脚外部中断,下降沿触发,触发后即进入起始位判断.
* @Input : None.
* @Output : None.
* @Return : None.
* @Author&Data : MrShuCai 2019.4.11.
*******************************************************************************/
void EXTI4_15_IRQHandler(void)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_4) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_4);
if(gsUARTBuff.UartStat == COM_NONE_BIT_DEAL)
{
gsUARTBuff.RxEn = 1;
ucRecvData = 0;
gsUARTBuff.UartStat = COM_START_BIT_DEAL;
HAL_TIM_Base_Start_IT(&htim6);
}
}
}
/*******************************************************************************
* @FunctionName : TIM6_IRQHandler.
* @Description : 中断处理函数,包括发送和接收两部分.
* @Input : None.
* @Output : None.
* @Return : None.
* @Author&Data : MrShuCai 2019.4.11.
*******************************************************************************/
void TIM6_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6);
if(gsUARTBuff.TxEn == 1) /*数据发送,发送优先,无发送后才进入接收状态*/
{
switch(gsUARTBuff.UartStat) /*串口发送位状态判断*/
{
case COM_START_BIT_DEAL :
{
HAL_GPIO_WritePin(COM_TX_GPIO_Port, COM_TX_Pin, GPIO_PIN_RESET);
if(++ ucAcquCx == 6) /*由于发送时在立即拉低就进入判断,且自加,所以需要加到4,实际延时3个周期*/
{
gsUARTBuff.UartStat = COM_DATA_BIT_DEAL;
ucAcquCx = 0;
ucSendBitCx = 0;
}
}
break;
case COM_DATA_BIT_DEAL :
{
HAL_GPIO_WritePin(COM_TX_GPIO_Port, COM_TX_Pin, (GPIO_PinState)((gsUARTBuff.UART_Send_buf[ucSendLengCx] >> ucSendBitCx) & 0x01));
if(++ ucAcquCx >= 6)
{
ucAcquCx = 0;
ucSendBitCx ++;
if(ucSendBitCx >= 8)
{
if(gsUARTBuff.CheckType == NONE)
{
gsUARTBuff.UartStat = COM_STOP_BIT_DEAL;
}
else
{
gsUARTBuff.UartStat = COM_CHECK_BIT_DEAL;
}
}
}
}
break;
case COM_CHECK_BIT_DEAL :
{
}
break;
case COM_STOP_BIT_DEAL :
{
HAL_GPIO_WritePin(COM_TX_GPIO_Port, COM_TX_Pin, GPIO_PIN_SET);
if(++ ucAcquCx == 7)
{
ucAcquCx = 0;
ucSendBitCx = 0;
if(ucSendLengCx < gsUARTBuff.Sendlength - 1)
{
gsUARTBuff.UartStat = COM_START_BIT_DEAL;
ucSendLengCx ++;
}
else
{
ucSendLengCx = 0;
gsUARTBuff.UartStat = COM_NONE_BIT_DEAL;
gsUARTBuff.TxEn = 0;
gsUARTBuff.RxEn = 1;
HAL_TIM_Base_Stop_IT(&htim6);
TIM6->CNT = 0;
}
}
}
break;
default:
break;
}
}
if(gsUARTBuff.RxEn == 1)
{
switch(gsUARTBuff.UartStat)
{
case COM_START_BIT_DEAL : //起始位 必须连续三次采集正确才认可
{
ucRecvLastBit = ucRecvNowBit;
ucRecvNowBit = HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin);
if( ucAcquCx == 0)
{
ucAcquCx = 1;
return ;
}
if(ucRecvLastBit == ucRecvNowBit)
{
ucRecvTrueCx ++;
}
if(++ ucAcquCx >= 5)
{
if(ucRecvTrueCx >= 2)
{
gsUARTBuff.UartStat = COM_DATA_BIT_DEAL;
ucAcquCx = 0;
ucRecvTrueCx = 0;
}
else
{
ucAcquCx = 0;
ucRecvTrueCx = 0;
gsUARTBuff.UartStat = COM_STOP_BIT_DEAL;
}
}
}
break;
case COM_DATA_BIT_DEAL : //数据位
{
ucRecvLastBit = ucRecvNowBit;
ucRecvNowBit = HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin);
if(0 == ucAcquCx)
{
ucAcquCx = 1;
return;
}
if(ucRecvNowBit == ucRecvLastBit)
{
ucRecvTrueCx ++;
}
if(++ ucAcquCx >= 6)
{
ucAcquCx = 0;
if(ucRecvTrueCx >= 2)
{
if(ucRecvLastBit == 1)
{
ucRecvData |= (1 << ucRecvBitCx);
}
else
{
ucRecvData &= ~(1 << ucRecvBitCx);
}
if(ucRecvBitCx >= 7)
{
ucRecvBitCx = 0;
if(gsUARTBuff.CheckType == NONE)
{
gsUARTBuff.UartStat = COM_STOP_BIT_DEAL;
}
else
{
gsUARTBuff.UartStat = COM_CHECK_BIT_DEAL;
}
}
else
{
ucRecvBitCx ++;
}
}
else
{
ucAcquCx = 0;
ucRecvTrueCx = 0;
gsUARTBuff.UartStat = COM_STOP_BIT_DEAL;
}
}
}
break;
case COM_CHECK_BIT_DEAL : //校验位
{
}
break;
case COM_STOP_BIT_DEAL : //停止位
{
ucRecvLastBit = ucRecvNowBit;
ucRecvNowBit = HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin);
if(0 == ucAcquCx)
{
ucAcquCx = 1;
return;
}
if(ucRecvNowBit == ucRecvLastBit && ucRecvNowBit == 1)
{
ucRecvTrueCx ++;
}
if( ++ ucAcquCx >= 6)
{
ucAcquCx = 0;
if(ucRecvTrueCx >= 2)
{
ucRecvTrueCx = 0;
if(gsUARTBuff.Recvlength < gsUARTBuff.UartMaxLength)
{
gsUARTBuff.UART_Recv_buf[gsUARTBuff.Recvlength] = ucRecvData;
gsUARTBuff.Recvlength ++;
}
gsUARTBuff.UartStat = COM_NONE_BIT_DEAL;
HAL_TIM_Base_Stop_IT(&htim6);
TIM6->CNT = 0;
}
else
{
ucRecvTrueCx = 0;
}
}
}
break;
default:
break;
}
}
}
复制代码
在使用中已经达到要求,无错误识别,但是由于在电路中使用了光耦,所以波形存在一定形变,不能及时将电平立即拉低,同时中断次数还是比较频繁的,为减少误判和减少中断次数,采取另一种方式。
4、 最优实现
减少中断次数,每位只需中断三次,同时为避免光耦导致的滞后,将前面部分过滤不采集,只在中间快速采集三次,动态实现定时器定时时间调整。
比如在4800波特率下,一位208us,则定时器从起始位开始,定时序列为80,20,20,168,20,20,168,20,20,168......这样的序列。168为前一位的最后定时时间加当前位的前80us时间,也就是上图的起始位的4位置到第0位的2位置的时间。
复制代码
#include "UartSet.h"
#include "string.h"
typedef enum
{
TimeRecvStartStep1 = 0,
TimeRecvStep2 = 1,
TimeRecvStep3 = 2,
TimeRecvStep1 = 3,
TimeSendStep = 4,
} TimeStep;
uint16_t TimeSet[5];
uint16_t TimeSetBuff[][5] = {{1199, 59, 59, 2378, 2498 }, //1200
{539, 59, 59, 1128, 1247 }, //2400
{239, 59, 59, 503, 624 }, //4800
{149, 29, 29, 251, 311 }, //9600
{0, 0, 0, 0, 0 }, //预留
};
UART gsUARTBuff; //定义串口
uint8_t ucRecvData = 0; //每次接收的字节
uint8_t ucAcquCx = 0; //三次滤波计数
uint8_t ucRecvBitCx = 0; //接收位计数
uint8_t ucSendBitCx = 0; //发送位计数
uint8_t ucSendLengCx = 0; //发送长度计数
uint8_t ucRecvBitBuff = 0; //采集位保存
TIM_HandleTypeDef htim6;
void MX_TIM6_Init(void)
{
__HAL_RCC_TIM6_CLK_ENABLE();
htim6.Instance = TIM6;
htim6.Init.Prescaler = 15;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = TimeSet[TimeRecvStartStep1];
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_NVIC_SetPriority(TIM6_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_IRQn);
HAL_TIM_Base_Init(&htim6);
}
void UART_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = COM_TX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(COM_TX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = COM_RX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(COM_RX_GPIO_Port, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI4_15_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);
}
/*******************************************************************************
* @FunctionName : UART_Init.