使用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){//读到的数据为1recDat |= 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){//读到的数据为1uartBuf[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.hqwc.cn/news/8807.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从0开始,手写MySQL数据管理器DM

说在前面 从0开始&#xff0c;手写一个MySQL的学习价值在于&#xff1a; 可以深入地理解MySQL的内部机制和原理&#xff0c;MySQL可谓是面试的绝对重点和难点&#xff0c; 尼恩曾经指导过的一个7年经验小伙&#xff0c;凭借精通MySQL 搞定月薪40K。 从而更好地掌握MySQL的使…

前端实现pdf,图片,word文件预览

前端实现文件预览功能 需求&#xff1a;实现一个在线预览pdf、excel、word、图片等文件的功能。 介绍&#xff1a;支持pdf、xlsx、docx、jpg、png、jpeg。 以下使用Vue3代码实现所有功能&#xff0c;建议以下的预览文件标签可以在外层包裹一层弹窗。 图片预览 iframe标签能够将…

Vue3 JSX 插槽、v-model 的用法以及 React JSX 的区别

前言 写这篇文章的初衷是&#xff0c;Vue3 JSX 部分与 React JSX 容易混淆&#xff0c;比如如本文所说的 slot & v-model&#xff0c; 如果你是第一次接触 JSX&#xff0c;可先阅读前面写过的 React & JSX 日常用法与基本原则 来对 JSX 有一个整体的认知以及比较两者间…

架构重构|性能和扩展性大幅提升的Share Creators智能数字资产管理软件3.0

作为数字资产管理行业的领军者&#xff0c;Share Creators智能数字资产管理软件持续致力于帮助企业和团队智能化管理数字资产&#xff0c;提升工业化管线制作效率。经过本次重构&#xff0c;Share Creators 3.0版本重装上阵&#xff0c;全面更新的服务架构标志着软件整体性能的…

2023最新ChatGPT商业运营网站源码+支持ChatGPT4.0+新增GPT联网功能+支持ai绘画+实时语音识别输入+用户会员套餐+免费更新版本

2023最新ChatGPT商业运营网站源码支持ChatGPT4.0新增GPT联网功能支持ai绘画实时语音识别输入用户会员套餐免费更新版本 一、AI创作系统二、系统程序下载三、系统介绍四、安装教程五、主要功能展示六、更新日志 一、AI创作系统 提问&#xff1a;程序已经支持GPT3.5、GPT4.0接口…

磁盘镜像软件

什么是磁盘镜像 磁盘镜像是存储在计算机磁盘中的数据的副本或副本。磁盘镜像将包含数据存储设备的内容&#xff0c;并复制此类设备的结构。它还将包含操作系统分区。 磁盘镜像本质上是一种从主系统复制操作系统和存储在磁盘中的数据以将其分发到其他目标计算机的方法。自动化…

有源差分探头的电压限定和检查步骤

为了使传统示波器能够显示和测试高共模电压的电路波形&#xff0c;有源差分探头对示波器测量性能延伸到了电子功率变换器、 逆变器、 电机的速度控制、 开关电源和许多应用程序的测试。 差分探头外观构成&#xff1a; A-输出线&#xff1a;连接示波器的 BNC 输出连接头和辅助性…

Vscode——python代码调试时,列表(变量)内容显示不全的一种解决办法

一、情况描述 列表维度是(1080,1920)&#xff0c;但只能看到前500行&#xff0c;后面的看不到了。 二、解决办法1——添加到监视 右键变量&#xff0c;选择 添加到监视 编辑表达式 三、解决办法2——数据查看器中查看值 右键变量&#xff0c;选择 在数据查看器中查看值&…

瑞吉外卖-Day02

title: 瑞吉外卖-Day02 abbrlink: ‘1’ date: 2023-04-1 19:30:00 瑞吉外卖-Day02 课程内容 完善登录功能新增员工员工信息分页查询启用/禁用员工账号编辑员工信息 分析前端页面效果是如何实现的 为什么点击左边 右边会根着变化 [外链图片转存失败,源站可能有防盗链机制…

nodejs-pm2管理js并发/自动重启/恢复等

目录 一、nodejs安装二、启动运行js三、实用功能1-pm2对进程名起别名四、实用功能2-pm2启动多个进程五、实用功能3-pm2内存限制自动重启六、实用功能4-服务器宕机前保存记录恢复进程 一、nodejs安装 nodejs安装使用nohup后台启动项目&#xff0c;倒是解决了控制台问题&#xf…

高适的逆袭之路

骆宾王7岁咏鹅&#xff0c;王勃10岁写论文&#xff0c;王维18岁名震天下。 高适50岁才步入仕途。 父亲官居刺史&#xff0c;祖父正三品还封了平原郡公&#xff0c;拿着将门虎子剧本的高适&#xff0c;却因父亲早逝&#xff0c;荣耀和显赫已成过往云烟。能握住的&#xff0c;是…

JUC--CompletableFuture下

对计算速度进行选用 import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit;public class Test4 {public static void main(String[] args) {CompletableFuture<String> a CompletableFuture.supplyAsync(() -> {try { TimeUnit.SE…