本文的大部分内容来自B站up主 江协科技, 此文只供本人学习记录用途, 侵删
一、通信协议
- 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
- 双工: 全双工表示能同收同发, 半双工表示同一时刻只能收或者发, 单工代表只能收或者发,(固定方向)
- 时钟: 同步就是有根时钟线控制发送和接收的时机, 异步就是双方约定好收发的时机(如波特率), 无时钟
- 电平: 单端需要共地才能接收信号, 差分通过差分引脚的电压差来传输信号
二、UART通信协议
电路: 典中典, 共电源, 共地, TX接RX, RX接TX
时序:
- 波特率:串口通信的速率
- 起始位:标志一个数据帧的开始,固定为低电平
- 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
- 校验位:用于数据验证,根据数据位计算得来(奇校验和偶校验)
- 停止位:用于数据帧间隔,固定为高电平
简单说下奇校验和偶校验, 奇偶校验的本质就是为了确保数据中1的个数为奇数或者偶数, 在校验位补1或者0,例如下面这个偶校验时序,发送0x55也就是:
1 0 1 0 1 0 1 0
发现有4个1 也就是偶数, 于是校验位补0,使得时序中的1的个数是偶数
如果此时是奇检验, 就会校验位补1,使得时序中的1的个数是奇数
三、UASRT外设
- USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
- 其实同步模式就是多了个时钟, 用的很少, 本章不涉及
- USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
- 自带波特率发生器,最高达4.5Mbits/s
- 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)可选校验位(无校验/奇校验/偶校验)
- 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
- STM32F103C8T6 USART资源: USART1、 USART2、 USART3
工作流程:
(TDR和RDR在物理上是两个寄存器,但在软件上是同一个DR)
发送时:
把数据写入DR, 如果发送移位寄存器没有数据在移位, 就会把DR的数据放到发送移位寄存器中, 准备发送,此时将TXE置1, 此时我们就可以写入下个数据了, 注意此时数据还没发送完毕, 只不过是从TDR放到了发送移位寄存器, 然后在发送控制器的驱动下, 一位一位的往外发
接收时:
数据进来, 在接收控制器的驱动下, 数据一位一位进入接收移位寄存器, 当接收移位寄存器接收了8个位, 这一个字节的数据会一下子被放进RDR中, 此时将RXNE置1,我们就可以读取数据了
发送和接收剔除帧头帧尾这个工作已经被硬件完成, 我们只需要配置好, 然后关注数据就好了,(51震怒)
四、常用配置代码
为了使用printf的库函数stdio, 在魔术棒中勾选 Use MicroLIB
使用微库,将以更精简短小的C 库替代标准C 库,减小代码大小。MicroLib 是默认C 库的备选库。它主要用于内存有限的嵌入式应用程序中。这些应用程序不在操作系统中运行。
用中断接收的配置代码:
//C文件
#include "stm32f10x.h" // Device header
#include "usart.h"
void UART_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); //PA9(USART1_TX)复位推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //PA10(USART1_RX)浮空输入
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_BaudRate=9600; 波特率9600
USART_HardwareFlowControl_None 硬件流控制失能
USART_Mode=USART_Mode_Tx |USART_Mode_Rx; 发送使能,接收使能
USART_Parity=USART_Parity_No; 校验失能(无校验)
USART_StopBits=USART_StopBits_1; 一位停止位
USART_WordLength=USART_WordLength_8b; 字长8Bit
*/
USART_Init(USART1,&USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//USART中断配置
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);//NVIC中断
USART_Cmd(USART1, ENABLE);//使能USART
}
//发送字节
void UART_SendByte(uint8_t data){
USART_SendData(USART1,data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);//等待TXE置1
}
//发送数组
void UART_SendArray(uint8_t* Arr,uint8_t Length){
for(uint8_t i=0;i<Length;i++)
UART_SendByte(Arr[i]);
}
//发送字符串
void UART_SendString(uint8_t* Str){
while(*Str){
UART_SendByte(*Str);
Str++;
}
}
//平方运算
uint32_t UART_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)Result *= X;
return Result;
}
//发送数字
void UART_SendNum(uint32_t Num,uint8_t Length){
uint8_t temp;
for(int8_t i=(int8_t)Length-1;i>=0;i--){
temp=Num/UART_Pow(10,i)%10;
UART_SendByte(temp);
}
}
//printf重定向, printf的底层是fputc 把它重定向到串口上
int fputc(int ch,FILE *f){
UART_SendByte(ch);
return ch;
}
uint8_t UART_Flag; //接收完成标志位
uint8_t UART_Data; //接收到的数据
//中断服务函数
void USART1_IRQHandler(void){
//如果RXNE为1
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
UART_Flag=1;
//接收数据
UART_Data=USART_ReceiveData(USART1);
//清除标志位
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//H文件
#ifndef __USART_H__
#define __USART_H__
extern uint8_t UART_Flag;
extern uint8_t UART_Data;
void UART_Init(void);
void UART_SendByte(uint8_t data);
void UART_SendArray(uint8_t* Arr,uint8_t Length);
void UART_SendString(uint8_t* Str);
void UART_SendNum(uint32_t Num,uint8_t Length);
#include <stdio.h>
#endif
假设我们有这样一种数据包格式, 包长度可变, 以@开头,以\r\n结尾
基于以上代码, 我们用状态机的思维来编写接收数据包
//发送数据包 直接SendString
void UART_SendStringPacket(uint8_t * Str){
UART_SendString(Str);
}
//接收的数据包, 在h中extern出去
char UART_ReturnStr[100];
//接收状态
typedef enum{
UART_Rxstate_WaitingStart, //等待开始
UART_Rxstate_Receiving, //接收中
UART_Rxstate_WaitingOver, //等待结束
}UART_Rxstate;
UART_Rxstate s=UART_Rxstate_WaitingStart;//接收状态实例化
//中断服务函数, 用于接收数据包到ReturnStr
void USART1_IRQHandler(void){
static uint8_t UART_Count=0;
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
UART_Data=USART_ReceiveData(USART1);//接收一个字节
//状态为等待开始
if(s==UART_Rxstate_WaitingStart){
if(UART_Data=='@'){
//如果该字节为@,进入接收状态
s=UART_Rxstate_Receiving;
}
}
//状态为正在接收
else if(s==UART_Rxstate_Receiving){
if(UART_Data=='\r')
//如果该字节为\r',进入等待结束状态
s=UART_Rxstate_WaitingOver;
else{
//正常接收字节
UART_ReturnStr[UART_Count]=UART_Data;
UART_Count++;
}
}
//状态为等待结束
else if(s==UART_Rxstate_WaitingOver){
//结束接收,Flag置1
if(UART_Data=='\n'){
s=UART_Rxstate_WaitingStart;
UART_ReturnStr[UART_Count]='\0';
UART_Count=0;
UART_Flag=1;
}
}
}
}