[应用方案] IO模拟串口

[复制链接]
5279|60
sanfuzi 发表于 2025-8-28 16:14 | 显示全部楼层 |阅读模式

1

纯延时模拟

这种方式就是当年老师出模拟串口题我所采用的办法,可以说该办法仅仅只是为了模拟一个串口出来(俗称 : 为了交作业),从一个电平到下一个电平的过程均采用硬延时,然而这里的延时就是对应着波特率所规定的电平持续时间,传输1位所需要的时间 T = 1/9600 约为104.167us,那么我们只需要按照对应的格式翻转IO口,然后delay延时对应的时间即可完成模拟。

参考伪代码:

  1. 1/************************************************
  2. 2 * Fuction :IO_UartSend
  3. 3 * Descir  :  IO口模拟串口发送
  4. 4 * Author  :  (公众号:最后一个bug)
  5. 5 ***********************************************/
  6. 6void IO_UartSend( sUart *pUart,unsigned char byte)
  7. 7{
  8. 8
  9. 9    unsigned char bitCnt = 8;
  10. 10    pUart->SetTxPin(pUart,PIN_LOW);  //发送 Start bit
  11. 11    pUart->BaudDelay(pUart);         // 根据baudRate延时
  12. 12    while(bitCnt--)                       //循环发送data bit
  13. 13    {
  14. 14        pUart->SetTxPin(pUart,(pUart & 0x01)); //发送 Start bit   
  15. 15        byte >>= 1;                            //移位所发数据
  16. 16        pUart->BaudDelay(pUart);               //根据baudRate延时  
  17. 17    }
  18. 18    pUart->SetTxPin(pUart,PIN_HIGH); //发送stop bit
  19. 19    pUart->BaudDelay(pUart);         //根据baudRate延时
  20. 20}
  21. 21
  22. 22/************************************************
  23. 23 * Fuction :IO_UartRecv
  24. 24 * Descir  :  IO口模拟串口接受
  25. 25 * Author  :  (公众号:最后一个bug)
  26. 26 ***********************************************/
  27. 27unsigned char IO_UartRecv(sUart *pUart)
  28. 28{
  29. 29    unsigned char Recv;
  30. 30    unsigned char bitCnt = 8;
  31. 31
  32. 32    while(!pUart->GetRxPin(pUart)) //如果接受到低电平起始位
  33. 33    {
  34. 34        pUart->BaudDelay(pUart);         //根据baudRate延时
  35. 35        while(bitCnt--)
  36. 36        {
  37. 37            Recv >>= 1;
  38. 38            if(pUart->GetRxPin(pUart))Recv |= 0x80; //如果接受到电平为1,则置位
  39. 39            pUart->BaudDelay(pUart);         //根据baudRate延时
  40. 40        }
  41. 41    }
  42. 42    return Recv; //最终返回接受到的数据
  43. 43}

分析一下:

上面主要是IO口模拟串口的发送和接受,发送相对比较简单,接受部分通过不断的查询对应的接收引脚是否已经拉低成为低电平,如果拉低成为了低电平就认为接受到了start_bit,后面便通过延时进行后面数据的接收。然而其中根据波特率进行的延时一般就直接用指令周期来进行测量延时了。

此方法对于简单的模拟串口收发功能基本实现了,不过其只能实现通信的半双工,同时通过不断的查询RX的电平状态比较浪费CPU资源,那么需要进一步改善。

2

外部中断法

查询比较耗费时间和资源,那么自然而然就想到采用中断的方法来进行处理,采用IO口的外部中断功能当RX引脚接受到一个start_bit的时候触发一个下降沿外部中断(记得关外部中断),然后在外部中断中进行延时获得对应的bit数据,其处理过程与上面的延时法并没有很大的区别,所以这就不过多解释。

以上均存在的不稳定因素 :

其不稳定因素主要来源于传输的电平翻转不是绝对的稳定,同时波特率传输的时间也不一定完全相同,如下图所示:

分析一下:

如上图所示首次获取电平的位置,都是在下降沿的位置开始进行数据的获取,然后通过波特率所对应的延时来进行下一bit位的获取,从而获得最终的传输数据。

大家应该都知道通信线路上是存在物理阻抗的,其对应的通信线路上的电平变化是不可能像上图中的方波那么标准的,其过程均存在一个上升时间和下降时间,同时再加上传输的bit时间间隔并不是严格的一致,所以在电平变化附近进行电平的判断是会存在误判的风险。

然而如果我们在首次获取以后延时半个周期,如上图蓝色虚线箭头所示位置进行判断便能够比较可靠的获得通信bit数据了。

虽然能够获得稳定的数据,不过采用硬延时在软件设计中终究是一个不太好的实现方案,同时以上通信还无法实现全双工,所以还是有必要再进行优化改善。

3

外部中断+定时器法

其实要解决硬延时最直接的处理办法就是使用定时器来进行处理,大家把发送和接受都放到对应的时间间隔里处理,这里大家比较常用的一种方案就是使用外部中断获得start_bit的位置,然后在外部中断中开启1/2bit定时,比如9600波特率,其一个bit传输需要104.167us,那么一般我们会采用104.167us/2的来设置定时时间进行后续电平的获取,如下图所示:

分析一下:

然而这样的方案,在仅仅模拟一个串口还是比较方便,不过如果模拟多个串口就需要多个定时器,这样实在是太浪费资源了。

那么是否用一个定时器就能搞定呢?很多小伙伴可能会说:我直接开一个bit周期的定时器不断的定时周期到来进行判断不就可以了吗?下面我们简单的看下该办法的效果。

4

单定时器法

首先这里实验一下bit周期定时法,作者编写好相应的代码以后,以20ms的速度发送两个字符55,然后让其回显的实验结果如下:

我们发现其存在较高的误码问题,其主要的原因还是跟我们之前所说的影响因素有关,如果定时器中断到来的时间刚好位于串口电平跳变附近,那么极有可能会存在读取IO口电平错误问题。

那么所有的问题就归结到如何在电平稳定的时候读取IO口的状态,那么最直接的办法就是提高定时器的中断频率,比如1/3bit周期法等等更高的定时器中断频率,如下图所示1/3bit周期法:

分析一下:

采用1/3bit周期法,其起始位的下降沿一定在1-2之间,如果我们判断起始位在1位置处,后续数据bit仍然是1位置,还是会出现之前的不稳定因素,所以这里需要调整读取IO的位置。

那么采用1/3bit周期**在判断起始bit下降沿的下一个定时器周期开始读取对应的电平,如果在1位置读取到了第一个低电平,那么后续都会在2位置进行数据读取;如果在2位置才读取到了第一个低电平,后续都会在3位置进行数据读取,这样在2,3位置读取的数据均是处于比较稳定的数据。

下面是作者采用1/3bit周期法的结果,该办法也是大家经常选用的。

4

其他方法

对于一些高端的MCU一般会有捕获口,其实捕获口有点类似于中断外部+定时器的方法,不过其原理是通过计算每个相邻边沿跳变中间所包含的bit个数,从而获得最终的数据,如下图所示:

分析一下:

采用捕获的办法不再是采集电平,通过定时器获得每个跳变之间的时间间隔,然后通过时间间隔/波特率对应的电平持续时间 = 电平个数,从而最终算出最后的数据。



gygp 发表于 2025-9-2 11:13 | 显示全部楼层
在接收数据时,可以采用软件滤波的方法
mmbs 发表于 2025-9-2 12:01 | 显示全部楼层
使用高精度外部晶振              
hilahope 发表于 2025-9-2 12:39 | 显示全部楼层
可以使用硬件辅助手段,如使用三极管或MOS管放大IO口的驱动
hearstnorman323 发表于 2025-9-2 14:41 | 显示全部楼层
在模拟串口通信时,需要考虑外界干扰的影响,采取适当的抗干扰措施。
tifmill 发表于 2025-9-2 15:24 | 显示全部楼层

起始位是数据发送前的一个​​下降沿​​(从高电平跳变到低电平)。接收方需通过检测该下降沿来启动接收流程。
lzbf 发表于 2025-9-2 16:32 | 显示全部楼层
对接收端 RXD 引脚添加施密特触发器
jtracy3 发表于 2025-9-2 17:24 | 显示全部楼层
需在 RXD 线路加装 TVS 二极管 ,防止静电击穿。
minzisc 发表于 2025-9-2 18:28 | 显示全部楼层
在不需要通信时,可以将IO口设置为低功耗模式,以降低整体功耗
kmzuaz 发表于 2025-9-2 19:33 | 显示全部楼层
发送端与接收端的波特率误差应控制在 ±2% 以内,否则长期运行必然出现帧同步丢失。
maqianqu 发表于 2025-9-2 20:43 | 显示全部楼层
为了提高接收的准确性,通常在每个比特的中间时刻进行采样。可以通过定时器中断来实现精确的采样时刻。
chenci2013 发表于 2025-9-2 21:41 | 显示全部楼层
电源的稳定性,以避免电源波动导致的通信错误。
fengm 发表于 2025-9-4 14:29 | 显示全部楼层
发送时序通常由定时器中断驱动              
sdCAD 发表于 2025-9-4 15:25 | 显示全部楼层
避免波特率漂移​              
primojones 发表于 2025-9-4 16:45 | 显示全部楼层
起始位去抖动,施密特输入,上拉电阻,校验位/超时检测。
maudlu 发表于 2025-9-4 20:20 | 显示全部楼层

停止位的作用是标识一帧数据的结束,接收方通过检测停止位的高电平来准备接收下一帧。
ccook11 发表于 2025-9-6 22:01 | 显示全部楼层
串口通信中,数据位通常是低位在前,高位在后。发送和接收时需要注意数据位的顺序。
lzmm 发表于 2025-9-8 13:54 | 显示全部楼层
使用定时器时              
uptown 发表于 2025-9-8 15:52 | 显示全部楼层
信号线的布线合理,避免信号干扰。
benjaminka 发表于 2025-9-9 17:04 | 显示全部楼层
发送和接收每个比特的时间必须严格满足波特率的要求,否则会导致数据错误。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

37

主题

3417

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部