前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实现USART串口协议及printf重定向

实现USART串口协议及printf重定向

作者头像
WuShF
发布2024-02-12 10:35:46
1350
发布2024-02-12 10:35:46
举报
文章被收录于专栏:笔记分享笔记分享

通信协议

常见通信协议

传输模式

数据通常是在两个站(点对点)之间进行传输,按照数据流的方向可分为三种传输模式:单工、半双工、全双工。

  • 单工通信simplex:只支持信号在一个方向上传输(正向或反向)。
  • 半双工通信half-duple:允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。
  • 全双工full-duplex:允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。

同步异步

  • 同步通信:收发设备双方会使用一根信号线表示时钟信号。双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
  • 异步通信:不使用时钟信号进行数据同步,异步通信在发送字符时,所发送的字符之间的时间间隔可以是任意的。接收端必须时刻做好接收的准备,发送端可以在任意时刻开始发送字符。必须在每一个字符中加入起始位、奇偶校验位、停止位等。某些通讯中还需要双方约定数据的传输速率,以便更好地同步 。适用于短距离、速率不高的情况下。

电平标准

电平标准是数据 1 和数据 0 的表达方式,是传输线缆中人为规定的电压与数据的对应关系,这种关系又将电平信号分为两种,单端信号和差分信号。

单端信号:一根电线承载表示信号的变化电压,而另一根电线连接到通常为接地的参考电压。

  • TTL电平:高电平(1)>2.4V,低电平(0)<0.4V。
  • RS232电平:-3V-15V表示1,+3V+6V表示0。

差分传输:区别于传统的一根信号线一根地线的单端信号传输,差分传输在这两根线上都传输信号,这两个信号的振幅相同,相位相反。在这两根线上的传输的信号就是差分信号。信号接收端比较这两个电压的差值来判断发送端发送的逻辑状态。在电路板上,差分走线必须是等长、等宽、紧密靠近、且在同一层面的两根线。

  • RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)。

发送数据

stm32中封装了USART(Universal synchronous and asynchronous receiver-transmitter,通用同步和异步收发器)库。 对于各项参数和寄存器允许我们粗略了解就可以收发消息。

配置时钟和gpio

这一行无需多言,要用到USART1,要先挂载到时钟上。

代码语言:javascript
复制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

接下来是挂载外设相关的gpio口。

收发数据只需要用到TX和RX。

代码语言:javascript
复制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

//tx
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//rx
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

对于TX所在的A9,因为是作为串口输出,所以配置为GPIO_Mode_AF_PP复用推挽输出。 对于A10,设置为GPIO_Mode_IPU上拉输入。

配置USART

USART_InitStructure只是在配置,USART_Init是使配置生效,USART_Cmd是启动设备。 在TIM定时器中,也是同样的结构。

代码语言:javascript
复制
USART_InitTypeDef	USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStructure);

USART_Cmd(USART1,ENABLE);
  • USART_BaudRate波特率:这需要通信双方提前协商好,库函数内部实现了波特率的计算,我们无需关注底层实现。
  • USART_HardwareFlowControl硬件流控制:不使用硬件流控制,即不使用RTS(请求发送)和CTS(清除发送)信号进行数据流控制。
  • USART_Mode:同时配置为发送和接收模式。
  • Parity奇偶校验:这种方式可靠性低,还占用一位,因此填USART_Parity_No不采用校验。
  • USART_StopBits:停止位设置为1位。这是每个数据字节之后发送的停止位的数量,1位是标准设置。
  • USART_WordLength:8位刚好是一个字节的长度,可能省掉很多工作。

发送数据

代码语言:javascript
复制
void Usart1_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

添加while循环,它等待直到USART1发送寄存器中的数据已经传输完成。 USART_GetFlagStatus是一个库函数,用于检查USART标志状态,而USART_FLAG_TXE表示USART发送缓冲区空闲标志。因此,这行代码会等待直到USART发送缓冲区为空闲,即数据已经传输完毕。

发送多字节数据

在一个Byte的基础上,字符串、字节流、多位数都可以视作多个字符的序列。

代码语言:javascript
复制
void Usart1_SendArray(uint8_t *Array,uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i++)
	{
		Usart1_SendByte(Array[i]);
	}
}

void Usart1_SendString(char *String)
{
	uint8_t i;
	for(i = 0; String[i] != '\0';i++)
	{
		Usart1_SendByte(String[i]);
	}
}

//进值转换
uint32_t Usart1_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

void Usart1_SendNum(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)							
	{
		Usart1_SendByte(Number / Usart1_Pow(10, Length - i - 1) % 10 + '0');
	}
}

比如发送数字123,实际是依次发送'1''2''3'。 串口通信采用的是最低有效位优先传输,接收方收到的是小端存储的二进制数据。 小端存储指的是一个字节内小端存储,而非整个数据块。

printf重定向

输出的消息可能会被发送到不同的通信接口,我们必须要告诉 printf 消息需要发送到哪一个通信接口上,这个过程一般被称做“重定向”。

如果没有配置输出的位置,那么会导致程序崩溃。

也就是添加这几行:

代码语言:javascript
复制
#include "stm32f10x.h"                  // Device header
#include "Usart.h"
//关闭ARM的半主机模式
#pragma import(__use_no_semihosting_swi)

struct __FILE { 
    int handle; /* Add whatever you need here */ 
};
FILE __stdout;
FILE __stdin;

int fputc(int ch, FILE *f) {
    Usart1_SendByte(ch);
    return ch;
}

void _sys_exit(int return_code) {

}

输出结果

接收数据

接收数据显然要用到中断,因为我不可能周期轮询串口状态:效率太低。

配置中断

中断通道可以在.s文件中查找。

NVIC_InitStructure的作用是启用中断、配置优先级。

代码语言:javascript
复制
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =1;
NVIC_Init(&NVIC_InitStructure);

USART_Cmd(USART1,ENABLE);
  • USART_ITConfig:启用了USART1的接收数据寄存器非空中断 (USART_IT_RXNE)。当接收缓冲区非空时,这个中断会触发,表示接收到了新的数据。
  • NVIC_InitStructure:用于配置中断控制器NVIC(Nested Vectored Interrupt Controller)。
  • NVIC_IRQChannelPreemptionPriority:抢占优先级,数值越低,优先级越高
  • NVIC_IRQChannelSubPriority:子优先级,抢占优先级相同时比较。

需要在主函数中进行中断优先级分组。

代码语言:javascript
复制
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

实现中断处理函数

代码语言:javascript
复制
uint8_t Rx_Data;
uint8_t Rx_Flag;
uint8_t Usart1_GetRxFlag()
{
    if (Rx_Flag == 1)
    {
        Rx_Flag = 0;
        return 1;
    }
    return 0;
}

uint8_t Usart1_GetRxData()
{
    return Rx_Data;
}

void USART1_IRQHandler()
{
    if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
    {
        Rx_Data = USART_ReceiveData(USART1);
        Rx_Flag = 1;
        USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);
    }
}

上面的代码会保留接收到的最后一个字节数据。新数据会覆盖掉Rx_Data。

代码语言:javascript
复制
if (Usart1_GetRxFlag())
{
    OLED_ShowHexNum(1,9,Usart1_GetRxData(),2);
}

在主函数中通过上面的代码,先读取是否有数据,然后取出,即可访问。

接受字节流/字符串

Rx_Data的数据类型是uint8_t,最多只能存储8位数据。 如果要存储多位,可以使用数组。 对于接收到的数据,为了保证数据的有效性,我们可以约定请求头和请求尾,符合要求才会视为成功接收。

代码语言:javascript
复制
void USART1_IRQHandler()
{
	if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
	{
		Rx_Data = USART_ReceiveData(USART1);
		if (RxState == 0)
		{
			if (Rx_Data == '@')
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1)
		{
			if (Rx_Data == '\r')
			{
				RxState = 2;
			}
			else
			{
				Rx_Packet[pRxPacket] = Rx_Data;
				pRxPacket++;			
			}
		}
		else if (RxState == 2)
		{
			if (Rx_Data== '\n')
			{
				RxState = 0;
				Rx_Packet[pRxPacket] = '\0';
				Rx_Flag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);
	}
}

把请求头和请求尾过滤掉,把请求体放到数组中。 读取还是同样的过程,不过现在返回值是char*指针而非8个比特位。 可以配合串口收发装置验证结果。

代码语言:javascript
复制
int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	Usart1_Init();
	OLED_Init();
	LED_Init();
	
	OLED_ShowString(1,1,"Rx_Data:");
	while(1)
	{
		if (Usart1_GetRxFlag())
		{
			if (strcmp(Rx_Packet,"LED_ON") == 0)
			{
				LED1_ON();
				OLED_ShowString(2,1,"LED_ON_OK");
				Usart1_SendString("LED_ON_OK!");
				
			}
			else if (strcmp(Rx_Packet,"LED_OFF") == 0)
			{
				LED1_OFF();
				OLED_ShowString(2,1,"LED_OFF_OK");
				Usart1_SendString("LED_OFF_OK!");
			}	
			else
			{
				OLED_ShowString(2,1,Rx_Packet);
				Usart1_SendString(Rx_Packet);
			}
		}
	}
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 通信协议
    • 传输模式
      • 同步异步
        • 电平标准
        • 发送数据
          • printf重定向
          • 接收数据
            • 接受字节流/字符串
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档