stm32F1以及51单片机串口通信详解


1、连线: 如图所示:我们先记住四条线,分别是电源线,地线,以及发送和接收线
既然两个单片机要通讯,那么一个发送一个接收,那么肯定是一个单片机的发送端连接到另一个单片机的接收端,就像两个人说话,一个用嘴说,一个用耳朵听,那么话语就是其中的抽象连线
2、数据的传输格式
(1)核心思想:就是低位先发高位后发(也就是说先发低位比如01234567位先发0,如果是二进制数据11100100先发0)
(2)数据的本质是什么?答案:电平状态
比如:
发送一个 0xE4 这个数据,用二进制形式表示就是 0b11100100,在 UART 通信过程中,是低位先发,高位后发的原则,那么就让 TXD首先拉低电平,持续一段时间,发送一位 0,然后继续拉低,再持续一段时间,又发送了一位 0,然后拉高电平,持续一段时间,发了一位 1……一直到把 8 位二进制数字 0b11100100全部发送完毕。
(3)数据传输格式是什么?答案:数据包
每个数据包包含1个起始位,5至9个数据位(一般是8位),可选的奇偶校验位(一般不设置)和1或1.5或2个停止位(一般是1位),协议如下:


如图:我们可以很清晰明了的看出数据包包含4种数据位类型,终结起来就是:起始>>数据>>校验>>停止
其实是和我们生活中做事是一样的总要有个开始和结束,然后包含事情的内容以及检查一下做的事到底对不对,当然啦,现实中的事情往往复杂的多,毕竟很多事往往无疾而终……
3、数据传输的动力:波特率
那么波特率的作用是什么?答案:其实说白了就是告诉单片机多久发送一个数据包
比特率是每秒钟传输二进制代码的位数,单位是:位/秒(bps)。
在电子通信领域,波特(Baud)即调制速率,指的是有效数据讯号调制载波的速率,即单位时间内载波调制状态变化的次数。
波特率表示每秒钟传送的码元符号的个数,它是对符号传输速率的一种度量,它用单位时间内载波调制状态改变的次数来表示,1波特即指每秒传输1个符号。
数据传输速率使用波特率来表示。单位bps(bits per second),常见的波特率9600bps、115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为115200bps,那么传输一个比特需要的时间是1/115200≈8.68us。

以上就是串口通信的原理,其实总结起来一句话:
以波特率为动力发送数据包!!!(so easy)
原理有了那么具体怎么实现传输数据的功能呢?就像道理大家都懂就是不会做事情
下面我们以51单片机以及stm32单片机为例,分别写一下串口通讯的驱动代码:
4、软件驱动:
4.1 51单片机:
4.1.1 80C51串行口的结构

有两个物理上独立的接收、发送缓冲器SBUF,它们占用同一地址99H ;接收器是双缓冲结构 ;发送缓冲器,因为发送时CPU是主动的,不会产生重叠错误。
4.1.2 串行口的控制寄存器(SCON)
用以设定串行口的工作方式、接收/发送控制以及设置状态标志:

SM0和SM1为工作方式选择位,可选择四种工作方式:

SM2,多机通信控制位,主要用于方式2和方式3。当接收机的SM2=1时可以利用收到的RB8来控制是否激活RI(RB8=0时不激活RI,收到的信息丢弃;RB8=1时收到的数据进入SBUF,并激活RI,进而在中断服务中将数据从SBUF读走)。当SM2=0时,不论收到的RB8为0和1,均可以使收到的数据进入SBUF,并激活RI(即此时RB8不具有控制RI激活的功能)。通过控制SM2,可以实现多机通信。在方式0时,SM2必须是0。在方式1时,如果SM2=1,则只有接收到有效停止位时,RI才置1。
REN,允许串行接收位。由软件置REN=1,则启动串行口接收数据;若软件置REN=0,则禁止接收。
TB8,在方式2或方式3中,是发送数据的第九位,可以用软件规定其作用。可以用作数据的奇偶校验位,或在多机通信中,作为地址帧/数据帧的标志位。在方式0和方式1中,该位未用。
RB8,在方式2或方式3中,是接收到数据的第九位,作为奇偶校验位或地址帧/数据帧的标志位。在方式1时,若SM2=0,则RB8是接收到的停止位。
TI,发送中断标志位。在方式0时,当串行发送第8位数据结束时,或在其它方式,串行发送停止位的开始时,由内部硬件使TI置1,向CPU发中断申请。在中断服务程序中,必须用软件将其清0,取消此中断申请。
RI,接收中断标志位。在方式0时,当串行接收第8位数据结束时,或在其它方式,串行接收停止位的中间时,由内部硬件使RI置1,向CPU发中断申请。也必须在中断服务程序中,用软件将其清0,取消此中断申请。
看起很复杂,但是实际上没有啥东西:比如,我们选择方式1,那么只需将sm0=0,sm1=1,记忆:01,使能位REN=1;TI和RI就是中断标志位而已,我们需要做的就是软件清零而已,TB8通常不用
SCON=0X50;这是什么意思?解析成2进制就是0000 0000 0101 0000
我们主要看低8位:0101的前面两个数代表方式1,sm2=0,并使能,接收到的数据直接进入Buff并且使RI=1,这个时候需要我们在中断函数里将标志位清0即可
4.1.3 PCON功率控制寄存器中只有一位SMOD与串行口工作有关 :

SMOD(PCON.7) 波特率倍增位。在串行口方式1、方式2、方式3时,波特率与SMOD有关,当SMOD=1时,波特率提高一倍。复位时,SMOD=0。
这里不要想那么复杂只需要做一件事情:PCON=0X80; 这条语句应该可以看得懂
4.1.4波特率的计算(用波特率计算器)
方式0的波特率 = fosc/12
方式2的波特率 =(2SMOD/64)· fosc
方式1的波特率 =(2SMOD/32)·(T1溢出率)
方式3的波特率 =(2SMOD/32)·(T1溢出率)
T1 溢出率 = fosc /{12×[256 -(TH1)]}
在单片机的应用中,常用的晶振频率为:12MHz和11.0592MHz。所以,选用的波特率也相对固定。常用的串行口波特率以及各参数的关系如表所示。

4.1.5串口如何使用?
(1)确定串行口控制(编程SCON寄存器);
(2)确定T1的工作方式(编程TMOD寄存器);TMOD=0X20;
(3)计算T1的初值,装载TH1、TL1;
(4)启动T1(编程TCON中的TR1位);
4.1.6定时器知识点:
定时器/计数器的结构
定时器/计数器的实质是加1计数器(16位),由高8位和低8位两个寄存器组成。TMOD是定时器/计数器的工作方式寄存器,确定工作方式和功能;TCON是控制寄存器,控制T0、T1的启动和停止及设置溢出标志。

定时器/计数器的工作原理
计数器输入的计数脉冲源
系统的时钟振荡器输出脉冲经12分频后产生;
T0或T1引脚输入的外部脉冲源。
计数过程
每来一个脉冲计数器加1,当加到计数器为全1(即FFFFH)时,再输入一个脉冲就使计数器回零,且计数器的溢出使TCON中TF0或TF1置1,向CPU发出中断请求(定时器/计数器中断允许时)。如果定时器/计数器工作于定时模式,则表示定时时间已到;如果工作于计数模式,则表示计数值已满。
定时应用
用作定时器:此时设置为定时器模式,加1计数器是对内部机器周期计数(1个机器周期等于12个振荡周期,即计数频率为晶振频率的1/12)。计数值N乘以机器周期Tcy就是定时时间t 。
计数运用
用作计数器:此时设置为计数器模式,外部事件计数脉冲由T0或T1引脚输入到计数器。每来一个外部脉冲,计数器加1。但单片机对外部脉冲有基本要求:脉冲的高低电平持续时间都必须大于1个机器周期。
工作方式寄存器(TMOD)

GATE:门控位。
GATE=0时,只要用软件使TCON中的TR0或TR1为1,就可以启动定时器/计数器工作;(即需要一个启动条件)
GATE=1时,要用软件使TR0或TR1为1,同时外部中断引脚也为高电平时,才能启动定时器/计数器工作,即需要两个启动条件。
C/T :定时/计数模式选择位。
C/T =0为定时模式; C/T =1为计数模式。
M1M0:工作方式设置位。
计数器工作方式选择
M1 M0 工 作 方 式 功 能 说 明
0 0 方式0 13位计数器
0 1 方式1 16位计数器
1 0 方式2 自动重装8位计数器
1 1 方式3 定时器0:分成两个8位
定时器1:停止计数
定时器/计数器的控制
控制寄存器TCON
TCON的低4位用于控制外部中断,已在前面介绍。TCON的高4位用于控制定时器/计数器的启动和中断申请。其格式如下:

TF1(TCON.7):T1溢出中断请求标志位。T1计数溢出时由硬件自动置TF1为1。CPU响应中断后TF1由硬件自动清0。TR1(TCON.6):T1起/停控制位。1:启动 0:停止
TF0(TCON.5):T0溢出中断请求标志位,其功能与TF1类同。
TR0(TCON.4):T0起/停控制位。1:启动 0:停止
定时器/计数器的工作方式
方式0
方式0为13位计数,由TL0的低5位(高3位未用)和TH0的8位组成TL0的低5位溢出时向TH0进位,TH0溢出时,置位TCON中的TF0标志,向CPU发出中断请求。

方式1
方式1的计数位数是16位,由TL0(TL1)作为低8位、TH0(TH1)作为高8位,组成了16位加1计数器 。
方式2
方式2为自动重装初值的8位计数方式。

在方式2下,当计数器计满255(FFH)溢出时,CPU自动把TH 的值装入TL中,不需用户干预。因此特别适合于用作较精确的脉冲信号发生器。
方式3

方式3只适用于定时器/计数器T0,定时器T1方式3时相当于TR1=0,停止计数。
工作方式3将T0分成为两个独立的8位计数器TL0和TH0 。
注意:1、EA:访问外部程序存储器控制信号。
2、ES:串口中断允许控制位。
TF1:其值位1时,表示定时器T1计满溢出。
TF0:其值位1时,表示定时器T0计满溢出。
TR1:为1是定时器T1开始计数;0时不计数。
TR0:为1是定时器T0开始计数;0时不计数。
IE1:当发生外部中断1时其值为1。
IE0:当发生外部中断0时其值为1
以上的知识都掌握后就可以写程序啦:
4.1.7程序代码:
PC和单片机通信:
#include<reg52.h>
typedef unsigned char uchar;
void myuart()
{
SCON=0X50;
TMOD=0X20;
PCON=0X80;
TH1=0xFA; TL1=0XFA;
ES=1;
EA=1;
TR1=1;
}
void main(void)
{
myuart();
while(1);
}
void iuart() interrupt 4
{
uchar receiveData;
receiveData=SBUF;
RI = 0;
SBUF=receiveData;
while(!TI);
TI=0;
}
这里还需要注意一点是什么?也就是SBUF,如果是接收数据就从SBUF里读出数据来receiveData=SBUF;如果想发送,就往SBUF里写数据SBUF=receiveData;
4.2 stm32F103 库函数
4.2.1、简介:
STM32F103ZET6 有 3 个 USART(通用同步和异步收发器) + 2 个 UART(通用异
步收发器),分别是 USART1,USART2,USART3 和 UART4,UART5
4.2.2、USART 和 UART 有什么区别呢?
当进行异步通信时,这两者是没有区别的。区别在于 USART 比 UART 多了同步
通信功能,同步通信需要 STM32 提供时钟来同步。
这个同步通信功能可以把 USART 当做 SPI 来用,比如用 USART 来驱动 SPI 设
备。我们用得最多的是全双工异步通信功能


4.2.2.1、 我们需要设置的数据有通信速率,数据字长,奇偶检验位,停止位。一个典 型的设置是 115200 波特率,8 位数据,无奇偶校验,1 位停止位。
这个设置在固件函数库里面,我们是通过设置 USART_InitStructure 结构体,
然后调用 USART_Init 函数来实现的:
USART_InitStructure.USART_BaudRate = 115200; //设置通信波特率为 115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //设置通信数据格式为 8 位数据
USART_InitStructure.USART_StopBits = USART_StopBits_1; //设置停止位为 1 位
USART_InitStructure.USART_Parity = USART_Parity_No ; //设置为无奇偶校验
/* 设置为无硬件流控制,即无 CTS/RTS 控制 /
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
/ 设置发送使能,接收使能 */
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure); //调用 USART_Init,把上面的参数分别设置进 USART
的控制寄存器 USART1->CR1,USART1->CR2,USART1->CR3
USART_Cmd(USART1, ENABLE); //使能串口
4.2.2.2、 上面 USART_Init 函数配置了 USART1 的数据通信格式,但串口能工作的前提是需要配置相应的TX,RX引脚,这个是通过GPIO_Configuration函数来配置的:
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //打开 USART1 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //打开 AFIO 时钟
/* 配置 USARTx_Tx 为复用推挽输出 /
GPIO_InitStructure.GPIO_Pin = GPIO_TxPin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOx, &GPIO_InitStructure);
/ 配置 USARTx_Rx 为输入悬空 */
GPIO_InitStructure.GPIO_Pin = GPIO_RxPin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOx, &GPIO_InitStructure);
}
4.2.2.3、 配置好 USART1 使用的引脚,数据通信格式,下面就可以收发数据了,USART_GetFlagStatus 函数可以读取收发状态等,读取状态标志可以是以下几个:

4.2.2.4、 发送数据示例:
USART_SendData(USART1, ‘a’); //发送一个字符 a
4.2.2.5、 接收数据示例:
u16 RxData;
RxData = USART_ReceiveData(USART1); //从 USART1 接收数据到 RxData 变量
FlagStatus USART_GetFlagStatus();//获取状态标志位
void USART_ClearFlag();//清除状态标志位
ITStatus USART_GetITStatus();//获取中断状态标志位
void USART_ClearITPendingBit();//清除中断状态标志位
***4.2.2.6、下面是串口通信 printf 程序里的主要功能,上电打印一串信息,把接收到的数
据回显到 PC 上:
/ 用 printf 打印一串信息到 PC 的超级终端或串口调试软件上 /
printf("\n\rUSART Printf Example: retarget the C library printf function to the USART\n\r");
while (1)
{
if(USART_GetFlagStatus(USARTx,USART_FLAG_RXNE)==SET) //判断是否有数据要接收
{
i = USART_ReceiveData(USARTx); //接收数据
printf("%c\n\r",i&0xff); //回显到 PC 的超级终端或串口调试软件上
}
}
4.2.2.7、 printf 的实现
上面的 printf 是怎么实现的呢,这个是 C 标准库里定义的函数,我们是怎 样把它的输出重定向到串口的呢?
我们知道 printf 是调用 fputc 函数来打印的,所以我们只要把 fputc 函数 重定义就可以了:
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE f)
PUTCHAR_PROTOTYPE
{
/ 调用 USARTx 发送一个字符/
USART_SendData(USARTx, (u8) ch);
/ 等待发送完成 /
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET)
{
}
return ch;
}
另外还要加上头文件
#include “stdio.h”
4.2.2.8、 串口配置的一般步骤
(1)串口时钟使能,GPIO时钟使能:RCC_APB2PeriphClockCmd();
(2)串口复位:USART_DeInit(); 这一步不是必须的
(3)GPIO端口模式设置:GPIO_Init();
(4)串口参数初始化:USART_Init();
(5)开启中断并且初始化NVIC(如果需要开启中断才需要这个步骤)
NVIC_Init();
USART_ITConfig();
(6)使能串口:USART_Cmd();
(7)编写中断处理函数:USARTx_IRQHandler();
(8)串口数据收发:
void USART_SendData();//发送数据到串口,DR
uint16_t USART_ReceiveData();//接受数据,从DR读取接受到的数据
(9)串口传输状态获取:
FlagStatus USART_GetFlagStatus(USART_TypeDef USARTx, uint16_t USART_FLAG);
void USART_ClearITPendingBit(USART_TypeDef USARTx, uint16_t USART_IT);
4.2.2.9、 简单通信程序
#include "stm32f10x.h"
#include "sys.h"
#include "key.h"
#include "led.h"
#include "beep.h"
#include "delay.h"
void My_USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStrue;
USART_InitTypeDef USART_InitStrue;
NVIC_InitTypeDef NVIC_InitStrue;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE);
GPIO_InitStrue.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStrue.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStrue.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOA,&GPIO_InitStrue);
GPIO_InitStrue.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStrue.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStrue.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOA,&GPIO_InitStrue);
USART_InitStrue.USART_BaudRate=115200;
USART_InitStrue.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_InitStrue.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
USART_InitStrue.USART_Parity=USART_Parity_No;
USART_InitStrue.USART_StopBits=USART_StopBits_1;
USART_InitStrue.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USART_InitStrue);
USART_Cmd(USART1,ENABLE);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_InitStrue.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStrue.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStrue.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStrue.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStrue);
}
void USART1_IRQHandler(void)
{
u8 res;
if(USART_GetITStatus(USART1,USART_IT_RXNE))
{
res= USART_ReceiveData(USART1);
LED_Init();BEEP_Init();
BEEP=!BEEP;
LED1=!LED1;
USART_SendData(USART1,res);
}
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
My_USART1_Init();
while(1);
}
4.2.2.10、 原子printf函数实现:
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{ int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x)
{ x = x; }
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif
4.2.2.11、 原子的串行通信协议(重点,多项目可参考其思想):

程序如下:
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1);
if((USART_RX_STA&0x8000)==0)
{
if(USART_RX_STA&0x4000)
{
if(Res!=0x0a)USART_RX_STA=0;
else USART_RX_STA|=0x8000;
}
else
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
}
}
}
}
12、原子主函数:
while(1)
{
if(USART_RX_STA&0x8000)
{
len=USART_RX_STA&0x3fff;
printf("\r\n您发送的消息为:\r\n\r\n");
for(t=0;t<len;t++)
{
USART_SendData(USART1, USART_RX_BUF[t]);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);
}
printf("\r\n\r\n");
USART_RX_STA=0;
}else
{
times++;
if(times%5000==0)
{
printf("\r\n 串口实验\r\n");
printf("ALIENTEK\r\n\r\n");
}
if(times%200==0)printf("请输入数据,以回车键结束\n");
if(times%30==0)LED0=!LED0;
delay_ms(10);
}
}
}