当前位置: 首页 > 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.dtcms.com/a/331343.html

相关文章:

  • 从轨道根数计算惯性系到轨道系旋转矩阵
  • 2020/12 JLPT听力原文 问题二 1番
  • [激光原理与应用-268]:理论 - 几何光学 - 人眼结构与颜色感知
  • Nacos 配置热更新:Spring Boot Bean 自动获取最新配置
  • 【21-倾斜数据集的误差指标】
  • 金融风控实战:从数据到模型的信用评分系统构建全解析
  • 使用马尔可夫链如何解码、预测股市模式
  • 西门子PLC通过稳联技术EtherCAT转Profinet网关连接baumuller伺服器的配置案例
  • ThreadPoolExecutor 最佳实践
  • 8月AI面试工具测评:破解规模化招聘难题
  • 哈希表特性与unordered_map/unordered_set实现分析
  • 风电功率预测实战:从数据清洗到时空建模​​
  • 从单机到分布式:用飞算JavaAI构建可扩展的TCP多人聊天系统
  • 大规模分布式光伏并网后对电力系统的影响
  • 用SQL实现对DuckDB rusty_sheet插件批量测试
  • 前端-vue全局路由守卫的详情
  • 地测管理部绩效考核关键指标与地质数据分析
  • 如果未来出现了意识移植技术,如何确保移植后的意识是原本的意识而不是复制了一份
  • C++-setmap详解
  • 无人机图传模块——智能飞行的关键技术
  • 解锁AI潜能:五步写出让大模型神级指令
  • Cloudflare Tunnels穿透ssh
  • 51单片机-驱动LED模块教程
  • 【C#】Region、Exclude的用法
  • 无需公钥的无损加密解密
  • 深入详解C语言数组:承上启下——从C语言数组基础到数据结构衔接
  • 码上爬第八题【协程+ob混淆】
  • 【Java虚拟机】JVM相关面试题
  • 2025天府杯数学建模C题
  • 2025天府杯数学建模A题分析