文章目录
STM32通信接口
通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
STM32通信接口:
名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
---|---|---|---|---|---|
USART | TX、RX | 全双工 | 异步 | 单端 | 点对点 |
I2C | SCL、SDA | 半双工 | 同步 | 单端 | 多设备 |
SPI | SCLK、MOSI、MISO、CS | 全双工 | 同步 | 单端 | 多设备 |
CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 |
USB | DP(D+)、DM(D-) | 半双工 | 异步 | 差分 | 点对点 |
串口通信
串口的基本概念
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢
- 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信(因为一般串口都采用点对点的通信协议,所以是实现两个设备之间的通信)
- 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力
串口设备示例:
串口硬件电路
-
简单双向串口通信有两根通信线(发送端TX和接收端RX)
-
TX与RX要交叉连接
-
当只需单向的数据传输时,可以只接一根通信线
-
当电平标准不一致时,需要加电平转换芯片
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
-
TTL电平(最常用):+3.3V 或 +5V 表示 1,0V 表示 0
-
RS232电平:-3 ~ -15V表示1,+3 ~+15V表示0(一般在大型机器上使用)
-
RS485电平:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0(差分信号,抗干扰能力强,通讯距离长)
在硬件电路上,协议规定,一个设备使用 TX 发送高低电平,另一个设备使用RX 接收高低电平
-
串口参数及时序
串口数据帧的基本结构
串口中每一个字节都装载在一个数据帧里面,每个数据帧都由起始位、数据位(奇偶校验位)和停止位组成
串口参数
-
波特率:串口通信的速率(因为串口是异步通信,所以需要通信双方约定一个通信频率)
-
起始位:标志一个数据帧的开始,固定为低电平
-
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行(数据从低位开始发送)
-
校验位:用于数据验证,根据数据位计算得来(串口使用奇偶校验的方法)
- 无校验:如上图 串口数据帧的基本结构 图中左边结构所示,没有校验位
- 奇校验:发送方在发送数据后会补一个校验位,保证 1 的个数为奇数,接收方在接收数据后会验证数据位和校验位
- 偶校验:保证 1 的个数为偶数
奇偶校验的检出率不高,如果两位同时出错就不能检验出来
-
停止位:用于数据帧间隔,固定为高电平(停止位也是为下一个起始位做准备)
通过以上介绍,应该就清楚数据帧是如何使用 1 和 0 来组成我们想要发送的一个字节数据的
USART介绍
USART基本概念
-
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
-
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
可以理解为:USART外设就是串口通信的硬件支持电路,USART大体可分为发送和接收两部分,发送部分就是将数据寄存器的一个字节数据自动转换成协议规定的波形,从 TX 引脚发出去,接收部分就是自动接收RX引脚的波形,按照协议规定解码为一个字节的数据存放在数据寄存器里
-
自带波特率发生器,最高达4.5Mbits/s
波特率发生器就是用于配置波特率的,可以视为一个分频器
-
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
-
可选校验位(无校验/奇校验/偶校验)
-
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
STM32F103C8T6 USART资源: USART1(APB2)、 USART2(APB1)、 USART3(APB1)
USART框图
USART简化结构图
USART常用库函数
/*初始化相关函数*/
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);
串口发送
接线图
执行程序之前需要先确保串口驱动成功
Strial模块
Strial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
void Serial_Init(void){
/*第一步:开启时钟,把需要用的USART和GPIO的时钟打开*/
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;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
/*配置USART,直接使用一个结构体*/
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600; //波特率的数值可以直接写,USART_Init函数内部会自动计算好对应的分频系数
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);
/*开启USART*/
USART_Cmd(USART1, ENABLE);
}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART1,Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
Strial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
#endif
【补充】
串口常用的模块函数
-
发送数组函数
void Serial_SendArray(uint8_t* Array, uint16_t Length){ uint16_t i; for(i = 0; i < Length; i++){ Serial_SendByte(Array[i]); } }
-
发送字符串函数
void Serial_SendString(char* String){//由于字符串自带一个结束标志位,所以不需要传递长度参数 uint8_t i; for(i = 0; String[i] != '\0'; i++){ Serial_SendByte(String[i]); } }
-
发送字符串形式的数字
uint32_t Serial_Pow(int32_t X, uint32_t Y){ //函数返回值为X的Y次方 int32_t Result = 1; while(Y--){ Result *= X; } return Result; } void Serial_SendNumber(uint32_t Number, uint8_t Length){ uint8_t i; for(i = 0; i < Length; i++){ Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); } }
printf( ) 函数的移植
使用 printf 之前打开工程选项,将 Use MicroLIB 勾选上
MicroLIB 是 Keil 为嵌入式平台优化的一个精简库
方法一
对 printf 进行重定向
因为 printf 函数默认输出到屏幕,但是单片机没有屏幕,所以要进行重定向,将 printf 输出的东西输出到串口
-
Step1:在串口模块(Strial.c 和 Strial.h)中加上
#include <stdio.h>
头文件 -
Step2:在串口模块(Strial.c)中重写
fputc
函数int fputc(int ch, FILE *f){ Serial_SendByte(ch); return ch; }
fputc 函数是 printf 函数的底层, printf 函数在打印的时候就是不断调用 fputc 函数一个个打印的
这种 printf 移植的缺点是 printf 只能有一个,如果 printf 被重定向到串口1了,那么串口2就不能使用了
方法二
想要多个串口都可以使用 printf 函数,可以使用 sprintf
sprintf 可以把格式化字符输出到一个字符串里
-
Step1:直接在 main.c 函数中定义一个字符串(长度给够)
-
Step2:调用 sprintf 和 Serial_SendString 函数实现 printf 函数移植
char String[100]; sprintf(String, "Num = %d\r\n", 666); Serial_SendString(String);
sprintf 函数第一个参数是指定打印的位置,利用 sprintf 将要打印的内容传送到指定位置后再利用 Serial_SendString(char* String) 函数将要打印的内容通过串口发送出去
因为 sprintf 可以指定打印位置,不涉及重定向的东西,所以每个串口都可以使用 sprintf 进行格式化打印
main.c 源程序
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int main(void){
OLED_Init();
Serial_Init();
Serial_SendByte(0x41);
//uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
//Serial_SendArray(MyArray, 4);
//Serial_SendString("HelloWorld!");
//Serial_SendNumber(12345, 5);
//printf("Num = %d\r\n",666);
//char String[100];
//sprintf(String, "Num = %d\r\n", 666);
//Serial_SendString(String);
while(1){
}
}
串口发送+接收
接线图
和串口发送接线图一样
Strial模块
strial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void Serial_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
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);
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);
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_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
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);
}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART1,Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
uint8_t Serial_GetRxData(void){
return Serial_RxData;
}
uint8_t Serial_GetRxFlag(void){
if(Serial_RxFlag == 1){
Serial_RxFlag = 0;
return 1;
}else{
return 0;
}
}
void USART1_IRQHandler(void){
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET){
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
serial.h
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
uint8_t Serial_GetRxFlag(void);
uint8_t Serial_GetRxData(void);
#endif
main.c 源程序
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void){
OLED_Init();
OLED_ShowString(1, 1, "RaData:");
Serial_Init();
while(1){
if(Serial_GetRxFlag() == 1){
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1, 8, RxData, 2);
}
}
}
STM32 专栏文章均参考 《STM32入门教程-2023版 细致讲解 中文字幕》教程视频