实现USART串口通信及printf重定向

通信协议

常见通信协议

传输模式

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

  • 单工通信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,要先挂载到时钟上。

c 复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

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

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

c 复制代码
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定时器中,也是同样的结构。

c 复制代码
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位刚好是一个字节的长度,可能省掉很多工作。

发送数据

c 复制代码
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的基础上,字符串、字节流、多位数都可以视作多个字符的序列。

c 复制代码
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 消息需要发送到哪一个通信接口上,这个过程一般被称做"重定向"。

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

也就是添加这几行:

c 复制代码
#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的作用是启用中断、配置优先级。

c 复制代码
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:子优先级,抢占优先级相同时比较。

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

c 复制代码
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

实现中断处理函数

c 复制代码
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。

c 复制代码
if (Usart1_GetRxFlag())
{
    OLED_ShowHexNum(1,9,Usart1_GetRxData(),2);
}

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

接受字节流/字符串

Rx_Data的数据类型是uint8_t,最多只能存储8位数据。

如果要存储多位,可以使用数组。

对于接收到的数据,为了保证数据的有效性,我们可以约定请求头和请求尾,符合要求才会视为成功接收。

c 复制代码
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个比特位。

可以配合串口收发装置验证结果。

c 复制代码
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);
			}
		}
	}
}

参考

相关推荐
lb36363636363 小时前
介绍一下数组(c基础)(详细版)
c语言
一丝晨光5 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
执笔者5486 小时前
C语言:函数栈帧的创建与销毁
c语言
_小柏_7 小时前
C/C++基础知识复习(15)
c语言·c++
weixin_399264297 小时前
信捷 PLC C语言 POU 指示灯交替灭0.5秒亮0.5秒(保持型定时器)
c语言·开发语言
Darkwanderor7 小时前
用数组实现小根堆
c语言·数据结构·二叉树·
敲上瘾8 小时前
C++11新特性(二)
java·c语言·开发语言·数据结构·c++·python·aigc
知星小度S10 小时前
数据结构——排序
c语言·数据结构·算法
lb363636363610 小时前
扫雷游戏代码分享(c基础)
c语言·算法·游戏
DdddJMs__13513 小时前
C语言 | Leetcode C语言题解之第542题01矩阵
c语言·leetcode·题解