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. * @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) { memcpy(TimeSet, &TimeSetBuff[0][0], sizeof(TimeSet)); } else if(BaudRate == 2400) { memcpy(TimeSet, &TimeSetBuff[1][0], sizeof(TimeSet)); } else if(BaudRate == 4800) { memcpy(TimeSet, &TimeSetBuff[2][0], sizeof(TimeSet)); } else if(BaudRate == 9600) { memcpy(TimeSet, &TimeSetBuff[3][0], sizeof(TimeSet)); } else { } 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); if(gsUARTBuff.UartStat == COM_NONE_BIT_DEAL) { gsUARTBuff.TxEn = 1; gsUARTBuff.RxEn = 0; gsUARTBuff.UartStat = COM_START_BIT_DEAL; TIM6->ARR = TimeSet[TimeSendStep]; TIM6->EGR = TIM_EGR_UG; 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; TIM6->ARR = TimeSet[TimeRecvStartStep1]; TIM6->EGR = TIM_EGR_UG; //初始化定時器 EXTI->IMR &= ~(0x10); EXTI->EMR &= ~(0x10); HAL_TIM_Base_Start_IT(&htim6); } } } /******************************************************************************* * @FunctionName : BitValueChk. * @Description : 判斷采集bit值,三次中為1的次數大於等於2則值為1否則為0. * @Input : n 采集記錄的位值. * @Output : BitValue. * @Return : BitValue. * @Author&Data : MrShuCai 2019.5.1. *******************************************************************************/ uint8_t BitValueChk(uint8_t n) { uint8_t BitValCx = 0; for(BitValCx = 0; n; n >>= 1) { BitValCx += n & 0x01; } return (BitValCx < 2) ? (0) : (1); } /******************************************************************************* * @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); gsUARTBuff.UartStat = COM_DATA_BIT_DEAL; 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)); 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); 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); EXTI->IMR |= 0x10; EXTI->EMR |= 0x10; TIM6 ->CNT = 0; } } break; default: break; } } if(gsUARTBuff.RxEn == 1) { switch(gsUARTBuff.UartStat) { case COM_START_BIT_DEAL : { ucRecvBitBuff = (ucRecvBitBuff << 1) | (HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin) & 0x01); if(++ ucAcquCx >= 3) { if(BitValueChk(ucRecvBitBuff) == 0) { gsUARTBuff.UartStat = COM_DATA_BIT_DEAL; TIM6->ARR = TimeSet[ucAcquCx]; } else { gsUARTBuff.UartStat = COM_STOP_BIT_DEAL; } ucRecvBitBuff = 0; ucAcquCx = 0; } else { TIM6->ARR = TimeSet[ucAcquCx]; } } break; case COM_DATA_BIT_DEAL : //數據位 { ucRecvBitBuff = (ucRecvBitBuff << 1) | (HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin) & 0x01); if(++ ucAcquCx >= 3) { ucRecvData |= (BitValueChk(ucRecvBitBuff) & 0x01) << 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 ++; } TIM6->ARR = TimeSet[ucAcquCx]; ucAcquCx = 0; ucRecvBitBuff = 0; } else { TIM6->ARR = TimeSet[ucAcquCx]; } } break; case COM_CHECK_BIT_DEAL : //校驗位 { } break; case COM_STOP_BIT_DEAL : //停止位 { ucRecvBitBuff = (ucRecvBitBuff << 1) | (HAL_GPIO_ReadPin(COM_RX_GPIO_Port, COM_RX_Pin) & 0x01); if( ++ ucAcquCx >= 3) { if(BitValueChk(ucRecvBitBuff) == 1) { 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); EXTI->IMR |= 0x10; EXTI->EMR |= 0x10; TIM6->CNT = 0; } else { ucAcquCx = 0; } ucRecvBitBuff = 0; ucAcquCx = 0; } else { TIM6->ARR = TimeSet[ucAcquCx]; } } break; default: break; } } }
發送就以波特率時間定時發送位狀態即可,實現簡單。
僅供初學或者有興趣的朋友參考,第一種方法由於後來放棄,只是大致實現,推薦第二種,歡迎大家指點。
STM32之模擬串口設計