前言
串口,是一种应用范围十分广泛的通信接口,串口的成本较低,容易使用,通信线路简单,可以实现两个设备之间的通信。单片机的串口可以实现单片机与单片机,单片机与电脑,单片机与其他设备的通信,极大地扩展了单片机的应用范围,增强了单片机的硬件实力。
USART即通用同步/异步收发器,USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
USART的部分主要特性
- 全双工,异步通信
- 分数波特率发生器系统,发送和接受共用的可编程波特率,最高打4.5Mbits/s。
- 可编程数据字节长度(8位或9位)
- 可配置的停止位——支持1或2个停止位
- 单独的发送器和接收器使能位
- 检测标志
——接收缓冲器满
——发送缓冲器空
——传输结束标志位
- 校验控制
——发送校验位
——对接收数据进行校验
USART部分功能概述
任何USART双向通信至少需要两个脚,接收数据输入(RX)和发送数据输出(TX)。
- 总线在发送或接收前应处于空闲状态
- 一个起始位
- 一个数据字(8位或9位)
- 最低有效位在前
- 0.5,1,1.5,2个的停止位,由此表明数据帧的结束
USART框图
字长设置 
字长可通过编程USART_CR1寄存器中的M位,选择8位或9位。在起始位期间,TX脚处于低电平,在停止位期间处于高电平。
停止位
起始位侦测
起始位侦测,在空闲状态下,串口通信线路保持高电平,接收线路以波特率的16倍频率对信号进行采样,持续检测线路的状态。
当采样检测到从高电平(1)到低电平(0)的变化时,说明可能出现了起始位的下降沿。为了确保这不是噪声引起,接收电路会进一步确认。
在检测到下降沿之后,接收电路会在接下来的采样周期中继续采样,确认是否是起始位。接收电路会在第3次,第5次,第7次采样一批数据,并在第8次,第9次,第10采样一批数据。如果这两批采样结果都满足“每三次采样中至少有两个0”,则认为检测到了有效起始位。如果不满足条件,则认为是噪声干扰,重新回到空闲状态等待新的下降沿。
一旦确认起始位有效,接收电路会将第8,9,10次采样的位置对齐到数据位的中间位置,这样可以确保每个后续每个数据位的采样都在位的正中间,从而提高了数据接收的准确性。
如果有一次三个采样点上仅有2个时‘0’,那么起始位有效,但会设置NE噪声标志位。
波特率
发送器和接收器的波特率都由波特率寄存器BBR里的DIV值来确定。
这里的fck是外设的时钟,PCLK1用于USART2,3,4,5,PCLK2用于USART1.
1. UART波特率的基本原理
- 波特率表示每秒传输的符号数(通常为比特数),它决定了数据传输的速度。
- 在UART通信中,波特率是由时钟信号通过分频来生成的。
- STM32的UART模块使用一个波特率发生器,通过分频系统时钟(PCLK)来生成所需的波特率。
2. 为什么需要先除以分频因子(USARTDIV)?
(1)PCLK频率太高
- STM32的外设时钟(PCLK)通常是一个较高的频率,例如72 MHz、36 MHz等。
- 直接用如此高的时钟频率去驱动UART会导致难以生成低速的波特率(如9600 bps)。因此,必须对PCLK进行分频。
(2)分频因子的作用
- 分频因子(USARTDIV)是一个可编程值,用于将PCLK分频到一个适合生成波特率的频率范围。
- 公式如下:
UART时钟=PCLK/USARTDIV
- USARTDIV的值可以通过计算得出,确保最终的UART时钟接近所需的波特率。
3. 为什么还需要再除以16?
(1)过采样技术
- STM32的UART模块采用16倍过采样技术,即每个数据位的时间宽度被分为16个采样点。
- 这种过采样技术可以提高数据接收的精度和抗噪声能力。
- 因此,UART的实际波特率是基于“分频后的时钟再除以16”来生成的。
(2)波特率公式
- 综合上述两步,波特率的计算公式为:
波特率=PCLK /(USARTDIV×16)
- 通过这种方式,可以更精细地调整波特率,使其更接近目标值。
4. 分数波特率发生器
- 在某些情况下,即使使用整数分频因子(USARTDIV),也可能会导致波特率存在一定的误差。
- 为了解决这个问题,STM32引入了分数波特率发生器,允许对USARTDIV的小数部分进行调整。
- 分数部分的精度通常是1/16,这样可以进一步减小波特率误差。
5. 总结:为什么要先除以分频因子再除以16?
分频因子(USARTDIV)的作用:
- 将高频的PCLK分频到一个较低的频率范围,便于生成目标波特率。
16倍过采样的作用:
- 通过对分频后的时钟再除以16,实现高精度的数据采样和抗噪声能力。
- 这种设计使得UART可以在不同的波特率下保持稳定性和可靠性。
灵活性和精度:
- 结合分频因子和16倍过采样,STM32可以灵活地支持各种波特率,并通过分数波特率发生器进一步提高精度。
示例计算
假设:
- PCLK = 72 MHz
- 目标波特率 = 9600 bps
根据公式:
USARTDIV=PCLK波特率×16USARTDIV=波特率×16PCLK
代入数值:
USARTDIV=72,000,0009600×16=468.75USARTDIV=9600×1672,000,000=468.75
- 整数部分为468,小数部分为0.75。
- 配置USARTDIV寄存器时,将整数部分写入DIV_Mantissa,小数部分写入DIV_Fraction。
通过这种方式,STM32可以精确生成9600 bps的波特率。
过采样技术:
- STM32使用波特率的16倍频率对输入信号进行过采样。这意味着在一个比特时间内,会进行16次采样。
- 过采样的好处是可以更精确地定位起始位、数据位和停止位,并且有助于识别可能的噪声干扰。
技术实现
连线图
(自己在PA2引脚插个LED)
代码实现
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" //延时函数
#include "OLED.h"
#include "Serial.h"
#include "LED.h"uint8_t Key_Num;int main(void)
{/*初始化*/LED_Init(); Key_Init();OLED_Init();Serial_Init();Serial_TxPacket[0] = 0x1f;Serial_TxPacket[1] = 0x2f;Serial_TxPacket[2] = 0x3f;Serial_TxPacket[3] = 0x4f;OLED_ShowString(1,1,"RxData:");OLED_ShowString(3,1,"TxData:");while(1){Key_Num = Key_GetNum();if(Key_Num == 1){Serial_TxPacket[0]++;Serial_TxPacket[1]++;Serial_TxPacket[2]++;Serial_TxPacket[3]++;Serial_SendPacket();OLED_ShowHexNum(4,1,Serial_TxPacket[0],2);OLED_ShowHexNum(4,5,Serial_TxPacket[1],2);OLED_ShowHexNum(4,9,Serial_TxPacket[2],2);OLED_ShowHexNum(4,13,Serial_TxPacket[3],2);LED2_Turn();}if(Serial_GetITFlag() == 1){OLED_ShowHexNum(2,1,Serial_RxPacket[0],2);OLED_ShowHexNum(2,5,Serial_RxPacket[1],2);OLED_ShowHexNum(2,9,Serial_RxPacket[2],2);OLED_ShowHexNum(2,13,Serial_RxPacket[3],2);}}
}
Serial.h
#ifndef SERIAL_H
#define SERIAL_H#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h> //Variable parameters
#include <key.h>extern uint8_t Serial_TxPacket[4];
extern uint8_t Serial_RxPacket[4];void Serial_Init(void);
uint8_t Serial_GetITFlag(void);
void Serial_Printf(const char *format,...);
void Serial_SendArray(uint8_t* Array,uint16_t Length);
void Serial_SendByte(uint8_t Byte);
void Serial_SendNum(uint32_t Num, uint8_t Length);
void Serial_SendPacket(void);
void Serial_SendString(char *String);#endif
Serial.c
#include "Serial.h"uint8_t Serial_RxFlag = 0;
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];/*** @brief Initialize the USART peripheral* @param None* @retval None*/
void Serial_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIOGPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);USART_InitTypeDef USART_InitStruct;USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_Odd; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_9b; USART_Init(USART1,&USART_InitStruct);//串口中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;NVIC_Init(&NVIC_InitStruct);USART_Cmd(USART1,ENABLE);
}/*** @brief Send a byte via serial port* @param Byte: Data byte to be transmitted* @retval None*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1,Byte); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}/*** @brief Send data via serial port* @param Data parameters:* @arg Array: Data buffer to be transmitted* @arg Length: Number of bytes to transmit* @retval None*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{uint16_t i;for(i=0;i<Length;i++){Serial_SendByte(Array[i]); }
}/*** @brief Send a string via serial port* @param String: Pointer to the null-terminated string to transmit* @retval None*/
void Serial_SendString(char *String)
{while((*String) != '\0'){Serial_SendByte(*String);String++;}
}/*** @brief Calculate X raised to the power of Y (X^Y)* @param X: Base number (must be positive if Y=0)* @param Y: Exponent (must be non-negative)* @retval Result of X^Y*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1;while(Y--){Result *= X;}return Result;
}/*** @brief Send string via serial port* @param String: Pointer to null-terminated string* @retval None*/
void Serial_SendNum(uint32_t Num, uint8_t Length)
{uint8_t i;for(i=0;i<Length;i++){Serial_SendByte(Num / Serial_Pow(10,Length-i-1) % 10 + '0');}
}///**
// *@brief fputc
// *@param None
// *@retval None
//**/
//int fputc(int ch,FILE *f)
//{
// Serial_SendByte(ch);
// return ch;
//}/***@brief Brief description of the function or code*@param None*@retval None*@note parmN is format.The actual parameter passed to this parameter is the number of parameters represented bythe ellipsis part.
**/
void Serial_Printf(const char *format,...)
{char buffer[128];va_list args; //Declear an object that store parametersva_start(args, format); //Initialize args as parameters listvsnprintf(buffer, sizeof(buffer), format, args);va_end(args); //Clearing workSerial_SendString(buffer);
}/***@brief Deliver HEX data packet*@param None*@retval None
**/
void Serial_SendPacket(void)
{Serial_SendByte(0xFF);Serial_SendArray(Serial_TxPacket,4);Serial_SendByte(0xFE);
}/***@brief Detect the interrupt flag bit of the serial port.*@param None*@retval Where to enter the interrupt flag state
**/
uint8_t Serial_GetITFlag(void)
{if(Serial_RxFlag == 1){Serial_RxFlag = 0;return 1;}return 0;
}/***@brief Serial port interrupt*@param None*@retval None
**/
void USART1_IRQHandler(void)
{uint8_t Serial_RxData;static uint8_t Rx_status = 0;static uint8_t Rx_Count = 0;if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){Serial_RxData = USART_ReceiveData(USART1);switch(Rx_status){case 0:if(Serial_RxData == 0XFF){Serial_RxFlag = 0;Rx_status = 1;}break;case 1:Serial_RxPacket[Rx_Count] = Serial_RxData;if(++Rx_Count == 4){Rx_status = 2;Rx_Count = 0;}break;case 2:if(Serial_RxData == 0xFE){Serial_RxFlag = 1;Rx_status = 0;}break;default:break;}USART_ClearITPendingBit(USART1,USART_IT_RXNE); //读取DR寄存器会自动将USART_CR1寄存器中的RXNEIE位清除}
}
OLED部分代码参照文章《STM32基础教程——OLED显示》http://【STM32基础教程 ——OLED显示 - CSDN App】https://blog.csdn.net/2301_80319641/article/details/145837521?sharetype=blog&shareId=145837521&sharerefer=APP&sharesource=2301_80319641&sharefrom=link
技术要点
串口引脚
USART1的TX和RX引脚分别为PA9和PA10的复用功能引脚。
GPIO引脚配置
//GPIOGPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStruct);
PA9作为复用Tx引脚,应设置为复用推挽输出(GPIO引脚的行为取决于所选的复用功能,由特定的外设控制)。
PA10作为复用Rx引脚,设置为上拉输入(没有复用输入是因为引脚作为输入功能只被动的接收或采集信号而不产生驱动信号)(因为数据信号在空闲状态保持高电平,不能使用下拉输入,可选浮空输入。)
USART初始化
USART_InitTypeDef USART_InitStruct;USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_Odd; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_9b; USART_Init(USART1,&USART_InitStruct);
波特率的设置不需要自己计算,只需将想要设置的波特率赋值给USART_BaudRate成员,本实验设置波特率为9600,不使用硬件流控制,USART_Mode同时启用收发功能,数据帧采用奇校验,一个停止位,数据字节长度设置为9。
开启USART中断
//串口中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;NVIC_Init(&NVIC_InitStruct);
优先级分组选择第2组,中断通道选择USART中断通道。
发送1位数据
/*** @brief Send a byte via serial port* @param Byte: Data byte to be transmitted* @retval None*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1,Byte); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}
调用USART_SendData()函数发送一位数据,然后while循环等待数据发送完成后TXE标志位被置1,再次发送输入即向USART_DR寄存器写入数据时会自动将该标志位清0。
串口中断数据接收
/***@brief Serial port interrupt*@param None*@retval None
**/
void USART1_IRQHandler(void)
{uint8_t Serial_RxData;static uint8_t Rx_status = 0;static uint8_t Rx_Count = 0;if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){Serial_RxData = USART_ReceiveData(USART1);switch(Rx_status){case 0:if(Serial_RxData == 0XFF){Serial_RxFlag = 0;Rx_status = 1;}break;case 1:Serial_RxPacket[Rx_Count] = Serial_RxData;if(++Rx_Count == 4){Rx_status = 2;Rx_Count = 0;}break;case 2:if(Serial_RxData == 0xFE){Serial_RxFlag = 1;Rx_status = 0;}break;default:break;}USART_ClearITPendingBit(USART1,USART_IT_RXNE); //读取DR寄存器会自动将USART_CR1寄存器中的RXNEIE位清除}
}
每接收到一个数据就会触发一次串口中断,在中断里使用状态机的思想,初始状态先检测包头,当检测到包头0XFF后进入下一个状态,否则继续等待检测。在第二种状态中,使用长度固定的数据缓冲区接收数据,当接收到的数据达到固定长度后进入下一状态,否则继续接收。进入第三种状态后,检测包尾,如果检测到包尾0XFE,说明接收到了完整的数据,然后进入初始状态等待下一个包头,否则继续等待检测包尾。每进入一次中断执行一次该状态下的任务。
串口打印
/***@brief Brief description of the function or code*@param None*@retval None*@note parmN is format.The actual parameter passed to this parameter is the number of parameters represented bythe ellipsis part.
**/
void Serial_Printf(const char *format,...)
{char buffer[128];va_list args; //Declear an object that store parametersva_start(args, format); //Initialize args as parameters listvsnprintf(buffer, sizeof(buffer), format, args);va_end(args); //Clearing workSerial_SendString(buffer);
}
基于C库的printf函数的原理实现的串口打印函数。该函数定义了一个128个字节的缓冲区,va_start()初始化可变参数类型变量args,使其指向第一个可变参数(即format后的第一个参数),
vsnprintf()函数将格式化字符串写入缓冲区buffer,va_end函数完成清理args的工作,完成对可变参数列表的使用。
在C语言中,处理函数的可变参数(variadic arguments)需要用到标准库中的
<stdarg.h>
头文件。这个头文件提供了处理不定数量参数所需的功能和宏定义。下面详细解释可变参数类型以及如何使用指向这些参数的变量。可变参数类型
va_list
类型:
- 这是一个特殊的类型,用于声明一个变量,该变量将被用来遍历函数调用时传递的所有额外参数。
- 它本质上是一个指向参数列表的指针或类似结构体,但其具体实现是编译器相关的,通常对用户是不透明的。
宏定义:
va_start
:初始化一个va_list
类型的变量,使其指向第一个可变参数。需要传入两个参数:一个是va_list
类型的变量,另一个是指向固定参数列表中的最后一个已知参数的标识符(通常是省略号前的那个参数)。va_arg
:获取当前指针位置的参数值,并根据提供的类型向前移动指针到下一个参数。需要传入两个参数:一个是va_list
类型的变量,另一个是你想要获取的参数的数据类型。va_end
:清理工作,释放由va_start
初始化的资源。虽然在现代系统上这可能不是严格必要的,但它是一种良好的编程实践。作用
当你有一个函数接受不定数量的参数时(比如
printf
),你可以使用va_list
类型的变量来遍历所有额外参数。具体步骤如下:
- 首先,使用
va_start
初始化你的va_list
变量,告诉它从哪里开始查找额外参数。- 然后,使用
va_arg
来依次提取每个参数的值,并根据需要处理它们。- 最后,在完成所有参数处理之后,调用
va_end
来进行必要的清理。
发送数据和接收的数据的显示
Serial_TxPacket[0] = 0x1f;Serial_TxPacket[1] = 0x2f;Serial_TxPacket[2] = 0x3f;Serial_TxPacket[3] = 0x4f;OLED_ShowString(1,1,"RxData:");OLED_ShowString(3,1,"TxData:");while(1){Key_Num = Key_GetNum();if(Key_Num == 1){Serial_TxPacket[0]++;Serial_TxPacket[1]++;Serial_TxPacket[2]++;Serial_TxPacket[3]++;Serial_SendPacket();OLED_ShowHexNum(4,1,Serial_TxPacket[0],2);OLED_ShowHexNum(4,5,Serial_TxPacket[1],2);OLED_ShowHexNum(4,9,Serial_TxPacket[2],2);OLED_ShowHexNum(4,13,Serial_TxPacket[3],2);LED2_Turn();}if(Serial_GetITFlag() == 1){OLED_ShowHexNum(2,1,Serial_RxPacket[0],2);OLED_ShowHexNum(2,5,Serial_RxPacket[1],2);OLED_ShowHexNum(2,9,Serial_RxPacket[2],2);OLED_ShowHexNum(2,13,Serial_RxPacket[3],2);}}
先初始化发送数据缓冲区,然后在OLED的第一行和第三行分别显示接收数据和发送数据的标识。
检测按键是否按下,每按下一次按键,发送数据缓冲区的数据会自增,并且将数据显示在OLED的第四行,然后执行LED状态翻转。
当接收到的完整的数据包后中断数据接收标志会被置1,(该位在状态机的初始状态清0),这里检测该标志位,如果检测到该位被置1,则将接受到的数据显示在OLED上。
实验结果
使用串口助手发送数据,格式为:FF XX XX XX XX FE,OLED上就会显示收到的数据;
按下按键,LED状态翻转,OLED显示当前发送的数据,串口助手会显示收到的数据。
问题记录
1.对可变参数相关知识理解较浅。
2.USART波特率基本原理倍率采样理解反了。