热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

STM32IO口模拟串口通讯

转自:http:ziye334.blog.163.comblogstatic224306191201452833850647前阵子,调项目时需要用到低波

转自:http://ziye334.blog.163.com/blog/static/224306191201452833850647

 

前阵子,调项目时需要用到低波特率串口通讯(300的波特率),才发下发现在正常情况下(PCLK1时钟频率为72M,PCLK2时钟频率为36M):STM32的USART0的最低波特率只能设置到1200,;而USART1最低波特率只能设置到600。怎么设置STM32的600或以下的波特率呢?有两种方法:一种是改变外设时钟频率,而另一种方法就是使用IO口模拟串口通讯。今天就来讲讲,用IO口模拟串口通信!

1、串口传输协议

首先,必须要知道串口通讯时数据是怎样传输的?这里以异步传输字符为例子,如下图所示:
 一般字符传输都采用:1位起始位,8位数据位,1位停止位,没有校验位 的形式传输,其他形式的这里不讲。串口异步传输在空闲状态时都必须是高电平。第一位传输的是起始位,起始位会将原来空闲时的高电平拉成低电平,起始位用来来标识数据开始传输,提示接收方准备开始接收数据;当接收方第一次检测到一个下降沿时,就表示接收到了起始位。起始位后就是8位的数据位,接收方在接收每一位数据的时候会采集几十次,如果结果都是低电平,则接收到的数据位0,如果结果都是高电平,则棘手到的数据位是1。1位停止位会将电平拉成高电平,以为接收下一个数据做准备。


2、IO模拟串口发送程序

IO口模拟串口发送数据,必须严格按照上面的异步传输协议。我们用伪代码实现这一过程:

void VirtualCOM_ByteSend(u8 val)

{

u8 i;

IO_LOW(); //起始位,拉低电平

Delay(sometime);

for(i &#61; 0; i <8; i&#43;&#43;) //8位数据位

{

if(val & 0x01)

IO_HIGH();

else

IO_LOW();

Dealy(sometime);

val >>&#61; 1;

}

IO_HIGH(); //停止位&#xff0c;拉高电平

Delay(sometime);

}


代码很简单&#xff0c;思路也很清晰&#xff0c;完全是按照异步传输的过程写的。这里最重要的是Delay(sometime)&#xff0c;sometime的延时时间就决定了传输的速度&#xff0c;sometime去取某些值才可以设置程序标准的串口波特率(1200、2400、9600、38400、115200等等)。

下面&#xff0c;我采用STM32开发板实现IO模拟串口发送。

&#xff08;1&#xff09;选择IO引脚设置为虚拟串口TX引脚

我选择PA4引脚来模拟串口的TX引脚&#xff0c;所以需要配置下PA4这个引脚为推挽输出&#xff1a;

#define COM_TX_PORT GPIOA

#define COM_TX_PIN GPIO_Pin_4

void VirtualCOM_TX_GPIOConfig(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

/* PA4最为数据输出口&#xff0c;模拟TX */

GPIO_InitStructure.GPIO_Pin &#61; COM_TX_PIN;
GPIO_InitStructure.GPIO_Mode &#61; GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed &#61; GPIO_Speed_50MHz;
GPIO_Init(COM_TX_PORT, &GPIO_InitStructure);
GPIO_SetBits(COM_TX_PORT, COM_TX_PIN);

}

这里需要说明的是&#xff0c;在配置完引脚后&#xff0c;需要将PA4引脚拉高&#xff0c;这样做是为了防止在发送数据起始位时&#xff0c;由于原来引脚是低电平而导致没有产生一个下降沿信号。

&#xff08;2&#xff09;IO模拟串口发送一个字节

遵循串口异步传输协议&#xff0c;编写了STM32上面的相应代码&#xff1a;

#define COM_TX_PORT GPIOA

#define COM_TX_PIN GPIO_Pin_4

#define COM_DATA_HIGH() GPIO_SetBits(COM_TX_PORT, COM_TX_PIN) //高电平
#define COM_DATA_LOW() GPIO_ResetBits(COM_TX_PORT, COM_TX_PIN) //低电平

u32 delayTime;

void VirtualCOM_ByteSend(u8 val)
{
u8 i &#61; 0;
COM_DATA_LOW(); //起始位
Delay_us(delayTime);
for(i &#61; 0; i < 8; i&#43;&#43;) //8位数据位
{
if(val & 0x01)
COM_DATA_HIGH();
else
COM_DATA_LOW();
Delay_us(delayTime);
val >>&#61; 1;
}
COM_DATA_HIGH(); //停止位
Delay_us(delayTime);
}

&#xff08;3&#xff09;IO模拟串口发送字符串

既然发送一个字节的函数已将实现了&#xff0c;那么发送字符串函数就简单了&#xff1a;

void VirtualCOM_StringSend(u8 *str)
{
while(*str !&#61; 0)
{
VirtualCOM_ByteSend(*str);
str&#43;&#43;;
}
}

 
3、IO模拟接收程序
接收的代码比发送的代码复杂些。先讲讲怎么IO口接收数据的思路。为了接收数据&#xff0c;IO引脚必须可以检测到传输数据的起始位&#xff0c;检测起始位其实相当于与要检测一个下降沿信号&#xff0c;那么引脚只要配置成外部中断模式就可以检测到这个起始信号。然后根据传输速率配置一个相应时间定时的定时器。当检测到起始信号后&#xff0c;打开该定时器&#xff0c;每隔一定时间就会进入定时器中断&#xff0c;检测当前的IO引脚高低电平&#xff0c;从而决定接收到的数据是‘1’还是‘0’。当第九次进入定时器中断服务程序时&#xff0c;说明已经收到了一个字节的数据&#xff0c;此时关闭定时器。
下面在讲讲怎么在STM32开发板上实现这一过程。
&#xff08;1&#xff09;选择IO引脚模拟串口接收引脚RX
我选择PA5来模拟串口的接收引脚RX&#xff0c;所以需要配置PA5为输入模式&#xff0c;同时打开它的外部中断。

#define COM_RX_PORT GPIOA

#define COM_RX_PIN GPIO_Pin_5

void VirtualCOM_RX_GPIOConfig(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

EXTI_InitTypeDef EXTI_InitStruct;

NVIC_InitTypeDef NVIC_InitStructure;

 

/* PA5最为数据输入&#xff0c;模拟RX */
GPIO_InitStructure.GPIO_Pin &#61; COM_RX_PIN;
GPIO_InitStructure.GPIO_Mode &#61; GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed &#61; GPIO_Speed_50MHz;
GPIO_Init(COM_RX_PORT, &GPIO_InitStructure);

 

EXTI_InitStruct.EXTI_Line&#61;EXTI_Line5;
EXTI_InitStruct.EXTI_Mode&#61;EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger&#61;EXTI_Trigger_Falling;//下降沿都中断
EXTI_InitStruct.EXTI_LineCmd&#61;ENABLE;
EXTI_Init(&EXTI_InitStruct);

 

NVIC_InitStructure.NVIC_IRQChannel&#61;EXTI9_5_IRQn; //外部中断&#xff0c;边沿触发
NVIC_InitStructure.NVIC_IRQChannelSubPriority &#61; 2;
NVIC_InitStructure.NVIC_IRQChannelCmd&#61;ENABLE;
NVIC_Init(&NVIC_InitStructure);

}

&#xff08;2&#xff09;配置一个定时器用来定时接收数据
我配置TIM2定时器为一定的定时周期&#xff0c;在它的中断服务程序中读取串口发送过来数据。定时器配置代码如下&#xff1a;

void TIM2_Configuration(u16 period)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//使能TIM2的时钟
TIM_DeInit(TIM2); //复位TIM2定时器
TIM_InternalClockConfig(TIM2); //采用内部时钟给TIM2提供时钟源

TIM_TimeBaseStructure.TIM_Prescaler &#61; 72 - 1; //预分频系数为72&#xff0c;这样计数器时钟为72MHz/72 &#61; 1MHz
TIM_TimeBaseStructure.TIM_ClockDivision &#61; 0; //设置时钟分频
TIM_TimeBaseStructure.TIM_CounterMode &#61; TIM_CounterMode_Up;//设置计数器模式为向上计数模式
TIM_TimeBaseStructure.TIM_Period &#61; period - 1; //设置计数溢出大小&#xff0c;每计period个数就产生一个更新事件
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); //将配置应用到TIM2中

TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除溢出中断标志
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //开启TIM2的中断
TIM_Cmd(TIM2,DISABLE); //关闭定时器TIM2

 

NVIC_InitStructure.NVIC_IRQChannel &#61; TIM2_IRQn; //通道设置为TIM2中断
NVIC_InitStructure.NVIC_IRQChannelSubPriority &#61; 1;//响应式中断优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd &#61; ENABLE; //打开中断
NVIC_Init(&NVIC_InitStructure);
}

&#xff08;3&#xff09;IO口模拟串口接收功能的实现
IO口接收串口数据的功能是通过PA5引脚的外部中断服务程序与定时器的中断服务程序相互配合实现的。首先需要为数据定一些状态机&#xff0c;方便标识接收到数据的状态&#xff1a;

enum{
COM_START_BIT, //停止位
COM_D0_BIT, //bit0
COM_D1_BIT, //bit1
COM_D2_BIT, //bit2
COM_D3_BIT, //bit3
COM_D4_BIT, //bit4
COM_D5_BIT, //bit5
COM_D6_BIT, //bit6
COM_D7_BIT, //bit7
COM_STOP_BIT, //bit8
};

定义好了状态机&#xff0c;还需要一个变量&#xff0c;来保存这些状态机的变化&#xff0c;并定义它的初始状态为COM_STOP_BIT&#xff1a;

u8 recvStat &#61; COM_STOP_BIT; //定义状态机

下面是PA5的外部中断服务程序&#xff0c;它的主要任务是检测起始位&#xff0c;当他第一次检测到下降沿&#xff0c;则说明数据即将到来&#xff0c;这时只要打开定时器就可以了&#xff1a;

#define COM_RX_STAT GPIO_ReadInputDataBit(COM_RX_PORT, COM_RX_PIN)

void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line5)!&#61;RESET)
{
if(!COM_RX_STAT) //检测引脚高低电平&#xff0c;如果是低电平&#xff0c;则说明检测到下升沿
{
if(recvStat &#61;&#61; COM_STOP_BIT) //状态为停止位
{
recvStat &#61; COM_START_BIT; //接收到开始位
Delay(1000); //延时一定时间
TIM_Cmd(TIM2, ENABLE); //打开定时器&#xff0c;接收数据
}
}
EXTI_ClearITPendingBit(EXTI_Line5); //清除EXTI_Line1中断挂起标志位
}
}

上面代码中&#xff0c;检测到下降沿并设置了状态之后&#xff0c;延时了一定的时候&#xff0c;才打开定时器&#xff0c;这样做的原因是让定时器每次在信号的中间检测&#xff0c;而不要在信号边沿检测。正如下面图所示&#xff1a;
 下面就是定时器的中断服务程序&#xff0c;它主要是接收串口发送过来的数据&#xff0c;在它之前我们需要线定义一个变量用来保存接收到的数据&#xff1a;

u8 recvData;

然后&#xff0c;定时器中断中&#xff0c;每收到1位数据就改变下状态机并同时写入这个recvData对应的数据位中&#xff0c;当收到8为数据后&#xff0c;然后关闭定时器定时&#xff0c;以等待新的数据到来&#xff1a;

void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) !&#61; RESET) //检测是否发生溢出更新事件
{
TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);//清除中断标志
recvStat&#43;&#43;; //改变状态机
if(recvStat &#61;&#61; COM_STOP_BIT) //收到停止位
{
TIM_Cmd(TIM2, DISABLE); //关闭定时器
return; //并返回
}
if(COM_RX_STAT) //&#39;1&#39;
{
recvData |&#61; (1 << (recvStat - 1));
}
else //&#39;0&#39;
{
recvData &&#61; ~(1 <<(recvStat - 1));
}
}
}

4、一些精确延时与不精确延时的实现
上面代码中&#xff0c;需要定义一个不精确定时与两个精确定时&#xff0c;分别用在检测到下降沿后延时一段时间在打开定时器和控制传输速率中&#xff1a;

void Delay(u32 t)
{
while(t--);
}

void Delay_us(u32 nus)
{
SysTick->LOAD&#61;nus*9; //时间加载
SysTick->CTRL|&#61;0x01; //开始倒数
while(!(SysTick->CTRL&(1<<16)));//等待时间到达
SysTick->CTRL&#61;0X00000000; //关闭计数器
SysTick->VAL&#61;0X00000000; //清空计数器
}

void Delay_ms(u16 nms)
{
SysTick->LOAD&#61;(u32)nms*9000; //给重装载寄存器赋值&#xff0c;9000时&#xff0c;将产生1ms的时基
SysTick->CTRL|&#61;0x01; //开始倒数
while(!(SysTick->CTRL&(1<<16))); //等待时间到达
SysTick->CTRL&#61;0X00000000; //关闭计数器
SysTick->VAL&#61;0X00000000; //清空计数器
}

5、编写一个配置波特率的函数

这里能配置的只有300、600、1200三种波特率&#xff0c;其他的波特率我不想弄&#xff0c;也没有必要弄。下面编写一个初始化IO模拟的串口&#xff0c;包括引脚配置、波特率设置、定时时间设置等&#xff1a;

#define _300BuadRate 3150
#define _600BuadRate 1700
#define _1200BuadRate 800

void VirtualCOM_Config(u16 baudRate)

{
u32 period;
VirtualCOM_TX_GPIOConfig();

VirtualCOM_RX_GPIOConfig();
if(baudRate &#61;&#61; _300BuadRate) //波特率300
period &#61; _300BuadRate &#43; 250;
else if (baudRate &#61;&#61; _600BuadRate) //波特率600
period &#61; _600BuadRate &#43; 50;
else if (baudRate &#61;&#61; _1200BuadRate) //波特率1200
period &#61; _1200BuadRate &#43; 50;
TIM2_Configuration(period); //设置对应模特率的定时器的定时时间
delayTime &#61; baudRate; //设置IO串口发送的速率
}

要问上面的那些数字是怎么得来的&#xff0c;实话说&#xff1a;我是试出来&#xff0c;但是是有根据地试出来的。我以波特率为1200为例&#xff1a;IO串口发送函数VirtualCOM_ByteSend()中&#xff0c;我们用Delay_us(delayTime)来控制传输的速率&#xff0c;如果波特率设为1200&#xff0c;则1/1200&#61;830us相当于没830us传输1bit数据&#xff0c;所以在delayTime理论上应该设为830才能保证以波特率1200的速率发送数据&#xff0c;但是由于发送是由代码实现&#xff0c;有一定的延时&#xff0c;而不像真正串口通过移位寄存器发送那样快速&#xff0c;所以需要将delayTime的值在830附近调整&#xff0c;最后我试出来delayTime&#61;800时&#xff0c;正好实现了波特率为1200的速率发送。
同样的&#xff0c;在IO串口接收时&#xff0c;需要设定定时周期&#xff0c;这个周期也是试出来的&#xff0c;但是也是有依据的。还是以1200波特率接收为例&#xff1a;理论上应该设置定时时间为1/1200&#61;830us&#xff0c;则需要的定时值为72000000/(1/830us)&#61;72*830&#xff0c;这里设置定时器的预分频为72&#xff0c;则周期值应该为830&#xff0c;所以上面代码中period的理论上应该等于830&#xff0c;但是接收是由代码写成的&#xff0c;有一定的延时&#xff0c;而不像真正串口一样全部有硬件完成那样快速&#xff0c;所以需要将period的值在830附近调整&#xff0c;最后试出来period&#61;850时&#xff0c;可以正常接收串口发送过来的数据。
6、其他函数的编写
首先需要编写的BSP_Init()函数&#xff0c;来初始化板子的其他一些外设的的初始化&#xff1a;

void BSP_Init(void)

{

static volatile ErrorStatus HSEStartUpStatus &#61; SUCCESS;

RCC_DeInit(); //默认配置SYSCLK, HCLK, PCLK2, PCLK1, 复位后就是该配置

RCC_HSEConfig(RCC_HSE_ON); //使能外部高速晶振
HSEStartUpStatus &#61; RCC_WaitForHSEStartUp();//等待外部高速稳定

if(HSEStartUpStatus &#61;&#61; SUCCESS)
{
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);//使能flash预读取缓冲区
FLASH_SetLatency(FLASH_Latency_2); //令Flash处于等待状态&#xff0c;2是针对高频时钟的
RCC_HCLKConfig(RCC_SYSCLK_Div1); //HCLK &#61; SYSCLK 设置高速总线时钟&#61;系统时钟
RCC_PCLK2Config(RCC_HCLK_Div1); //PCLK2 &#61; HCLK 设置低速总线2时钟&#61;高速总线时钟
RCC_PCLK1Config(RCC_HCLK_Div2); //PCLK1 &#61; HCLK/2 设置低速总线1的时钟&#61;高速时钟的二分频
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); //PLLCLK &#61; 8MHz * 9 &#61; 72 MHz 利用锁相环讲外部8Mhz晶振9倍频到72Mhz
RCC_PLLCmd(ENABLE); //使能PLL锁相环
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) &#61;&#61; RESET){} //等待锁相环输出稳定
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//将锁相环输出设置为系统时钟
while(RCC_GetSYSCLKSource() !&#61; 0x08){} //等待校验成功
} //使能GPIO口所使用的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOF|RCC_APB2Periph_GPIOG, ENABLE);
最后是main函数&#xff0c;main函数很简单&#xff0c;只要调用配置IO串口的配置函数就可以了&#xff1a;

extern u8 recvData;

int main(void)

{

BSP_Init();

VirtualCOM_Config(_600BuadRate); //配置IO模拟串口的波特率为600

VirtualCOM_StringSend("HelloWorld!\r\n"); //发送“HelloWorld!”字符串

while(1)

{

VirtualCOM_ByteSend(recvData);

Delay(5000000);

}

}

7、现象

首先。我们需要用TTL转USB的串口线&#xff0c;连接到电脑&#xff0c;打开串口调试工具&#xff0c;设置波特率为600&#xff0c;1位停止位&#xff0c;然后就可以收到IO模拟串口发过来的"HelloWorld"&#xff0c;然后&#xff0c;我们发送一个字符&#39;a&#39;过去&#xff0c;然后就会每间隔一段时间打印出该字符。如下图所示&#xff1a;



推荐阅读
  • 本文将详细探讨Linux pinctrl子系统的各个关键数据结构,帮助读者深入了解其内部机制。通过分析这些数据结构及其相互关系,我们将进一步理解pinctrl子系统的工作原理和设计思路。 ... [详细]
  • 配置Windows操作系统以确保DAW(数字音频工作站)硬件和软件的高效运行可能是一个复杂且令人沮丧的过程。本文提供了一系列专业建议,帮助你优化Windows系统,确保录音和音频处理的流畅性。 ... [详细]
  • 本题探讨了在一个有向图中,如何根据特定规则将城市划分为若干个区域,使得每个区域内的城市之间能够相互到达,并且划分的区域数量最少。题目提供了时间限制和内存限制,要求在给定的城市和道路信息下,计算出最少需要划分的区域数量。 ... [详细]
  • 本次考试于2016年10月25日上午7:50至11:15举行,主要涉及数学专题,特别是斐波那契数列的性质及其在编程中的应用。本文将详细解析考试中的题目,并提供解题思路和代码实现。 ... [详细]
  • 作者:守望者1028链接:https:www.nowcoder.comdiscuss55353来源:牛客网面试高频题:校招过程中参考过牛客诸位大佬的面经,但是具体哪一块是参考谁的我 ... [详细]
  • Startup 类配置服务和应用的请求管道。Startup类ASP.NETCore应用使用 Startup 类,按照约定命名为 Startup。 Startup 类:可选择性地包括 ... [详细]
  • 本题旨在通过给定的评级信息,利用拓扑排序和并查集算法来确定全球 Tetris 高手排行榜。题目要求判断是否可以根据提供的信息生成一个明确的排名表,或者是否存在冲突或信息不足的情况。 ... [详细]
  • 通过Web界面管理Linux日志的解决方案
    本指南介绍了一种利用rsyslog、MariaDB和LogAnalyzer搭建集中式日志管理平台的方法,使用户可以通过Web界面查看和分析Linux系统的日志记录。此方案不仅适用于服务器环境,还提供了详细的步骤来确保系统的稳定性和安全性。 ... [详细]
  • 本文介绍了一种从与src同级的config目录中读取属性文件内容的方法。通过使用Java的Properties类和InputStream,可以轻松加载并获取指定键对应的值。 ... [详细]
  • 本文详细介绍了网络存储技术的基本概念、分类及应用场景。通过分析直连式存储(DAS)、网络附加存储(NAS)和存储区域网络(SAN)的特点,帮助读者理解不同存储方式的优势与局限性。 ... [详细]
  • 本文详细介绍了Java Web应用程序中的过滤器(Filter)功能,包括其作用、实现方式及配置方法。过滤器可以在请求到达目标资源之前对其进行预处理,并在响应返回给客户端之前进行后处理。 ... [详细]
  • 本文详细解释了华为ENSP模拟器中常用的命令,涵盖用户模式、系统模式、接口模式和地址池视图模式下的操作。这些命令对于进行计算机网络实验至关重要,帮助用户更好地理解和配置路由器及PC机的通信。 ... [详细]
  • 黑鸟安全团队发布最新警告,Apache Struts2框架曝出S2-048高危漏洞。目前该漏洞的攻击代码(POC)已公开,建议各企业和组织立即检查是否使用了受影响的Struts2版本,并采取相应措施进行防护。 ... [详细]
  • CentOS系统安装与配置常见问题及解决方案
    本文详细介绍了在CentOS系统安装过程中遇到的常见问题及其解决方案,包括Vi编辑器的操作、图形界面的安装、网络连接故障排除等。通过本文,读者可以更好地理解和解决这些常见问题。 ... [详细]
  • Struts与Spring框架的集成指南
    本文详细介绍了如何将Struts和Spring两个流行的Java Web开发框架进行整合,涵盖从环境配置到代码实现的具体步骤。 ... [详细]
author-avatar
捕鱼达人2602929461
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有