目录

一、串口介绍

二、串口通信时序(数据帧)

空闲状态

1.起始位

2.数据位(8bit)

3.校验位*

4.停止位

三、使用软件模拟串口通信(一般不用)

1.初始化

2.发送一字节

3.接收一字节

四、使用硬件串口

51单片机

1.初始化

2.发送一字节

3.接收一字节

STM32F1

1.初始化

2.发送数据

发送一字节

发送数据包(仅展示发送HEX数据包,发送文本数据包可直接用printf)

重定向printf

3.接收数据

接收一字节

接收HEX数据包

接收文本数据包


一、串口介绍

串口通信,全称串行通信,是一种数据逐位顺序传输的通信方式

  1. 最少两线制:只需要两根线——发送数据线(TX)和接收数据线(RX),即可实现全双工通信。通常还会包含地线(GND)作为电压参考。

  2. 点对点架构:通常用于两个设备之间的直接通信,不支持多主设备在同一对线上直接寻址。

  3. 无地址寻址:通信基于预先配置好的协议,通过物理连接直接进行,没有设备地址概念。地址识别需要依赖更高层协议(如Modbus)。

  4. 宽范围速率:通信速率(波特率)可配置范围很广,常见从几百bps到数Mbps,适应性很强。

  5. 全双工:可以同时进行数据的发送和接收,因为TX和RX线路是独立的。

  6. 无应答机制:数据发送后,不要求接收方为每个字节提供硬件层面的应答位,可靠性由软件或高层协议保障。

  7. 数据发送:低位先行。每个字节的数据从最低位开始传输。

串口通信通常需要至少三根线来完成:

  1. 发送线(TX):负责发送数据。

  2. 接收线(RX):负责接收数据。

  3. 地线(GND):作为共同的电压参考点,确保双方电平判断一致。

要使两个设备通过串口成功通信,它们的以下参数必须完全匹配

  1. 波特率:表示每秒传输的符号数。对于串口来说,基本等同于每秒传输的比特数。常见值有:9600, 115200等。如果双方波特率不匹配,接收到的将是乱码。

  2. 数据位:定义每个帧中数据部分的长度。通常为7或8位。

  3. 停止位:定义每个帧结束的标志长度。通常为1位。

  4. 校验位

    • None:无校验位。

    • Odd:奇校验,确保数据位和校验位中“1”的个数为奇数。

    • Even:偶校验,确保数据位和校验位中“1”的个数为偶数。

  5. 流控制:用于管理两个速度不匹配的设备间的数据流,防止数据丢失。常见的有:

    • 无流控制

    • 硬件流控制(RTS/CTS):使用额外的两根线(RTS请求发送,CTS清除发送)来协商何时可以发送数据。

    • 软件流控制(XON/XOFF):通过发送特殊的控制字符(XON=继续,XOFF=暂停)来控制数据流。

二、串口通信时序(数据帧)

串口通信一帧中每个功能位的位置是确定的

空闲状态

线路在无数据传输时保持高电平(逻辑1)

无校验的数据传输图

有校验的数据传输图

1.起始位

将高电平拉低产生下降沿

2.数据位(8bit)

实际要传输的数据,通常是7位或8位(一个字节)。从最低有效位(LSB)开始传输,即低位先行

3.校验位*

用于简单的错误检测(奇偶校验)。可以是奇校验、偶校验或无校验

4.停止位

总是高电平(逻辑1),用于表示一帧数据的结束。它同时为接收方提供了缓冲时间,为接收下一帧做准备。

重点在于电平状态而不是通过边沿判断是否结束

因为停止位前有数据位或校验位,它们可能也是高电平,所以判断一帧数据是否接收完毕不依靠电平从低到高的上升沿,而直接查看停止位的电平状态

三、使用软件模拟串口通信(一般不用)

软件模拟串口的方法在资源受限或需要特殊引脚配置的场景下非常有用,但需要注意其性能限制和时序精度要求。

  • 占用CPU资源较多

  • 波特率不能太高(通常<115200)

  • 时序精度依赖系统时钟

  • 波特率通过定时器或延时实现

以9600波特率,8位数据,无校验,1位停止位为例

1.初始化

#include "stm32f10x.h"

// 定义软件串口使用的GPIO引脚
#define SOFT_UART_TX_PORT GPIOA
#define SOFT_UART_TX_PIN GPIO_Pin_0
#define SOFT_UART_RX_PORT GPIOA
#define SOFT_UART_RX_PIN GPIO_Pin_1

// 定义波特率相关的定时器参数
#define BAUD_RATE 9600
#define SYSTEM_CLOCK 72000000 // 假设系统时钟为72MHz
#define BIT_TIME (SYSTEM_CLOCK / BAUD_RATE) // 计算一个位时间对应的定时器计数次数

// 全局变量
volatile uint8_t tx_data = 0; // 要发送的数据
volatile uint8_t rx_data = 0; // 接收到的数据
volatile uint8_t tx_bit_count = 0; // 发送位计数器
volatile uint8_t rx_bit_count = 0; // 接收位计数器
volatile uint8_t tx_active = 0; // 发送进行中标志
volatile uint8_t rx_active = 0; // 接收进行中标志
volatile uint16_t tx_buffer = 0; // 发送移位寄存器(包括起始位和停止位)
volatile uint16_t rx_buffer = 0; // 接收移位寄存器

// 函数声明
void SoftUART_Init(void);
void SoftUART_SendByte(uint8_t data);
void SoftUART_ReceiveStart(void);
void TIM2_IRQHandler(void); // 定时器中断处理函数
void EXTI1_IRQHandler(void); // 外部中断处理函数(用于RX检测起始位)

// 初始化软件串口
void SoftUART_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 配置TX引脚为推挽输出
    GPIO_InitStructure.GPIO_Pin = SOFT_UART_TX_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SOFT_UART_TX_PORT, &GPIO_InitStructure);
    GPIO_SetBits(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN); // 初始化为高电平

    // 配置RX引脚为浮空输入
    GPIO_InitStructure.GPIO_Pin = SOFT_UART_RX_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(SOFT_UART_RX_PORT, &GPIO_InitStructure);

    // 配置定时器2,用于产生位时间
    TIM_TimeBaseStructure.TIM_Period = BIT_TIME - 1; // 自动重装载值
    TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断

    // 配置定时器2的中断
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 配置RX引脚的外部中断(下降沿触发,即起始位)
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1);
    EXTI_InitStructure.EXTI_Line = EXTI_Line1;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    // 配置外部中断的中断
    NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

2.发送一字节

// 发送一个字节
void SoftUART_SendByte(uint8_t data) {
    while (tx_active); // 等待上一次发送完成

    // 构建发送帧:起始位(0) + 8位数据(LSB first) + 停止位(1)
    tx_buffer = (data << 1) | (1 << 9); // 停止位放在最高位,起始位自动为0(因为左移后最低位为0)
    tx_bit_count = 0;
    tx_active = 1;

    // 启动定时器,开始发送
    TIM2->CNT = 0;
    TIM_Cmd(TIM2, ENABLE);

    // 首先发送起始位(已经为0,所以直接拉低TX引脚)
    GPIO_ResetBits(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
}

// 在定时器中断中处理发送
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

        if (tx_active) {
            // 发送过程
            tx_bit_count++;
            if (tx_bit_count < 10) { // 一共10位(起始位1+数据位8+停止位1)
                // 逐位发送
                if (tx_buffer & (1 << tx_bit_count)) {
                    GPIO_SetBits(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
                } else {
                    GPIO_ResetBits(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
                }
            } else {
                // 发送完成,停止定时器,并置高TX引脚(空闲状态)
                GPIO_SetBits(SOFT_UART_TX_PORT, SOFT_UART_TX_PIN);
                tx_active = 0;
                TIM_Cmd(TIM2, DISABLE);
            }
        }

        if (rx_active) {
            // 接收过程(见下文)
        }
    }
}

3.接收一字节

// 在RX引脚的外部中断中检测起始位
void EXTI1_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line1) != RESET) {
        EXTI_ClearITPendingBit(EXTI_Line1);

        // 确保是起始位(低电平)
        if (GPIO_ReadInputDataBit(SOFT_UART_RX_PORT, SOFT_UART_RX_PIN) == Bit_RESET) {
            // 启动接收过程
            rx_active = 1;
            rx_bit_count = 0;
            rx_buffer = 0;

            // 启动定时器,并在位时间的一半处进行第一次采样(以便在位的中心采样)
            TIM2->CNT = 0;
            TIM_Cmd(TIM2, ENABLE);
        }
    }
}

// 在定时器中断中处理接收
// 注意:这段代码要放在定时器中断处理函数中(上面发送部分已经有一个定时器中断,所以合并在一起)
// 在TIM2_IRQHandler函数中,在发送处理之后,添加接收处理

// 在定时器中断中,除了发送处理,还有接收处理:
// if (rx_active) {
    rx_bit_count++;
    if (rx_bit_count == 1) {
        // 第一次中断发生在起始位期间,不做采样,等待到第一个数据位的中心
        // 重新设置定时器,使得下一次中断在位的中心(即0.5个位时间后)
        TIM2->ARR = BIT_TIME / 2 - 1;
    } else if (rx_bit_count >= 2 && rx_bit_count <= 9) {
        // 采样数据位(共8位)
        // 注意:数据位从LSB开始,所以第一个采样的是LSB
        if (GPIO_ReadInputDataBit(SOFT_UART_RX_PORT, SOFT_UART_RX_PIN)) {
            rx_buffer |= (1 << (rx_bit_count - 2));
        }
        // 如果不是最后一位,恢复定时器周期为一个位时间
        if (rx_bit_count < 9) {
            TIM2->ARR = BIT_TIME - 1;
        }
    } else if (rx_bit_count == 10) {
        // 采样停止位
        // 这里可以验证停止位是否为1,如果不是则说明可能有错误
        // 然后完成接收
        rx_active = 0;
        TIM_Cmd(TIM2, DISABLE);
        // 在这里,rx_buffer中就是接收到的数据(8位)
        // 可以调用一个回调函数或者设置标志位通知主程序
    }
// }

四、使用硬件串口

51单片机

  • SCON:串行控制寄存器

  • PCON:电源控制寄存器(包含波特率加倍位)

  • TMOD:定时器模式寄存器

  • TH1/TL1:定时器1重载值(用于波特率生成)

1.初始化

void UART_Init()
{
    //设置串口工作方式,8位UART,波特率可变
    SCON = 0x50;    // 0101 0000

    //设置定时器1为模式2(8位自动重装)
    TMOD &= 0x0F;   // 清除高4位
    TMOD |= 0x20;   // 设置定时器1为模式2
	
    //设置波特率(9600 @11.0592MHz)
    PCON |= 0x80;   // SMOD=1,波特率加倍
    TH1 = 0xFD;     // 波特率9600
    TL1 = 0xFD;
	
    //启动定时器1
    TR1 = 1;

    //可选:开启串口中断
    EA = 1;          // 开启总中断
	ET1 = 0;		 // 开启定时器1中断   
	ES = 1;          // 开启串口中断
}

2.发送一字节

void UART_SendByte(unsigned char Byte)
{
	SBUF = Byte;
	while(TI == 0);
	TI = 0;
}

3.接收一字节

void UART_Routine() interrupt 4
{
	//发的时候也可能收,收的时候也可能发,对单片机收进行判断
	//如果接收控制器工作(即外部发送数据)
	if(RI == 1)
	{
		P2 = SBUF;
		//UART_SendByte(SBUF);
		RI = 0;    //硬件只负责置1,需要软件清0
	}
}

STM32F1

1.初始化

/********************************** 串口初始化 **********************************/
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);					//将PA9引脚初始化为复用推挽输出
	
	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);					//将PA10引脚初始化为上拉输入
	
	/*USART初始化*/
	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;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

2.发送数据

发送一字节
/********************************** 串口发送一字节 **********************************/
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
发送数据包(仅展示发送HEX数据包,发送文本数据包可直接用printf)
uint8_t Serial_TxPacket[4];				//定义发送数据包数组,数据包格式:FF 01 02 03 04 FE

void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/************************** 串口发送HEX数据包 **************************/
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}
重定向printf
/****************************** 重定向printf底层函数 ******************************/
/* 需包含 stdio.h */
/* 在 Options for Target → Target 中勾选 Use MicroLIB */
/* 或在 Target → Code Generation 中选择 Use Standard Library */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

3.接收数据

接收一字节
/******************************** 串口接收标志位判断 ********************************/
/* 通过判断中断标志位也可以 */
uint8_t Serial_RxData;		   //定义串口接收的数据变量
uint8_t Serial_RxFlag;		   //定义串口接收的标志位变量,通过判断标志位确定有没有收到数据
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}

/********************************** 串口接收一字节 **********************************/
void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		Serial_RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		Serial_RxFlag = 1;										//置接收标志位变量为1
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);			//清除USART1的RXNE标志位
																//读取数据寄存器会自动清除此标志位
																//如果已经读取了数据寄存器,也可以不执行此代码
	}
}
接收HEX数据包
/********************************** 串口接收HEX数据包 **********************************/
uint8_t Serial_RxPacket[4];				//定义接收数据包数组
uint8_t Serial_RxFlag;					//定义接收数据包标志位

void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;	//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		
		/*使用状态机的思路,依次处理数据包的不同部分*/
		
		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == 0xFF)			//如果数据确实是包头
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;	//将数据存入数据包数组的指定位置
			pRxPacket ++;				//数据包的位置自增
			if (pRxPacket >= 4)			//如果收够4个数据
			{
				RxState = 2;			//置下一个状态
			}
		}
		/*当前状态为2,接收数据包包尾*/
		else if (RxState == 2)
		{
			if (RxData == 0xFE)			//如果数据确实是包尾部
			{
				RxState = 0;			//状态归0
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}
接收文本数据包
/********************************** 串口接收文本数据包 **********************************/
char Serial_RxPacket[100];		//定义接收数据包数组,数据包@开头,\r\n结尾
uint8_t Serial_RxFlag;			//定义接收数据包标志位,在主函数中处理完之后需将其清零

void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;	//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)	//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);			//读取数据寄存器,存放在接收的数据变量
		
		/*使用状态机的思路,依次处理数据包的不同部分*/
		
		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == '@' && Serial_RxFlag == 0)		//如果数据确实是包头,并且上一个数据包已处理完毕
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/
		else if (RxState == 1)
		{
			if (RxData == '\r')			//如果收到第一个包尾
			{
				RxState = 2;			//置下一个状态
			}
			else						//接收到了正常的数据
			{
				Serial_RxPacket[pRxPacket] = RxData;		//将数据存入数据包数组的指定位置
				pRxPacket ++;			//数据包的位置自增
			}
		}
		/*当前状态为2,接收数据包第二个包尾*/
		else if (RxState == 2)
		{
			if (RxData == '\n')			//如果收到第二个包尾
			{
				RxState = 0;			//状态归0
				Serial_RxPacket[pRxPacket] = '\0';			//将收到的字符数据包添加一个字符串结束标志
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐