单片机与外围设备之间的信息交换和传输我们称为通信。过去通信方式有两种:并行通信和串行通信。
并行通信:
定义:并行通信是指利用多条传输线将一个数据的各位同时传送。
传输方式:传输一个字节(8个位)的数据时,并口是将8个位一字排开,分别在8条连接线上同时传输。
特点:传输速度块,适用于短距离通信。
缺点:虽然,并行通信传输速度快,但是由于,线与线之间存在电磁干扰,会导致数据错误。而且由于线比较多,PCB布线比较麻烦,所以并行通信不常用,而串行通信用得比较广泛。
串行通信:
定义:串行通信是指利用一条传输线将数据一位位地顺序传送。
传输方式:传输一个字节(8个位)的数据时,串口是将8个位排好队,逐个地在1条连接线上传输。
特点:通信线路简单,适用于远距离通信和上、下机通信。
缺点:传输数度比较慢。(因为它是一位一位的传输的)
同步方式:即发送方和接收方的时钟频率要求一直,所以双方就需要在同步时钟线的牵引下,收、发数据需要同时进行。
异步方式:双方在相同的波特率的情况下,发送器只管发送,接收器只管接收,收发互不干涉。
单工通信:数据只能单方向传输。
半双工通信:数据传输上支持双方向传输,但是不能同时进行双向传输,在同一时刻,某一端只能进行发送或者接收。
全双工通信:双工是指数据同时在两个方向上传输,是两个单工通信的结合,要求发送设备和接收设备同时具有独立的接收和发送能力。
波特率:
所以,双方需要约定好波特率。
UART,是通用异步收发传输器(USART则是通用同步/异步收发传输器),既然是“器”,显然,它就是个设备而已,要完成一个特定的功能的硬件,它本身并不是协议。那么它要完成什么功能呢?它的最基本功能,是串行数据和并行数据之间的转换。注意:UART是TTL电平。注意:串口通信是一对一的通信,而不是像I^2C那样一对多的通信。
注意:UART、RS232、RS485,它们实际上都是串口上面的属性,只不过它们的功能不一样。UART的功能是将串行转并行,而RS232、RS485则是一种电平标准。
双工:
USART与UART的区别:
1,USART(同步,异步可选择)(全双工,半双工可选择)
2,UART(异步)(全双工,半双工可选择)
串口通信的作用:
1,用于设备间的通信
2,可以用于设备调试----printf的打印
发送数据寄存器:它接收CPU从数据总线上送来的并行数据。并加以保存。
发送移位寄存器:它接收从发送数据寄存器送来的并行数据,并给数据加上起始位和停止位(即:帧头和帧尾),然后以发送时钟的速率把数据逐位移出,即将并行数据转换为串行数据输出。
接收数据寄存器:它从输入移位寄存器中接收并行数据,由CPU(或者DMA)取走。
接收移位寄存器:它以接收时钟的速率把出现在串行数据输入线上的数据逐位移入,当数据装满后,并行送往接收数据寄存器,即将串行数据转换成并行数据,然后去掉起始位和停止位,然后将数据传给接收数据寄存器。
UART在发送数据时,是低位先行(即:低位先发送)还是高位先行?接收数据呢?
由图知:红线部分是从移位寄存器的尾部出发的,而移位寄存器的尾部是数据的低位,所以发送数据的时候是低位先行(即:先发送低位,再发送高位)
接收数据同理:红线部分是接移位寄存器的头部,先将低位接收,然后低位在移位寄存器中逐步往后移动,所以接收数据的时候也是低位先行(即:先接收低位,再接收高位)
UART内部总结构:
串口数据包:11bit
1. 起始位:低电平 1bit
2. 数据位:8个位,低位先行 8bit
3. 奇偶校验位:1个bit
奇校验:指的是数据位中1的个数。如果数据位中1的个数位偶数,那么该位为1。如果数据位中1的个数位奇数,那么该位为0。
偶校验:指的是数据位中1的个数,如果数据位中1的个数位偶数,那么该位为0。如果数据位中1的个数位奇数,那么该位为1。
4. 停止位:高电平 1bit
如果串口没有奇偶校验位:一共10个bit(1+8+1 = 10)
包含奇偶效验位:数据长度为9位(不包含起始位和停止位)
注意:正因为包含了奇偶效验位,所以数据为9位长度,所以可知,奇偶效验位属于数据位中的一员。
不包含奇偶效验位:数据长度为8位(不包含起始位和停止位)
所以我们设置数据位长度的时候,如果需要使用奇偶效验位,那么就设置为9位长度,不使用则设置为8位。
奇效验就是,如果8位数据位的1为偶数,那么再加上奇偶效验位中的1,就可以将1的个数凑成奇数。如果8位数据位的1为奇数,那么再加上奇偶效验位中的0,就可以将1的个数凑成奇数。
偶效验就是,如果8位数据位的1为偶数,那么再加上奇偶效验位中的0,就可以将1的个数凑成偶数。如果8位数据位的1为奇数,那么再加上奇偶效验位中的1,就可以将1的个数凑成偶数。
由此可知:为什么奇校验中,如果数据位中1的个数位偶数,那么该位为1。如果数据位中1的个数位奇数,那么该位为0。因为要把奇偶效验位一并算在数据位上。
但是,一般我们不用奇偶效验位。因为,每一次接收发送一帧数据,都要对数据中的1的个数进行判断,这样传输速度就会慢一些。通常我们的校验方式都会加在通信协议上面,每个字节的校验我们一般不用。
UART初始化函数:我们可以看到,初始化函数的结构体成员有很多,但是并不是我们都要去设置的,根据异步模式还是同步模式来设置,如果是异步模式,我们只需要设置前面6个成员就行了。如果是,同步模式,则全部成员都需要设置。
第一个成员:设置波特率,它可以直接赋值。如:直接赋值为115200
第二个成员:设置数据位长度。设置为9位长度,则是包含奇偶效验位。设置为8位长度,则是不包含奇偶效验位。
第三个成员:设置停止位
第四位成员:是否使用奇偶效验位。
第5个成员:是否使用硬件流控模式
第6个成员:使能发送/接收模式
串口助手串口设置:
这个是串口助手的串口设置,我们可以看到我们程序中需要对UART的设置与串口助手中对串口设置的对象是一模一样的。
UART外设使能函数。使能UART1还是使能UART2。
UART发送数据函数,注意:它一次只能发送一个字节,而不能多个字节一起发。因为数据寄存器只发送一个字节。
接收数据函数:它有一个返回值,返回最近接收到的一个字节数据。
获取某一个事件发生的标志位函数:通过标志位函数,检测到标志位,然后进行相应的操作。我们主要是用这4个标志位。发生则返回SET,没发生则返回RESET。注意:标志位需要自己手动去清空。
USART_FLAG_TXE:当发送数据寄存器中的数据已经取完了,该标志位就会被置1,从而引发该事件的中断。所以,其实USART_FLAG_TXE就是用来标志一个事件的,通过它的值可以知道该事件有没有发生(即发送数据寄存器中的数据有没有被取走)。当数据写完后,发送数据寄存器中的数据就会被移位寄存器取走,此时发送数据寄存器里面为空。
USART_FLAG_TC:当发送移位寄存器中的1个字节数据已经通过TX脚一位一位的移出去后,1个字节数据被发送完成,该标志位就会被置1,从而引发该事件的中断。所以,其实USART_FLAG_TC就是用来标志“发送移位寄存器中的数据有没有全部发送出去”这件事的。
USART_FLAG_RXNE:表示已经接收到了完整的数据。即接收到的数据已经从移位寄存器放到数据寄存器中,并被CPU(或者DMA)取走了。注意:此时接收数据寄存器里面的数据不会被清空,它需要保留该数据,因为可能后面需要去读这个数据。
实现stm32与PC串口通信,编程步骤:
1,打开时钟---GPIOA,串口1,AFIO
2,GPIO初始化
---GPIO_Pin_9(TX)
---复用推挽输出
---速度--2MHZ
---GPIO_Pin_10(RX)
---浮空输入(接收到的高低电平由片外外设来决定,所以用浮空输入模式)
3,串口初始化
---波特率---115200
---数据位数---8bit
---停止位---1bit
---奇偶校验---无
---硬件流控---无
---模式---发送和接收使能
4,使能串口
注意:USART1、GPIOA、AFIO(复用功能)都在APB2总线上面。
void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
// 1,打开时钟---GPIOA,串口1,AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO|RCC_APB2Periph_USART1,ENABLE);
// 2,GPIO初始化
GPIO_InitStruct.GPIO_Pin =GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode =GPIO_Mode_AF_PP; //推挽输出
GPIO_InitStruct.GPIO_Speed =GPIO_Speed_2MHz;
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);
// 3,串口初始化
USART_InitStruct.USART_BaudRate =115200; //波特率
USART_InitStruct.USART_HardwareFlowControl =USART_HardwareFlowControl_None; //硬件流控失能
USART_InitStruct.USART_Mode =USART_Mode_Rx|USART_Mode_Tx; //注意:这里使能RX/TX是可以用或操的
USART_InitStruct.USART_Parity =USART_Parity_No; //奇偶效验位
USART_InitStruct.USART_StopBits =USART_StopBits_1; //停止位
USART_InitStruct.USART_WordLength =USART_WordLength_8b; //数据位长度
USART_Init(USART1,&USART_InitStruct);
/*清空标志位,因为很多标志位一开始都是默认置上的(即:置1),所以我们需要在初始化的时候,就将标志位清空。如果没有这一步清空标志位,那么printf第一个字母就会打印不出来,所以我们需要在传输数据之前,就清空标志位。如果不清空标志位,那么printf函数打印的第一个字节还没来得即发送给PC,就会被第二个字节给覆盖掉了。*/
USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE);
// 4,使能串口
USART_Cmd(USART1,ENABLE);
}
/*功能:MCU从PC上面接收到的数据,又通过MCU发送到PC上面显示。这样,我们就可以即可以验证接收功能是否成功,也可以验证发送功能是否成功*/
int main(void)
{
Systick_Config(72);
USART1_Config();
printf("HELLO!\n");
while(1){
/*由于接收数据需要等移位寄存器接收数据,然后将数据移到接收数据寄存器中去,这些操作需要花费时间,如果我们不等数据接收完就马上进行下一个数据的接收,那么就会导致后面的数据将前面的数据给覆盖掉,这样的话数据就出现了错误*/
if(SET==USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){
USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空标志位
USART_SendData(USART1,USART_ReceiveData(USART1)); //发送数据给电脑,注意:是一个字节一个字节的发送
}
}
}
注意:
1.烧录程序的时候,记得要把串口助手给关掉,否则串口被占用,无法烧录程序。
2.发送/接收都是一个字节一个字节的发送/接收的。
3.我们一开始就需要清空标志位。因为很多标志位一开始都是默认置上的(即:置1),所以我们需要在初始化的时候,就将标志位清空。如果一开始不清空标志位,那么第一个字节还没来得即发送,就会被第二个字节给覆盖掉了。
4.标志位,只是一个值,它是用来给我们写while阻塞或者中断的。接收/发生标志位置不置位,单片机都不会去自动阻塞,如果我们需要阻塞,则需要我们自己手动去写while()循环在main函数中进行手动阻塞(这也体现出了它的灵活性)。
我们写单片机上面写printf函数,并不能将数据打印出来,因为它没有屏幕,嘿嘿。
那么,将数据传输到PC上面,在PC上面显示不就行了吗?这是个好主意。但是,在PC上面要怎么才能打印出数据来?
我们可以借助上面学的串口UART,利用UART将数据传输到串口助手上面,将数据显示出来。
但是,又个问题。printf函数内部并没有UART函数的配置,那么我们怎么将printf与UART相结合呢?
我们可以重写printf函数。实际上prinf函数是一个库函数,每次执行printf函数的时候,它就会调用内部的 fputc()函数。fputc()函数将数据打印在屏幕上面。所以,我们需要重新编写fputc()函数,将其内部改写成UART相关操作。
如果在单片机中需要使用printf,需要做两件事情
1,需要重写fputc----重定向
int fputc(int ch,FILE *f)
{
USART_SendData(USART1, ch);
while(SET!=USART_GetFlagStatus(USART1, USART_FLAG_TC))
;
USART_ClearFlag(USART1, USART_FLAG_TC);
return ch;
}
2,使用微库
keil---->魔术棒---->target---->USE MicroLIB打勾
12》printf第一个字母打印不出来----串口配置函数
USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE);
#include "stdio.h"
int fputc(int ch,FILE *f)
{
USART_SendData(USART1, ch);
while(SET!=USART_GetFlagStatus(USART1, USART_FLAG_TC)) //如果一个字节数据没有发送完,那么就阻塞在这里
;
USART_ClearFlag(USART1, USART_FLAG_TC); //传输完数据后,手动清空标志位
return ch;
}
int main(){
printf("HELLO!\n"); //此时串口助手上面就打印出来HELLO!了
return 0;
}
注意:不能在中断服务函数里面中用printf函数,这样会把MCU搞晕的。
因为在启动文件中,printf()函数是WEAK属性
weak属性也就是,如果没有重写该函数,那么就执行库中提供的函数。如果重写了该函数的话,就执行重写后的函数。