使用GPIO来模拟UART

news2024/11/16 13:54:53

前言

最近在看一些秋招的笔试和面试题,刚好看到一个老哥的经验贴,他面试的时候被问到了如果芯片串口资源不够了该怎么办?其实可以用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("%s\n", uartBuf);
         memset(uartBuf, 0x00, uartBufLen);
         uartBufLen = 0;
         uartHaveDat = 0;
      }
      
   }
   
}

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

 

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/696587.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣 404. 左叶子之和

题目来源&#xff1a;https://leetcode.cn/problems/sum-of-left-leaves/description/ C题解1&#xff1a;递归法&#xff0c;前序遍历。 1. 确定输入参数&#xff1a;当前节点&#xff0c;左叶子的和&#xff1b; 2. 确定终止条件&#xff1a;空节点时返回&#xff1b; 3. …

【每日一短语】在必要情况下

1、短语及释义 in a pinch 释义&#xff1a; 在紧要关头&#xff1b;在必要情况下 2、示例及出处 美剧&#xff1a;《生活大爆炸》第七季第21集 The Big Bang Theory, Season 7 Episode 21 Sheldon Cooper: Penny, there’s only one cookie with something in the middle tha…

基于STM32设计的城市绿化云端监控系统(华为云IOT)

一、设计需求 1.1 项目背景 随着科技的蓬勃发展改变了很多传统行业的作业方式,当我们用移动支付代替现金交易时,当我们足不出户就能满足饥饿的身体时,我们的生活方式因为科技而发生了改变;同样科技也在改变着我们周围的点点滴滴,城市绿化养护亦是如此。 通过智慧控制系统…

在Docker中使用MindSpore GPU版本

文章目录 在Docker中使用MindSpore GPU版本获取安装命令安装安装nvidia-container-toolkit获取MindSpore镜像测试运行MindSpore镜像运行代码 使用VSCode开发 在Docker中使用MindSpore GPU版本 参考官方文档&#xff1a;安装指南 获取安装命令 如图所示 命令为 docker pull…

MFC将二维数组写入文件中并进行读取

MFC将二维数组写入文件中并进行读取 当前项目需要将二维数组写入到本地文件中&#xff0c;并在另一个对话框中进行读取。网上查了很多资料&#xff0c;基本都是写字符串到文件中的&#xff0c;想依葫芦画瓢仿照字符串的写法来写二维数组&#xff0c;发现在写文件状态下&#x…

护网是什么?为什么【网安人】都想参加!

一、什么是护网行动&#xff1f; 护网行动是以公安部牵头的&#xff0c;用以评估企事业单位的网络安全的活动。 具体实践中。公安部会组织攻防两方&#xff0c;进攻方会在一个月内对防守方发动网络攻击&#xff0c;检测出防守方&#xff08;企事业单位&#xff09;存在的安全漏…

Flink之FileSink将数据写入parquet文件

Flink之FileSink将数据写入parquet文件 在使用FileSink将数据写入列式存储文件中时必须使用forBulkFormat,列式存储文件如ORCFile、ParquetFile,这里就以ParquetFile为例结合代码进行说明. 在Flink1.15.3中是通过构造ParquetWriterFactory然后调用forBulkFormat方法将构造好的…

第9章 异常处理

第9章 异常处理 9.1 Java异常处理 try-catch-finally ​ ​ 9.2 Scala异常处理 ​ ​ package chapter09object Test01_Exception {def main(args: Array[String]): Unit {try {val n 10 / 1} catch {case e: ArithmeticException > {println("发生算数异常&quo…

【Java高级编程】Java常用类

Java常用类 1、字符串相关的类1.1、字符串相关的类&#xff1a;String1.2、字符串相关的类&#xff1a;String常用方法1.3、String与基本数据类型、包装类之间的转换1.4、String与char[]之间的转换1.5、String与byte[]之间的转换1.6、String、StringBuffer、StringBuilder三者的…

element-plus中的el-table如何动态合并行(复制粘贴即可使用 亲测有效!)

demo场景&#xff1a; 一个 table&#xff0c;由5列组成&#xff0c;其 prop 分别为’resource_name’, ‘scene_name’, ‘type’, ‘content’, ‘desc’&#xff0c;渲染 table 的数据来源于接口。现在需要将 propdesc的这一列&#xff0c;按照resource_name 相等时进行合并…

使用EVPN构建二层VPN

一、业务背景 EVPN实例用于将EVPN路由与公网路由隔离。不同EVPN实例的路由之间也是相互隔离的。在所有EVPN组网方案中,都需要配置EVPN实例。 二、业务拓扑 三、配置步骤 1、配置基本接口IP 2、配置核心网IGP和LDP 3、配置PE和PE之间的BGP EVPN邻居关系 4、配置EVPN的源地址 …

Linux5.10 NoSQL 之 Redis配置与优化及数据类型

文章目录 计算机系统5G云计算第四章 LINUX NOSQL 之 Redis配置与优化及数据类型一、关系数据库与非关系型数据库1.关系型数据库2.非关系型数据库3.关系型数据库和非关系型数据库区别4.非关系型数据库产生背景5.总结 二、Redis简介1.Redis 具有以下几个优点2.使用场景3.哪些数据…

一、枚举类型——使用 EnumMap 分发

使用 EnumMap 可以实现“真正的”双路分发&#xff0c;它是专门为 enum 设计的高效 Map。我们的目标是在两个未知类型中切换&#xff0c;因此由 EnumMap 组成的 EnumMap &#xff08;嵌套 EnumMap &#xff09;可以实现双路分发。 RoShamBo5.java import java.util.EnumMap;imp…

如何 设计一个高质量的 API 接口?

目录 你是否也感同身受&#xff1f; 优秀API的特质 自解释 易学习 易使用 难误用 API 设计原则 1. 充分原则 2. 单一视角原则 3. 单一功能原则 4. 简单原则 5. 抽象原则 6. 兼容扩展原则 7. 最小惊讶原则 8. 低耦合原则 9. 正交原则 10. 易测试原则 11. 统一原…

怎样查看电脑开关机日志

最近想查看家里电脑是否每天都正常关机了。 家里的是Windows电脑。 使用windows自带功能或者说自带工具无疑是最方便的。 按下【开始→运行】&#xff0c;输入eventvwr&#xff0c;按下回车键。 打开事件查看器&#xff0c;展开Windows日志&#xff0c;双击系统。 选择筛…

Redis内存满分析

操作0&#xff1a; dbsize计算db大小&#xff0c;判断是哪个db的问题&#xff0c;发现是db1的问题。 操作1&#xff1a; Redis中先备份xxx.rdb文件&#xff0c;然后使用下面的工具进行分析 Redis内存分析工具之redis-rdb-tools的安装与使用_薛定谔的猫io的博客-CSDN博客 结…

如何在Windows 10中移动文档文件夹位置

默认情况下,Windows将你的个人文档文件夹存储在你帐户的%UserProfile%文件夹中(例如:“C:\Users\Kent”)。 你可以将“文档”文件夹中文件的存储位置更改为硬盘驱动器、其他驱动器或网络上的其他计算机上的其他位置。 一、如果你当前的文档文件夹受 OneDrive 保护,那么你…

记录一个iOS无法找到堆栈信息的崩溃修复

崩溃提示 2023-06-28 21:14:46.9624560800 -[UIDynamicCatalogColor length]: unrecognized selector sent to instance 0x6000073292c0 崩溃如下图所示 思路&#xff0c;既然我们无法通过调用的堆栈信息查找&#xff0c;那就试试通过崩溃对象的内存地址查看该对象的详细信息 …

攻防世界-web-mfw

题目描述&#xff1a;如图&#xff0c;只有这样的三个页面 home页面&#xff1a; about页面&#xff1a; contact页面&#xff1a; burp抓包可以看到返回的html中刚好对应了三个页面&#xff0c;以及注释掉的flag 尝试将page设置成flag&#xff0c;但是并没有什么反应。 1. 思…

【Web 网络管理】网络杂谈(8)之基于 Web 的网络管理

涉及知识点 基于 Web 的网络管理模式&#xff0c;WBM的介绍与标准&#xff0c;WBM的实现方式与关键技术&#xff0c;WBM的安全性考虑。深入了解WBM技术。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多内容可去其主页关注下哈&#xff0c;不胜感激 文章目录…