关于STM32 利用IO口模拟串口实现数据通信

2020-06-30来源: eefocus关键字:STM32  IO口  模拟串口  数据通信

网上有好多关于利用IO口来实现串口数据收发的实例,这种方法的实质都是可以应用于任何一款微处理器上,而不仅仅局限于stm32。有相关的源代码链接参考:

https://github.com/TonyIOT/SoftWareSerial


当然,如果成本要求不高,可以利用串口扩展芯片是最方便的,如CH438,WK系列芯片等。


IO口模拟串口的一些基本原理,阐述如下:

STM32单片机一般少则3个串口,多则5个,而项目还偏偏5个硬件串口还是不够用.板子上有几个预留IO口,可以用来模拟串口. 模拟串口一般都选9600,速度最快试了也才19200,所以限制还是较多的,一般不得以情况下才会用到.


IO口模拟串口的思路也比较简单,一切按照串口协议进行操作即可。对于发送,计算好不同波特率对应的延时时间进行数据发送。对于接收,稍微复杂。通过外部中断检测接收管脚的下降沿,检测到起始信号后开启定时器,定时器按照波特率设定好时间,每隔一段时间进入定时器中断接收数据,完成一个字节后关闭定时器。


模拟串口分收和发:收比较难,发送比较容易,那就先将接收这块吧。 接收:

有2种思路: 一种是第一个下降沿开始启动定时器,每个bit都去采样Rx电平; 从第一个边沿开始统计时间戳,接收完10个bit后再解析. 我这边碰巧选择的是第二种方法,具体实现思路是:

① 启动定时器2用来做背景时间,定时器分频后的计数频率为1MHz,那么对于9600bps来说1个bit就是104.16us,我们取整数104就可以了;

② 再同时启动一个定时器3,定时时间为1049.5,这个定时器中断发生的时候表示 一个字节接收完毕了


串口发送和接收数据波形示例

我们以0x37串口通讯时序图为例, ,传输的时候是LSB 1st, Rx引脚选择边沿触发, 第1次边沿到的时候启动定时器2,同时启动定时器3, 第2个边沿的时候将定时器2的计数值存到数组里,此时是104,第3个边沿触发的时候将其计数1045计数值,同理将4,5,6三个边沿对应的TIM2值存到缓存里,最终是 0,104,416,520,728,936,除以104变成0,1,4,5,7,9;


如何将0,1,4,5,7,9解析成0x37,看似简单的问题其实还是有点麻烦的~ 给个思路,先将0和9这样的数字去掉,变成1,4,5,7;然后从1开始数到8,跟数组里元素是否有匹配,如果匹配就将状态取反,没有就维持之前的状态:1有,2,3没有,4有,5有,6没有,7有,8没有–>11101100->倒序后就是0x37了;


再来分析几个处理后的值:4,5,7: 1,2,3没有,4有,5有,6没有,7有,8没有–>00010011->逆序后11001000->0xC8 1,2: 1有,2有,3,4,5,6,7,8没有->10000000->逆序后00000001->0x01

1,2,3,4,6,7,8:1有,2有,3有,4有,5没有,6有,7有,8有->10100101->逆序后10100101->0xA5

7,8:1,2,3,4,5,6没有,7有,8有->00000010->逆序后01000000->0x40

总结下如果第一个是1开头的那么得出的bit就是1,如果第一个是其他数字开头的,则 “1到其他数字” 之间用0来填充;


下面我们就根据总结出来的规律编写解析代码:


int process_byte(int nums)

{

int i;

u8 a=0;

u8 byte=0;

u8 new_array[8];

memset(new_array,0,sizeof(new_array));

for(i=0;i<=nums;i++) //1...7

{

timerecode[i]+=ONE_BIT_TIME/2;

timerecode[i]/=ONE_BIT_TIME;

}

if(timerecode[nums]>=9) //计算下标是从1开始的,并且去掉了9,所以剩下的也不多了

nums--;

if(nums<=0) //全0特殊处理

return 0;

find_max=nums; //去掉第一个0

/*

假设收到的是0,2,4,9

经过处理后就剩下2,4

将2放到new_array下标为1的地方,4放到下标为3的地方

*/

for(i=0;i

{

find_array[i]=timerecode[i+1];

if(find_array[i]!=0)

{

new_array[find_array[i]-1]=find_array[i];

}

}

/*

然后在for(i=find_array[0]-1;i<8;i++)

这个循环里找,如果对应的位有边沿改变,就改变a的值

找不到就复制a的值,这里有点要注意,i循环是从高电平

开始的

*/

for(i=find_array[0]-1;i<8;i++)

{

byte>>=1;

if(i==new_array[i]-1)

a=!a;

 

if(a)

byte|=0x80;

}

return byte;

}


对于上图0x37的例子,当定时器3中断时,就调用process_byte(nums-1),其中nums=6,就是边沿次数,来解析我们接收到的byte啦.

下面的截图是我用模拟串口编写的收发例子:```

![在这里插入图片描述](https://img-blog.csdnimg.cn/20190912085041489.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzMxMzY5Nw==,size_16,color_FFFFFF,t_70)

收发48个任意字节都正常,没有哪个是解析出错的。


发送部分:这部分完全依赖定时器7了,定时周期104us。

发送的原理很简单,我这里没有使用delay方式来发送,而是在定时器里实现发送的;


#define VIR_TXBUFF_SIZ 128

#define VIR_RXBUFF_SIZ 128


typedef struct {

//---------Rx------------

u8 rxov;

u8 rxlen;

u8 rxbuff_idx;

u8 rx_decode_flag;

u8 RxBuff[VIR_RXBUFF_SIZ];//大小一定要是2的次方关系

u8 RXREG;

//---------Tx------------

u8 send_flag;

u8 send_max;

u8 send_cnt;

u8 send_mode; //0-阻塞式发送 1-中断发送(放到sendbuff里,指定send_max即可)

u8 sendbuff[VIR_TXBUFF_SIZ];

u8 TXREG;


}VIRTUAL_UART_t;


static u8 send_a_byte(u8 dat)

{

if(VirtualUart.send_flag)

return 1;


VirtualUart.TXREG=dat;

TIM7->CR1 |= TIM_CR1_CEN;

Tx_Pin=0;

VirtualUart.send_flag=1;


return 0;


}


static void send_remain_byte(void)

{

if(VirtualUart.send_cnt>=VirtualUart.send_max)

{

VirtualUart.send_flag=0; //发送完毕

}

else

{

VirtualUart.TXREG=VirtualUart.sendbuff[VirtualUart.send_cnt++];

Tx_Pin=0; //产生START信号

}

}


//TIM7定时器里调用

static u8 tim_send_byte(void (*Callback)(void))

{

static int sendidx=0;

sendidx++;


if(sendidx<=8) //DATA 1,2,3...8

{

Tx_Pin=VirtualUart.TXREG&0x01;

VirtualUart.TXREG>>=1;

} else if(sendidx==9) { //STOP

Tx_Pin=1;

else if(sendidx==10) { //STOP的发送完毕了

sendidx=0;

if(Callback!=NULL) //有多个字节要发送,调用回调函数继续发送下一个字节

Callback();

else

VirtualUart.send_flag=0; //已经发送完毕了

return 0;

}


return 1; //1-busy


}


//阻塞式发送

void vu_send_string(u8 *s)

{

VirtualUart.send_mode=0;

while(*s)

{

while(VirtualUart.send_flag!=0);

send_a_byte(*s);

s++;

}

}

//阻塞式发送

void vu_send_len(u8 *s,int len)

{

VirtualUart.send_mode=0;

while(len–)

{

while(VirtualUart.send_flag!=0);

send_a_byte(*s);

s++;

}

}

//中断发送

int vu_send_some_byte_noblock(int len)

{

if(VirtualUart.send_flag)

return -1;

if(len>VIR_TXBUFF_SIZ)

return -2;


VirtualUart.send_mode=1;

VirtualUart.TXREG=VirtualUart.sendbuff[0];

TIM7->CR1 |= TIM_CR1_CEN;

Tx_Pin=0; //产生START信号

VirtualUart.send_flag=1;

VirtualUart.send_max=len;

VirtualUart.send_cnt=1;

return 0;



//主要发送部分代码就在这儿了

void TIM7_IRQHandler(void)

{

TIM7->SR = (uint16_t)~TIM_IT_Update;


if(VirtualUart.send_mode) {

if(tim_send_byte(send_remain_byte)==0) {

if(VirtualUart.send_flag==0)

TIM7->CR1 &= ~TIM_CR1_CEN;

TIM7->CNT=0;

}

}else {

if(tim_send_byte(NULL)==0) {

TIM7->CR1 &= ~TIM_CR1_CEN;

TIM7->CNT=0;

}

}


}


关键字:STM32  IO口  模拟串口  数据通信 编辑:什么鱼 引用地址:http://news.eeworld.com.cn/mcu/ic501618.html 本网站转载的所有的文章、图片、音频视频文件等资料的版权归版权所有人所有,本站采用的非本站原创文章及图片等内容无法一一联系确认版权者。如果本网所选内容的文章作者及编辑认为其作品不宜公开自由传播,或不应无偿使用,请及时通过电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。

上一篇:STM32之SWD连接配置说明
下一篇:STM32 IO模拟实现软件串口

关注eeworld公众号 快捷获取更多信息
关注eeworld公众号
快捷获取更多信息
关注eeworld服务号 享受更多官方福利
关注eeworld服务号
享受更多官方福利

推荐阅读

关于STM32CubeMx printf重定向,及报错。"FILE" is undefined
PFP *//* USER CODE BEGIN 0 */PUTCHAR_PROTOTYPE{    HAL_UART_Transmit(&huart1 , (uint8_t *)&ch, 1, 0xFFFF);    return ch;}/* USER CODE END 0 */ 这样写会报错"FILE" is undefined  添加头文件 stdio.h即可
发表于 2020-06-06
【STM32】keil MDK下重定向printf到串口(基于STM32CubeMX)
概述在keil MDK环境下重定向printf与keil C51不同,由于本人使用了STM32CubeMX生成工程模板,HAL_USART_Transmit函数即是模板里串口输出的函数。由于printf最终是调用fputc输出数据,fputc是一个弱引用(weak)函数,覆写即可重定向printf。代码清单extern USART_HandleTypeDef husart1;int fputc(int ch, FILE *f) {    HAL_USART_Transmit(&husart1, (uint8_t *)&ch, 1, 0xFFFF);    return ch
发表于 2020-06-06
STM32CubeMx启动串口调试功能Printf调试
## 概述项目中往往需要调试信息,调试stm32的时候,需要标准库里面的printf函数。在keil MDK环境下重定向printf与keil C51不同,由于本人使用了STM32CubeMX生成工程模板,HAL_USART_Transmit函数即是模板里串口输出的函数。由于printf最终是调用fputc输出数据,fputc是一个弱引用(weak)函数,覆写即可重定向printf。代码清单/* USER CODE BEGIN Includes */#include "FreeRTOS.h"#include "task.h"#include "queue.h"
发表于 2020-06-06
STM32CubeMx启动串口调试功能Printf调试
STM32F1xx HAL库中文版——USART篇
38.1 UART Firmware driver registers structures //串口固件驱动寄存器结构38.1.1 UART_InitTypeDefUART_InitTypeDef被定义在stm32f1xx_hal_uart.h头文件中数据字段:• uint32_t BaudRate 波特率• uint32_t WordLength 字长• uint32_t StopBits 停止位• uint32_t Parity 奇偶校验位• uint32_t Mode 模式• uint32_t HwFlowCtl 硬件流控制• uint32_t OverSampling 过采样字段的文档:• uint32
发表于 2020-06-06
Stm32-输入捕获
输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32 的定时器,除了 TIM6 和 TIM7,其他定时器都有输入捕获功能。STM32 的输入捕获,简单地说就是通过检测 TIMx_CHx 上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)中。1. 相关寄存器介绍1) 捕获/比较模式寄存器 (TIMx_CCMRx) 当在输入捕获模式下使用的时候,对应上图的第二行描述,从图中可以看出,TIMx_CCMR1 明显是针对 2 个通道的配置,低八位[7:0]用于捕获/比较通道 1 的控制,而高八位[15:8]则用
发表于 2020-06-06
Stm32-输入捕获
STM32库函数和寄存器的区别
库函数版和寄存器版的系统时钟设置的区别:**1.**库函数的目的是让用户应用的,而寄存器更加原始库函数的系统时钟是默认设置的,且放在启动文件里。而寄存器版的系统时钟是Stm32_Clock_Init(336,8,2,7);.**2.**库函数的快捷的,但不是每个芯片都有的;寄存器是复杂的,但是每个芯片厂商都有提供系统的寄存器设置信息。分别打开库函数和寄存器版的I/O口设置:库函数:RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);gotoh后先通过assert_param();函数检查格式是否正确同时只要是ENABLE,RCC->AHB1ENR
发表于 2020-06-06
STM32库函数和寄存器的区别
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京ICP证060456号 京ICP备10001474号 电信业务审批[2006]字第258号函 京公海网安备110108001534 Copyright © 2005-2020 EEWORLD.com.cn, Inc. All rights reserved