使用GPIO来模拟UART

前言

最近在看一些秋招的笔试和面试题,刚好看到一个老哥的经验贴,他面试的时候被问到了如果芯片串口资源不够了该怎么办?其实可以用IO口来模拟串口,但我之前也没有具体用代码实现过,借此机会用32开发板上的两个IO口来实现串口的功能,实现开发板和串口调试助手两者间数据的收发。

一:协议、硬件相关

为了方便,我这里就只有一位起始位,数据位是8位,一位停止位,没有奇偶校验位和流控,波特率是9600。

开发板我使用的是普中的一块32开发板,主控是stm32f103zet6,使用PB9模拟TX,PE0模拟RX,然后通过usb转串口模块和电脑相连。

具体的接线实物图如下:

二:TX、RX模拟

具体的32工程文件我放到了仓库里,完整的代码都在里面,接下来我就是解释一下编写的逻辑和一些注意点,工程模板是通过正点32的历程修改得到的。

门牙会稍息 / GPIO模拟UART · GitCode

IO模拟UART相关的内容我单独写到了一个.h和.c文件中。

myprintf.h文件中就是IO的一些宏定义和函数声明

#ifndef __MYPRINTF_H
#define	__MYPRINTF_H   

#include "./SYSTEM/sys/sys.h"

#define TX_GPIO_PORT                  GPIOB
#define TX_GPIO_PIN                   GPIO_PIN_9
#define TX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)             /* PB口时钟使能 */

#define RX_GPIO_PORT                  GPIOE
#define RX_GPIO_PIN                   GPIO_PIN_0
#define RX_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)             /* PE口时钟使能 */
#define RX_INT_IRQn                   EXTI0_IRQn
#define RX_INT_IRQHandler             EXTI0_IRQHandler

#define Set_TX(x)   do{ x ? 
                      HAL_GPIO_WritePin(TX_GPIO_PORT, TX_GPIO_PIN, GPIO_PIN_SET) : 
                      HAL_GPIO_WritePin(TX_GPIO_PORT, TX_GPIO_PIN, GPIO_PIN_RESET); 
                  }while(0)

#define Get_RX()   HAL_GPIO_ReadPin(RX_GPIO_PORT, RX_GPIO_PIN)

void myuart_init(void);
void send_byte(uint8_t data);
void send_str(char *dat); 
void myprintf(char *fmt, ...);
                  
#endif

myprintf.c 文件中内容

/**

 */

#include "./BSP/MYPRINTF/myprintf.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "./BSP/TIMER/btim.h"

//开始接收数据标志
volatile unsigned char uartStartFlag = 0;

//串口接收缓存
unsigned char uartBuf[256] = {0};
unsigned char uartBufLen = 0;
unsigned char uartHaveDat = 0;

//超时错误处理
volatile unsigned int uartBufTimeout = 0;
volatile unsigned int uartBufStartTimeout = 0;

void myuart_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
   
    TX_GPIO_CLK_ENABLE();
    gpio_init_struct.Pin = TX_GPIO_PIN;                   
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;         
    HAL_GPIO_Init(TX_GPIO_PORT, &gpio_init_struct);       
          
    RX_GPIO_CLK_ENABLE(); 
    gpio_init_struct.Pin = RX_GPIO_PIN;   
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */   
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;                  
    HAL_GPIO_Init(RX_GPIO_PORT, &gpio_init_struct);                        
    HAL_NVIC_EnableIRQ(RX_INT_IRQn);
    Set_TX(0);                                               
}

void send_byte(uint8_t data){
   Set_TX(0);
   delay_us(104);
   for(int i = 0; i < 8; i++){
      if(data & 0x01){
         Set_TX(1);
      }
      else{
         Set_TX(0);
      }
      delay_us(104);
      data = data >> 1;
   }
   Set_TX(1);
   delay_us(104);
      
}

void send_str(char *dat){
   for(int i = 0; i < strlen(dat); i++){
      send_byte(dat[i]);
   }
}

void myprintf(char *fmt, ...){
   va_list ap;
   char string[512];
   va_start(ap, fmt);
   vsprintf(string, fmt, ap);
   send_str(string);
   va_end(ap);
}


void RX_INT_IRQHandler(void){
    HAL_GPIO_EXTI_IRQHandler(RX_GPIO_PIN);         /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
    __HAL_GPIO_EXTI_CLEAR_IT(RX_GPIO_PIN);         /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
   if(GPIO_Pin == RX_GPIO_PIN){
      if(uartStartFlag == 0){
         uartStartFlag = 1;
         btim_timx_int_init(52 - 1, 72 - 1, BTIM_TIM6_INT);    //52us接收数据
      }
   }    
}

TX发送内容解释

1:发送的时候为了模拟时序图,需要延时,因为我设置通信波特率为9600,即一个高低电平持续时间就约为104us。

2:发送时数据先发送低位,所以发送一个byte的时候数据需要右移

3:有了发送字节函数(send_byte)之后循环调用就可以发送字符串(send_str)了

4:可以通过C语言中的va_list和vsprintf来实现自定义的printf函数

va_list 和vsprintf相关函数原型: 

 5:最后得到的myprintf函数就可以像使用C语言中的printf函数一样使用了

RX接收数据内容解释

1:配置RX的IO口是默认上拉,然后是外部中断下降沿触发

2:使用两个定时器来完成数据接收工作,Timer6定时52us用于数据接收,Timer7定时10ms用于确定数据是否传输完成。定时器相关配置和中断处理放到了btime.c和btime.h文件中

btime.h文件内容

#ifndef __BTIM_H
#define __BTIM_H

#include "./SYSTEM/sys/sys.h"

/******************************************************************************************/
/* 基本定时器 定义 */

 
#define BTIM_TIM6_INT                       TIM6
#define BTIM_TIM6_INT_IRQn                  TIM6_IRQn
#define BTIM_TIM6_INT_IRQHandler            TIM6_IRQHandler
#define BTIM_TIM6_INT_CLK_ENABLE()          do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0)   /* TIM6 时钟使能 */

#define BTIM_TIM7_INT                       TIM7
#define BTIM_TIM7_INT_IRQn                  TIM7_IRQn
#define BTIM_TIM7_INT_IRQHandler            TIM7_IRQHandler
#define BTIM_TIM7_INT_CLK_ENABLE()          do{ __HAL_RCC_TIM7_CLK_ENABLE(); }while(0)   /* TIM7 时钟使能 */

/******************************************************************************************/

void btim_timx_int_init(uint16_t arr, uint16_t psc, TIM_TypeDef* Timerx);    /* 基本定时器 定时中断初始化函数 */

#endif

btime.c文件内容,里面主要包含了数据的读取,然后放到缓存中,使用了比较多的标志位

#include "./BSP/LED/led.h"
#include "./BSP/TIMER/btim.h"
#include "./BSP/MYPRINTF/myprintf.h"

//开始接收数据标志
extern volatile unsigned char uartStartFlag;

//串口接收缓存
extern unsigned char uartBuf[256];
extern unsigned char uartBufLen;
extern unsigned char uartHaveDat;

//超时错误处理
extern volatile unsigned int uartBufTimeout;
extern volatile unsigned int uartBufStartTimeout;

TIM_HandleTypeDef g_tim6_handle;  /* 定时器句柄 */
TIM_HandleTypeDef g_tim7_handle;

void btim_timx_int_init(uint16_t arr, uint16_t psc, TIM_TypeDef* Timerx)
{
   if(Timerx == BTIM_TIM6_INT){
       g_tim6_handle.Instance = Timerx;                      /* 通用定时器X */
       g_tim6_handle.Init.Prescaler = psc;                          /* 设置预分频系数 */
       g_tim6_handle.Init.CounterMode = TIM_COUNTERMODE_UP;         /* 递增计数模式 */
       g_tim6_handle.Init.Period = arr;                             /* 自动装载值 */
       HAL_TIM_Base_Init(&g_tim6_handle);

       HAL_TIM_Base_Start_IT(&g_tim6_handle);    /* 使能定时器x及其更新中断 */
   }
    else if(Timerx == BTIM_TIM7_INT){
       g_tim7_handle.Instance = Timerx;                      /* 通用定时器X */
       g_tim7_handle.Init.Prescaler = psc;                          /* 设置预分频系数 */
       g_tim7_handle.Init.CounterMode = TIM_COUNTERMODE_UP;         /* 递增计数模式 */
       g_tim7_handle.Init.Period = arr;                             /* 自动装载值 */
       HAL_TIM_Base_Init(&g_tim7_handle);

       HAL_TIM_Base_Start_IT(&g_tim7_handle);    /* 使能定时器x及其更新中断 */
   }

}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == BTIM_TIM6_INT)
    {
        BTIM_TIM6_INT_CLK_ENABLE();                     /* 使能TIM时钟 */
        HAL_NVIC_EnableIRQ(BTIM_TIM6_INT_IRQn);         /* 开启ITM6中断 */
    }
    if (htim->Instance == BTIM_TIM7_INT)
    {
        BTIM_TIM7_INT_CLK_ENABLE();                     /* 使能TIM时钟 */
        HAL_NVIC_EnableIRQ(BTIM_TIM7_INT_IRQn);         /* 开启ITM7中断 */
    }
}


void BTIM_TIM6_INT_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_tim6_handle); /* 定时器中断公共处理函数 */
}

void BTIM_TIM7_INT_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_tim7_handle); /* 定时器中断公共处理函数 */
}


void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == BTIM_TIM6_INT)//52us接收数据
    {
       static unsigned char recvStep = 0; //接收步骤
       static unsigned char us52Cnt = 0;  //用于104us计数
       static unsigned char recDat = 0;   //接受一个字节
       static unsigned char bitCnt = 0;   //接收bit位数
       if(uartStartFlag == 1){
         if(recvStep == 0){//recvStep = 0是起始位检测步骤
            us52Cnt++;
            if(us52Cnt == 2){
               us52Cnt = 0;
               if(Get_RX() == 1){//起始位是高电平,是错误的
                  uartStartFlag = 0;
                  __HAL_TIM_DISABLE(&g_tim6_handle);
               }
               else{
                  recvStep = 1;  //起始位正确接收
                  recDat = 0;
                  bitCnt = 0;
               }
            }
         }
         else if(recvStep == 1){//正确接收到了起始位,现在开始接收8位数据
            us52Cnt++;
            if(us52Cnt == 2){
               us52Cnt = 0;
               recDat = recDat >> 1;
               if(Get_RX() == 1){//读到的数据为1
                  recDat |= 0x80;
               }
               bitCnt++;
               if(bitCnt > 7){//8位数据已近接收完
                  recvStep = 2; //recvStep = 2,准备接收停止位
               }
            }
         }
         else if(recvStep == 2){//接收完8位数据后,判断停止位是否正确接收
            us52Cnt++;
            if(us52Cnt == 2){
               us52Cnt = 0;
               if(Get_RX() == 1){//读到的数据为1
                  uartBuf[uartBufLen++] = recDat;
                  uartBufTimeout = 0;
                  uartBufStartTimeout = 1;
               }
               recvStep = 0;
               uartStartFlag = 0;
               __HAL_TIM_DISABLE(&g_tim6_handle);
            }
         }
       }
        __HAL_TIM_CLEAR_IT(&g_tim6_handle, TIM_IT_UPDATE);
       
    }
    
    
    
    if (htim->Instance == BTIM_TIM7_INT)//1ms超时处理
    {
       if(uartBufStartTimeout == 1){
         uartBufTimeout++;
          if(uartBufTimeout > 10){
             uartBufTimeout = 0;
             uartBufStartTimeout = 0;
             uartHaveDat = 1;
          }
       }
        __HAL_TIM_CLEAR_IT(&g_tim7_handle, TIM_IT_UPDATE);
    }
}

3:接收数据的主要流程就是先判断是否有数据发送,有的话就会触发RX的外部中断,打开52us的定时器,uartStartFlag会置1

4:打开52us定时器之后,通过us52Cnt标志位记录到2之后,代表一个高低电平的持续时间达到了104us,即可以判断一个位是高电平还是低电平。之后就是根据recvStep这个标志位来区分判断起始位、数据位、停止位的过程。

5:发送完一个字节之后将数据放到缓存中,开始10ms定时,超过10ms还没有数据来的话,就代表一次数据传输完成uartHaveDat标志位置1。

 

 6:在main中调用相关函数实现数据收发

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/MYPRINTF/myprintf.h"
#include "./BSP/TIMER/btim.h"
#include <string.h>

//串口接收缓存
extern unsigned char uartBuf[256];
extern unsigned char uartBufLen;
extern unsigned char uartHaveDat;

int main(void)
{
    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
    delay_init(72);                         /* 延时初始化 */
    led_init();                             /* 初始化LED */
    myuart_init();
    btim_timx_int_init(7200 - 1, 10 - 1, BTIM_TIM7_INT);  //1ms超时处理
    memset(uartBuf, 0x00, uartBufLen);
   while(1){
      if(uartHaveDat == 1){
         myprintf("%sn", uartBuf);
         memset(uartBuf, 0x00, uartBufLen);
         uartBufLen = 0;
         uartHaveDat = 0;
      }
      
   }
   
}

最终使用串口调试助手进行验证,可以达到数据收发的效果

 

总结

以上就是本文的内容了,建议看一下仓库的源码,理解起来会更快一些。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>