目录
通信分类
常见的通信和通信接口
常见的通信:串口,485,iic, spi, can(汽车电子),wifi,蓝牙,4G,NB,lora
常见的通信接口: 单总线 串口 485 IIC SPI CAN
常见的通信分类
串行和并行区分
串行:数据只能 1bit 1bit 的发送/接收 --传输会慢
并行:数据可以支持多个 bit 同时发送/接收 --传输数据快
区分:几根数据线 发送或者接收数据只有一根线 那就是串行
单端和差分
单端:一个数据线的高低区别 0 1 --串口 UART
差分:两个线电压差区别 0 1 -- 485 通信,CAN 通信 抗干扰能力强
区分:一根线确定数据 0 还是 1 叫做单端,两根线电平差确定数据 0 还是 1 叫做差分
单工和双工
单工: -- 收音机
设备只能发送(接收),只能做一件事
双工:设备能够发送也能接收
半双工:发送和接收都可以进行,但不可以同时进行 -- 485
全双工:发送和接收不相互影响,可以同时进行 --串口,SPI
区分:只能发送或者接收,叫做单工;半双工,可以发送,也可以接收,但是不能同时发送和接收;全双工,可以发送也可以接收,并能够同时进行
同步和异步
同步: -- 有时钟线
设备在通信时,用的是同一个时钟,时钟节奏由主机操作
异步: -- 无时钟线
设备在通信时,没有时钟线
区分:有时钟线就是同步通信,没有时钟线就是异步
常见通信和分类
串口的讲解
串口在电子领域的作用
UART:调试 -- printf
短距离有线通信:两个设备通过串口进行通信
设备之间的无线通信:4G、LORA、NB、蓝牙等,单片机通过串口实现无线通信
串口下载是如何实现的
FlyMCU 下载 -- 电脑 – USB -- CH340 -- USART1 -- STM32
CH340: 实现 USB 电平转换成 TTL 电平
一键下载电路,可以实现自动调节 Boot0 的电平
串口的分类
串口的通信方式:串行全双工单端,UART 和 USART 都是串口
UART:有线串行单端全双工异步通信
USART:有线串行单端全双工同步通信
S 指的是时钟
但是实际应用中,一般都是使用 UART
串口的不同电平和调试
TTL 232 485 本质上都是串口,只是电平规范不一样
TTL 电平
TTL 电平: 单片机的串口引脚 都是 TTL 电平
如果电脑接收 TTL 电平数据,需要将 TTL 电平转换成 USB 电平,常见通过 CH340 实现。
232 电平
232 电平:TTL+232 芯片 转化成 232 电平
485 电平
485 电平:TTL+485 芯片 转化成 485 电平
注意
如果两个设备通信:必须电平一致
TTL 接口: TX RX GND
232 接口: TX RX GND
485 接口: A B
串口通信的物理层
T:发送 transmission
R:接收 Receive
交叉连接
串口通信的协议层
位协议:以 bit 为单位,每位都有信息
字协议:以 byte 为单位,每个字节代表的有信息
串口的数据协议:
起始位 数据位 校验位 停止位
校验:奇校验、偶校验、无校验
数据位中 1 的个数+奇偶校验位 1 的个数 -- 奇数/偶数
校验位 1 -- 奇校验郑 0 -- 偶校验
校验是为了接收方检测接收的数据是否准确
波特率:1s 发送多少个位 bits/s --传输快慢 双方设备 必须一致
串口重要的参数
波特率 数据位 停止位 校验方式
波特率一致,数据位的个数一致,校验方式一致,停止位个数一致
9600 波特率 8 个数据位 无校验 1 个停止位 (简写:9600 8 N 1)
串口传输数据所需时间的计算
已知:9600 波特率(bits/s) 8 个数据位 1 个停止位 无校验
每传输 1 个字节,起始位(1 位)+数据位(8 位)+停止位(1 位)=共 10 位
波特率=9600 波特率(bits/s),那么传输 1 位时间 t=1/9600(s/bit)
传输 1 个字节时间=传输 10 位时间=10*t=10*(1/9600)=1/960s=1ms
已知:115200 波特率(bits/s) 8 个数据位 1 个停止位 奇校验
每传输 1 个字节,起始位(1 位)+数据位(8 位)+停止位(1 位)+校验位(1 位)=共 11 位
波特率=115200 波特率(bits/s),那么传输 1 位时间 t=1/115200(s/bit)
传输 1 个字节时间=传输 11 位时间=11*t=11*(1/115200)
STM32 中串口的讲解
STM32 中串口的基本结构
STM32 中串口发送
STM32 中串口的接收
STM32 中串口波特率的设置
常见的波特率 9600 115200
UART1:72M
UART2--5:36M
STM32 中串口的中断
接收数据就绪可读:接收到数据了
检测到空闲线路:一帧数据接收完成了
事件标志:TC TXNE IDLE
STM32 中串口的个数和引脚
数据手册,引脚定义
一共五个串口
USART1_TX -- PA9
USART1_RX -- PA10
USART2_TX -- PA2
USART2_RX -- PA3
USART3_TX -- PB10
USART3_RX -- PB11
USART4_TX -- PC10
USART4_RX -- PC11
USART4_CK -- PC12
USART5_RX -- PD2
一般使用默认复用功能,如果使用的是重定义的功能,需要开启 AFIO 的时钟
串口初始化的配置
需要配置,单片机内部的串口外设需要借助 GPIO 口和外界通信.
代码
#include "UART1.h"
#include "main.h"
#include "stdio.h"
#include "led.h"
#include "relay.h"
#include "stdio.h"
#include "string.h"
uint8_t U1_R_Buff[100];//接收缓冲区
uint8_t U1_R_Length = 0;//接收到的长度
uint8_t U1_R_Idle = 0;//接收完成标志位,1完成,0未完成
void UART1_Config(void)
{
#if (USB_STD_LIB==0)
//1.开启GPIOA
RCC->APB2ENR |= (0x01<<2);
GPIOA->CRH &= ~(0xF << 4);//先清0
GPIOA->CRH |= (0xB << 4);//在配置模式1011
GPIOA->CRH &= ~(0xF << 8);//先清0
GPIOA->CRH |= (0x4 << 8);//在置0100
//开串口1的时钟
RCC->APB2ENR |= (0x01<<14);
USART1->CR1 |= (0x1 << 13);
USART1->CR1 &= ~(0x1 << 12);
USART1->CR2 &= ~(0x3 << 12);
/*USARTDIV = fck / (波特率 * 16)
UART1接APB2总线72M,所以fck=72M
UART2--UART5接APB1总线36M,所以fck=36M
假如想要9600波特率
USARTDIV = fck / (波特率 * 16)=72000000/(9600*16)=468.75
小数部分=0.75*16=12=0x0C
整数部分=468=0x1D4
整数和小数拼接,写入寄存器的结果=0x1D4C
*/
USART1->BRR = 0x1D4C;
//发送使能TE
USART1->CR1 |= (0x01<<3);
//接收使能RE
USART1->CR1 |= (0x01<<2);
//开启接收中断RXNEIE
USART1->CR1 |= (0x01<<5);
//开启空闲中断IDLEIE
USART1->CR1 |= (0x01<<4);
NVIC_SetPriority(USART1_IRQn, 9);//抢占3次级0
//允许NVIC层面的中断
NVIC_EnableIRQ(USART1_IRQn);
#elif (USB_STD_LIB==1)
//1.开A端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.定义结构体 xxx需要传递结构体地址,PA9 PA10
GPIO_InitTypeDef GPIO_InitStruct = {0};//3.给结构体赋值
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;//代配置引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;//引脚速率
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;//代配置引脚
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStruct);//4.调用xxx_Init函数,将参数写入寄存器中
//3.开USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//4.配置USART
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;//波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制 ---- 自动发送和接收数据
USART_InitStructure.USART_Mode = USART_Mode_Rx | 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);
// 5. 启动USART1
USART_Cmd(USART1, ENABLE);
// 6. 配置USART中断,开启中断必须写中断服务函数,下方开启了俩中断,在中断中必须处理这两个
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//接收中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//空闲中断
// 7. 配置NVIC中断
NVIC_InitTypeDef NVIC_InitStructure = {0};
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//次级优先级
NVIC_Init(&NVIC_InitStructure);
#endif
}
//单字节发送
/*TC和TXE的区别
TXE == 1 表示发送数据寄存器空,数据移位寄存器不确定
TC == 1 表示发送数据寄存器空,数据移位寄存器空
*/
/*
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
*/
void UART1_SendByte(uint8_t data)
{
#if (USB_STD_LIB==0)
while((USART1->SR & (0x01 << 6)) == 0)//0上次没发完,1发送完成
{}
USART1->DR = data;//发送完成,发送新数据
#elif (USB_STD_LIB==1)
//非中断中使用不挂IT的函数
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)//RESET上次没发完,SET发送完成
{}
USART_SendData(USART1, data);//发送完成,发送新数据
// (1)软件序列清除该位(先读USART_SR,然后写入USART_DR)。 (2)写0清0
// USART_ClearFlag(USART1,USART_FLAG_TC);
#endif
}
//数组发送
void UART1_SendBuff(uint8_t *Buff, uint16_t Length)
{
for(uint16_t i = 0; i < Length; i++)
{
UART1_SendByte(*Buff++);
}
}
//字符串发送
void UART1_SendStr(uint8_t *Str)
{
while(*Str != '\0')
{
UART1_SendByte(*Str++);
}
}
/*
实现printf,fputc函数的重写
1.必须串口初始化
2.必须勾选魔法棒 Use MicroLIB
3.换行使用\r\n
4.printf只能和某一个串口一块使用,一般是调试口
*/
int fputc(int ch, FILE *f)
{
UART1_SendByte(ch);
return ch;
}
//阻塞接收,不断检测,很少使用
uint8_t UART1_RecByte(void)
{
#if (USB_STD_LIB==0)
uint8_t temp = 0;
while((USART1->SR & (0x01 << 5)) == 0)//检测RXNE
{//0未接收到
}
//1接收到
temp = USART1->DR;
return temp;
#elif (USB_STD_LIB==1)
uint8_t temp = 0;
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET)//检测RXNE
{//RESET未接收到
}
//1接收到
temp = USART_ReceiveData(USART1);
// USART_GetFlagStatus(USART1,USART_FLAG_RXNE);
return temp;
#endif
}
/*
进入中断接收一个数据
如果对方连续发送5个字节的数据,会触发6次中断,其中5次接收中断,1次空闲中断
*/
/*
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
放断点时,接收中断中一班不放断点,如果放了会导致后面的中断接收不到。
如果可以放断点运行进去,证明接收中断没问题。
一般可以再空闲中断放断点,可以查看watch窗口查看接收到的内容对不对。
*/
void USART1_IRQHandler(void)
{
uint8_t data = 0;
// 接收中断,RXNE
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
data = USART_ReceiveData(USART1); // 读取数据
U1_R_Buff[U1_R_Length++] = data; // 存入缓冲区
}
// 空闲中断(数据接收完毕)
if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
{
data = USART_ReceiveData(USART1); // 清除IDLE标志
U1_R_Idle = 1; // 标记接收完成
// 这里不需要清除中断标志,因为空闲中断不能通过`USART_ClearITPendingBit()`清除
}
}
/*
通过串口助手发送下面4个字节十六进制数据,按照要求,单片机执行对应的内容
5A 02 功能码 和校验
总共 4 个字节
5A 帧头
02 剩余长度
功能码 1 个字节 01 开灯 02 关灯 03 开蜂鸣器 04 关蜂鸣器
和校验 1 个字节 前面所有字节(不包括自己)的字节和
5A 02 01 5D 开灯
5A 02 02 5E 关灯
5A 02 03 5F 开蜂鸣器
5A 02 04 60 关蜂鸣器
*/
//处理函数
void USART1_Handler(void)
{
uint8_t data_cs = 0;
if(U1_R_Idle == 1)
{//1数据接收完成
U1_R_Idle = 0;//空闲标志位清0
//处理数据内容,处理完成后清除接收缓冲区
data_cs = U1_R_Buff[0] + U1_R_Buff[1] + U1_R_Buff[2]; // 校验和计算
// 校验接收到的校验字节
if (data_cs == U1_R_Buff[3])
{
if(U1_R_Buff[0] == 0x5A && U1_R_Buff[1] == 0x02)//校验帧头和剩余帧
{
uint8_t function_code = U1_R_Buff[2];
switch (function_code)
{
case 0x01: // 开灯
LED1_ON();LED2_ON();LED3_ON();LED4_ON();
printf("Received Data: ");
for (int i = 0; i < U1_R_Length; i++)
{
printf("%02X ", U1_R_Buff[i]);
}
printf("\r\nChecksum: %02X, Received checksum: %02X\r\n", data_cs, U1_R_Buff[3]);
break;
case 0x02: // 关灯
LED1_OFF();LED2_OFF();LED3_OFF();LED4_OFF();
printf("Received Data: ");
for (int i = 0; i < U1_R_Length; i++)
{
printf("%02X ", U1_R_Buff[i]);
}
printf("\r\nChecksum: %02X, Received checksum: %02X\r\n", data_cs, U1_R_Buff[3]);
break;
case 0x03: // 开继电器
Relay_ON();
break;
case 0x04: // 关继电器
Relay_OFF();
break;
default:
// 无效的功能码
break;
}
}
else
{
printf("帧头和剩余长度有错误!!!\n");
}
}
else
{
printf("校验不通过!!!\n");
}
memset(U1_R_Buff, 0, sizeof(U1_R_Buff));
U1_R_Length = 0;
}
//不能放在这里,因为数据还没接收完就被清除了
//memset(U1_R_Buff, 0, sizeof(U1_R_Buff));
//U1_R_Length = 0;
}