当前位置: 首页 > news >正文

STM32学习笔记11-通信协议-串口基本发送与接收

通信接口

串口USART

  • 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

USART:TX:数据发送脚;RX:数据接受脚

I2C:SCL是时钟;SDA是数据

SPI:SCLK是时钟、MOSI是主机输出数据脚、MISO主机输入数据脚、CS片选,用于指定通信的对象

CAN:CAN_H、CAN_L——差分数据脚,用两个引脚表示差分数据

USB:DP(D+)、DM(D-)——一对差分数据脚


时钟:需要一个时钟信号来告诉接收方,什么进行采样——同步:I2C和CAN:有单独的时钟线,所以可以在时钟信号的指引进行采样;——异步:由于没有时钟线,所以需要双方约定一个时间,并且加一些帧头帧尾等进行采样位置的对齐。

电平:——单端:引脚的高低电平都是对GND的电压差,所以单端通信必须要共地;——差分:靠两个差分引脚的电压差来传输信号,抗干扰强。

串口的通信协议——STM32内部的USART外设


串口通信

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信(点对点)
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力

硬件电路

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片,直接从控制器出来的信号,一般是TLL电平(也就是5V或者3.3V表示1,0V表示0)

TX与RX是单端的,所以相对于GND的,因此严格来说,GND也算是通信线;

VCC用于供电

电平标准
  • 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
  • TTL电平:+3.3V或+5V表示1,0V表示0
  • RS232电平:-3~-15V表示1,+3~+15V表示0——一般的大机器上
  • RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)——通讯距离可上千米

软件部分

串口参数及时序

  • 波特率:串口通信的速率(码元/s:二进制中一个码元就是一个bit,此时波特率就等于比特率)——串口一般是异步,所以需要双方约定的速率——它决定了每隔多少秒发送一位
  • 起始位:标志一个数据帧的开始,固定为低电平;
  • 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
  • 校验位:用于数据验证,根据数据位计算得来:无校验、奇校验(保证数据的二进制的1的个数是奇数,会在校验位上补一位,保证1的个数为奇数,接收方会验证数据位和校验位的1个数是否为奇数)和偶校验(同理)
  • 停止位:用于数据帧间隔,固定为高电平

串口时序

把示波器的GND接在负极,另一个探头接在发送设备的TX引脚

第一个波形:发送一个字节数据0x55,在TX引脚输出的波形,波特率是9600,所以每一位的时间是1/9600,大概是104us

中间的高低电平变化是USART自动进行的

USART简介


  • USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步(不常用的,只是输出时钟,而不能输入时钟,更多是去兼容其他的协议)/异步收发器
  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
  • 自带波特率发生器(本质上是分频器,通过分频,达到我们想要的波特率时钟),最高达4.5Mbits/s
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
  • 可选校验位(无校验/奇校验/偶校验)
  • 支持同步模式(多个CLK的时钟输出)、硬件流控制(防止接受方处理慢而导致数据丢失的问题)、DMA(数据转运)、智能卡、IrDA、LIN(局域网的通信协议)
  • STM32F103C8T6 USART资源: USART1(APB2总线上的设备)、 USART2、 USART3

USART框图

TDR是只写的,RDR是只读的

发送寄存器:把一个字节的数据一位一位地移出去,正好对应串口协议的波形的数据位;

当数据从TDR处移到发送寄存器中来时,有个标志位TXE(发送寄存器空)置1,当TXE置1时,就可以继续放新的数据等待原数据被处理,但此时原数据并没有被处理。原数据,会受到发送控制器,向右移位(可以串口协议定义的低位先行),一位一位地把数据输出到TX引脚。

接收端:数据从RX引脚通向接收移位寄存器,通过接收控制器的驱动下,将一位一位地读取RX电平,先放在最高位,然后向右移;读取完后会通向RDR,在过程中有个标志位RXNE(接受数据寄存器非空),当RXNE为1时,就可以把数据读走了

在发送过程中,需要补帧头帧尾,在接受中,需要解帧头帧尾——这些操作都可以让硬件配置来完成;

控制部分和其他的增强功能:

硬件数据流控:

两个引脚前面加n的意思是低电平有效,nRTS:请求发送,是输出脚;nCTS:清除发送,是输入脚

当对方的设备支持流控,我们就需要把RTS连到对面的CTS上,再把对面的TX接到自己的RX上,当我们准备好了,则向RTS输出一个低电平到对方CTS中,对方就通过TX不断地输入数据;如果不行了,往RTS输出一个高电压,对方就会停止。

引脚定义:

USART基本结构

在软件层面只有一个DR寄存器可供我们读写,写入DR,数据走上面的路,进行发送

读取DR,数据走下面,进行接收

其他:

数据帧

1.

时钟在上升沿的时候,进行采样,这样得到数据是最平稳的

2.不同停止位的变化

USART电路输入数据的策略:

起始位侦测

策略:最开始,空闲状态高电平,采样一直是1,在某个位置突然采到0,说明出现下降沿,如果没有任何噪声,则是起始位,在起始位会进行连续16次采样,为了防止噪声,所以会在下降沿之后的第3次、5次、7次、8次、9次、10进行采样,且每次采样都要求每3位里面至少应有2个0,如果全0 ,则无噪声,如果2个0,1个1的情况,则检测到了一个起始位并向NC(噪声标志位)记录置1,如果不满足,就不算检测到起始位,此时电路就忽略前面的数据,重新开始捕获下降沿,之后就继续在8,9,10次中进行采样,因为正好是频率的正中间。

数据采样

波特率发生器

  • 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
  • 计算公式:波特率 = fPCLK2/1 / (16 * DIV)

多除个16,是因为在采样内部有一个16倍的采样时钟

USB转串口模块的原理图

 

接线图

9-1 串口发送


  1. 开启时钟,把需要用的USART和GPIO的时钟打开
  2. GPIO初始化,把TX配置成复用输出,RX配置成输入
  3. 配置USART,使用结构体参数
  4. 如果只需发送,直接开启USRAT,初始化结束;如果需要接受的功能,需要配置中断,则需要在开启USRAT之前,加上ITConfig和NVIC的代码
  5. 适当的调用发送函数和接收函数,若需要发送和接收的状态,调用获取标志位的函数即可

相关函数:

void USART_DeInit(USART_TypeDef* USARTx);

void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);

void USART_StructInit(USART_InitTypeDef* USART_InitStruct);

//两个韩素华配置同步时钟输出的

void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);

void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);

void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);

void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);

void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);  //开启USART到DMA的触发通道

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data); //发送数据,写DR寄存器

uint16_t USART_ReceiveData(USART_TypeDef* USARTx);  //接收数据,读DR寄存器

//标志位

FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);

void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);

ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);

void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);

数据模式

  • HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
  • 文本模式/字符模式:以原始数据编码后的形式显示

ASCII码表:

字符和数据在发送和接收的关系:

main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
//#include "OLED_Font.h"
//串口发送
int main(void){OLED_Init();Serial_Init();
//	Serial_Send(0x41);//uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};	//定义数组//Serial_SendArray(MyArray, 4);//Serial_SendString("HelloWorld!\r\n");//  '/r':回到当前行的首行,继续输出原数据会被覆盖;'\n':回到当前位置的下一行位置//Serial_SendNumber(12345,5);//printf移植,使用MicroLIB库//printf移植1:单数据输出//printf("Num=%d\r\n",666);//printf移植2:多数据输出//用sprintf:格式化字符输出到一个字符串里,并且可以设置打印位置,所以不涉及重定向
//	char string[100];
//	sprintf(string,"Num=%d\r\n",666);
//	Serial_SendString(string);//把字符串通过串口发送出去//printf移植3:封装printf:目的是输出一个可变的参数——<stdarg.h>//Serial_Printf("Num=%d\r\n",666);//显示汉字的方法——默认的编译器是UTF-8,前提:要去配置c/c++中添加预处理--no-multibyte-charsSerial_Printf("你好,世界!");////用GBK的方式while(1){}
}Serial.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include <stdio.h>
#include <stdarg.h>
void Serial_Init(void){//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//初始化GPIO引脚GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//这个只用TX,作为输入模式,此实验只需发送GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化USARTUSART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600;//波特率USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//硬件流控制USART_InitStructure.USART_Mode=USART_Mode_Tx;//选择模式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);//启动USARTUSART_Cmd(USART1,ENABLE);}//发送数据的函数
void Serial_SendByte(uint8_t Byte){USART_SendData(USART1, Byte);//此时发送的数据会存到ADC->DR中,之后再从DR转到发送寄存器中,并且需要等待发送完,不然会对原数据直接覆盖//获取USART的TXE标志位,直到为SER就载入新的数据,又由手册得知TXE当对DR写操作时,会被清零while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待DR寄存器空while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);   // 等待发送完成}
//发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++)		//遍历数组{Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据}
}
//发送字符串
void Serial_SendString(char *String)
{for(uint8_t i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);Delay_ms(10);}
}
//发送数组,转换为字符串输出
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1;	//设置结果初值为1while (Y --)			//执行Y次{Result *= X;		//将X累乘到结果}return Result;
}/*** 函    数:串口发送数字* 参    数:Number 要发送的数字,范围:0~4294967295* 参    数:Length 要发送数字的长度,范围:0~10* 返 回 值:无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{for (uint8_t i = 0; i < Length; i ++)		//根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - 1 - i) % 10 + '0');	//依次调用Serial_SendByte发送每位数字}
}
//原本printf是输出到屏幕的,我们这个是串口,所以需要使用函数进行重定向
//使用fputc函数——因为C语言里,依赖于fputc写的printf函数将输出到屏幕或终端,此时进行配置就可以修改fputc,从而修改printf的输出位置
int fputc(int ch,FILE *f){Serial_SendByte(ch);return ch;}//printf封装——sprintf进行封装,char *format:接收格式化的字符串;...:用来接收后面的可变参数列表
void Serial_Printf(char *format,...){char string[100];va_list arg;//定义一个参数列表变量va_start(arg,format);//从format位置开始接收参数表,放在arg里面vsprintf(string,format,arg);//sprintf只能输出固定的参数,vsprintf用于封装格式va_end(arg);//释放列表Serial_SendString(string);
}

9-2 串口发送+接收

接线图同上

main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData;int main(void) {OLED_Init();    // 初始化OLEDSerial_Init();  // 初始化串口while (1) {//查询操作
//        if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET) {
//            RxData = USART_ReceiveData(USART1);
//            OLED_ShowHexNum(1, 1, RxData, 2);  // 显示16进制
//        }if (Serial_GetSerial_Flag() == 1) {RxData = Serial_GetSerial_Rxdata();//回传给电脑Serial_SendByte(RxData);OLED_ShowHexNum(1, 1, RxData, 2);  // 显示16进制}}
}Serial.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_Rxdata,Serial_Flag;
void Serial_Init(void){//开启时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//初始化GPIO引脚GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//这个只用TX,作为输入模式,此实验只需发送GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//初始化RX对应的引脚PA10GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化USARTUSART_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_ITConfig(USART1, USART_IT_RXNE, ENABLE);//配置NVIC//分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//初始化NVICNVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);//启动以后,会调用对应的启动函数(固定的)——USART1_IRQHandler//启动USARTUSART_Cmd(USART1,ENABLE);//对于串口接收:1.可以使用查询——初始化结束;2.可以使用中断——开启中断,配置NVIC//查询:在主函数中不断判断RXNE标志位,若置1,就说明收到数据了,再调用ReceiveData,读取DR寄存器,即可}//实现读后清出标志位
uint8_t Serial_GetSerial_Flag(void){if(Serial_Flag==1){Serial_Flag=0;return 1;}return 0;
}uint8_t Serial_GetSerial_Rxdata(void){return Serial_Rxdata;
}
//中断函数
void USART1_IRQHandler(void){if(USART_GetFlagStatus(USART1, USART_IT_RXNE) == SET){//读取模块的变量Serial_Rxdata=USART_ReceiveData(USART1);Serial_Flag=1;//定期清理标志位USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}//发送数据的函数
void Serial_SendByte(uint8_t Byte){USART_SendData(USART1, Byte);//此时发送的数据会存到ADC->DR中,之后再从DR转到发送寄存器中,并且需要等待发送完,不然会对原数据直接覆盖//获取USART的TXE标志位,直到为SER就载入新的数据,又由手册得知TXE当对DR写操作时,会被清零while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待DR寄存器空while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);   // 等待发送完成}
//发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++)		//遍历数组{Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据}
}
//发送字符串
void Serial_SendString(char *String)
{for(uint8_t i=0;String[i]!='\0';i++){Serial_SendByte(String[i]);Delay_ms(10);}
}
//发送数组,转换为字符串输出
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1;	//设置结果初值为1while (Y --)			//执行Y次{Result *= X;		//将X累乘到结果}return Result;
}/*** 函    数:串口发送数字* 参    数:Number 要发送的数字,范围:0~4294967295* 参    数:Length 要发送数字的长度,范围:0~10* 返 回 值:无*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{for (uint8_t i = 0; i < Length; i ++)		//根据数字长度遍历数字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - 1 - i) % 10 + '0');	//依次调用Serial_SendByte发送每位数字}
}
//原本printf是输出到屏幕的,我们这个是串口,所以需要使用函数进行重定向
//使用fputc函数——因为C语言里,依赖于fputc写的printf函数将输出到屏幕或终端,此时进行配置就可以修改fputc,从而修改printf的输出位置
int fputc(int ch,FILE *f){Serial_SendByte(ch);return ch;}//printf封装——sprintf进行封装,char *format:接收格式化的字符串;...:用来接收后面的可变参数列表
void Serial_Printf(char *format,...){char string[100];va_list arg;//定义一个参数列表变量va_start(arg,format);//从format位置开始接收参数表,放在arg里面vsprintf(string,format,arg);//sprintf只能输出固定的参数,vsprintf用于封装格式va_end(arg);//释放列表Serial_SendString(string);
}

http://www.lryc.cn/news/621066.html

相关文章:

  • Autoppt-AI驱动的演示文稿生成工具
  • pygame的帧处理中,涉及键盘的有`pg.event.get()`与`pg.key.get_pressed()` ,二者有什么区别与联系?
  • ModuleNotFoundError: No module named ‘vllm._C‘
  • 界面设计风格解析 | ABB 3D社交媒体视觉效果设计
  • 3ds MAX文件/贴图名称乱码?6大根源及解决方案
  • tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
  • Java 技术栈中间件优雅停机方案设计与实现全景图
  • FreeSWITCH 对接阿里云流式 TTS:让大模型通话秒级响应
  • Elasticsearch ABAC 配置:基于患者数据的动态访问控制
  • 功能菜:吃对比吃饱更实在的健康菜
  • 企业智脑正在构建企业第二大脑,四大场景引擎驱动数字化转型新范式
  • 资本的自我否定:四重矛盾中的历史辩证法
  • 【科研绘图系列】R语言绘制蝶形条形图蝶形柱状堆积图
  • nginx-集成prometheus监控(k8s)
  • 高并发内存池 性能瓶颈分析与基数树优化(9)
  • anaconda创建pytorch1.10.0和pytorch2.0.0的GPU环境
  • lesson38:MySQL数据库核心操作详解:从基础查询到高级应用
  • app-4 日志上传
  • 第一章 java基础
  • 在IAR Embedded Workbench for Arm中实现NXP S32K3安全调试
  • Wireshark中捕获的大量UDP数据
  • 一次 Unity ↔ Android 基于 RSA‑OAEP 的互通踩坑记
  • 【题解】P1000 超级玛丽游戏 题解
  • 2025中国快递物流智能装备产业发展论坛将于9月3日上海举办
  • 如何选择图表库|2025 年实现强大数据可视化的 6 个 JavaScript 图表库对比
  • 二进制与进制转换
  • SpringBoot+Vue线上部署MySQL问题解决
  • WinForm之自定义布局(了解)
  • Centos9傻瓜式linux部署CRMEB 开源商城系统(PHP)
  • C++ 仿RabbitMQ实现消息队列项目