使用GPIO来模拟UART

这篇具有很好参考价值的文章主要介绍了使用GPIO来模拟UART。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

一:协议、硬件相关

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

使用GPIO来模拟UART

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

具体的接线实物图如下:

使用GPIO来模拟UART

二: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。

使用GPIO来模拟UART

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

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

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

va_list 和vsprintf相关函数原型: 

使用GPIO来模拟UART

使用GPIO来模拟UART

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

RX接收数据内容解释

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

使用GPIO来模拟UART

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

使用GPIO来模拟UART

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

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

使用GPIO来模拟UART

 使用GPIO来模拟UART

 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("%s\n", uartBuf);
         memset(uartBuf, 0x00, uartBufLen);
         uartBufLen = 0;
         uartHaveDat = 0;
      }
      
   }
   
}

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

使用GPIO来模拟UART 

总结

以上就是本文的内容了,建议看一下仓库的源码,理解起来会更快一些。文章来源地址https://www.toymoban.com/news/detail-508823.html

到了这里,关于使用GPIO来模拟UART的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux Kernel 4.19+内核使用GPIO模拟I2C的方法

    1.修改内核配置文件,使内核支持GPIO模拟I2C 2.对应的dts里面增加GPIO模拟I2C的设备树 编译后烧写,会发现/dev下多了一个i2c总线,多出来的那个就是了。

    2024年02月16日
    浏览(51)
  • 高速USB转JTAG/SPI/I2C/UART/GPIO应用

    高速USB转接芯片CH347是一款集成480Mbps高速USB接口、JTAG接口、SPI接口、I2C接口、异步UART串口、GPIO接口等多种硬件接口的转换芯片。 接口示意图: 应用示意图: JTAG接口特点 工作在 Host/Master主机模式; 硬件信号:TMS、TCK、TDI、TDO和TRST; 支持自定义协议的快速模式和bit-bang模式

    2023年04月23日
    浏览(44)
  • 我最近的练习一些全栈项目

    嘿,大家好!作为一个程序员,我突然出现在这里,就像程序里的一个Bug一样突兀。我知道我很久没有发博客了,你们一定在想,这家伙是被代码迷宫困住了还是被Bug们抓走了?实际上,我一直忙于处理一些琐碎的事情,比如寻找丢失的分号和与花括号的恶战。但是,我发现

    2024年02月06日
    浏览(37)
  • 最近给shopify跨境电商网站搞google搜索引擎的seo优化,整理了一些内容

     接到一个网站,首先要做一些工作,然后按照这个步骤做好每一步,网站的搜索排名会有明显的效果。 对网站进行技术审核,以确保它符合搜索引擎的技术要求。 研究并确定目标。 优化网站内容,以便更好地针对目标。 建立高质量的外部链接,以提高

    2024年02月10日
    浏览(80)
  • SPI的介绍--GPIO口模拟SPI

    CPOL 是时钟的极性,用来表示SPI总线在空闲时SCK是低电平还是高电平,低电平为0,高电平为1; CPHA 是时钟的相位,用来决定何时进行信号采样,在第一个跳变沿还是第二个跳变沿,在第一个跳变沿采样则为0,在第二个跳变沿采样则为1。 Mode 0 : Clock Polarity (CPOL) = 0 and, Clock

    2024年02月12日
    浏览(28)
  • GPIO模拟时序控制外设1——WS2812B

    上一篇文章中介绍了整个板子的最基本功能模块——使用GPIO的通用输入输出实现简单的按键输入以及推挽输出控制的功能。本文深入一步,在只使用GPIO的输入输出功能的基础上,通过查看对应模块的芯片手册,模拟其对应的通信时序来驱动对应的模块。 首先来个网红模块—

    2024年02月12日
    浏览(48)
  • GPIO模拟时序控制外设4——红外发射管

    上一篇介绍了使用GPIO模拟时序实现I2C协议的功能,本文继续使用GPIO模拟的方式来实现一个平时生活中常用的模块——红外发射管的驱动,红外在家电的应用中十分广泛,空调、电视、投影等都是通过红外遥控来实现的。 红外发射管实物就是下图中左边的红框内的器件,右侧

    2024年02月11日
    浏览(45)
  • stm32 GPIO模拟SPI接口实现双机通信

    一、SPI协议简介     一般主从方式工作,这种模式通常有一个主设备和一个或多个从设备,通常采用的是4根线,它们是MISO(主机输入从机输出)、MOSI(主机输出,针对主机来说)、SCLK(时钟,主机产生)、CS(片选,一般由主机发送或者直接使能,通常为低电平有效) ●

    2023年04月08日
    浏览(49)
  • 普冉PY32系列(九) GPIO模拟和硬件SPI方式驱动无线收发芯片XL2400

    普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单 普冉PY32系列(四) PY32F002A/003/030的时钟设置 普冉PY32系列(五) 使用JLink RTT代替串口输出日志 普冉PY32系列(六) 通过I2C接口驱动PCF8574扩

    2024年02月08日
    浏览(65)
  • 普冉PY32系列(八) GPIO模拟和硬件SPI方式驱动无线收发芯片XN297LBW

    普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单 普冉PY32系列(四) PY32F002A/003/030的时钟设置 普冉PY32系列(五) 使用JLink RTT代替串口输出日志 普冉PY32系列(六) 通过I2C接口驱动PCF8574扩

    2024年02月08日
    浏览(78)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包