串口通讯是串行通讯里面的异步方式。串行通讯是相对于并行通讯来讲的。串口是一个事实存在的东西,好比DB9接口。 串口通信里面的波特率,其实是比特率。若是这两点你还不是很清楚地话,好好往下看。编程
(1)、同步和异步的区别:简单来讲就是发送方和接收方按照同一个时钟节拍工做就叫同步,发送方和接收方没有统一的时钟节拍、而各自按照本身的节拍工做就叫异步。 (2)、同步通讯中,通讯双方按照统一节拍工做,因此配合很好;通常须要发送方给接收方发送信息同时发送时钟信号,接收方根据发送方给它的时钟信号来安排本身的节奏。同步通讯用在通讯双方信息交换频率固定,或者常常通讯时。带时钟同步信号传输。如-SPI,IIC通讯。 (3)、异步通讯又叫异步通知。在双方通讯的频率不固定时(有时 3ms 收发一次,有时 3 天才收发一次)不适合使用同步通讯,而适合异步通讯。异步通讯时接收方没必要一直在乎发送方,发送方须要发送信息时会首先给接收方一个信息开始的起始信号,接收方接收到起始信号后就认为后面紧跟着的就是有效信息,才会开始注意接收信息,直到收到发送方发过来的结束标志。异步通讯:不带时钟同步信号。如·UART(通用异步收发器),单总线。markdown
(1)、电平信号和差分信号是用来描述通讯线路传输方式的。也就是说如何在通讯线路上表达 1 和 0. (2)、电平信号的传输线中有一个参考电平线(通常是 GND),而后信号线上的信号值是由信号线电平和参考电平线的电压差决定。 (3)、差分信号的传输线中没有参考电平,全部都是信号线。而后 1 和 0 的表达靠信号线之间的电压差。 总结:电平信号的 2 根通讯线之间的电平差别容易受到干扰,传输容易失败;差分信号不容易受到干扰所以传输质量比较稳定,现代通讯通常都使用差分信号,电平信号几乎没有了。总结 2:看起来彷佛相同根数的通讯线下,电平信号要比差分信号要快;可是实际仍是差分信号快,由于差分信号抗干扰能力强,所以 1 个发送周期更短。网络
(1)、串行、并行主要是考虑通讯线的根数,就是发送方和接收方同时能够传递的信息量的多少 (2)、譬如在电平信号下,1 根参考电平线+1 根信号线能够传递 1 位二进制;若是咱们有 3根线(2 根信号线+1 根参考线)就能够同时发送 2 位二进制;若是想同时发送 8 位二进制就须要 9 根线。 (3)、在差分信号下,2 根线(彼此差分)能够同时发送 1 位二进制;若是须要同时发送 8 位二进制,须要 16 根线。 总结:听起来彷佛并行接口比串行接口要快(串行接口一次只能发送 1 位二进制,而并行接口一次能够发送多位二进制)要更优秀;可是实际上串行接口才是王道,用的比较广。由于更省信号线,并且对传输线的要求更低、成本更低;并且串行时能够经过提升通讯速度来提升整体通讯性能,不必定非得要并行。 总结:异步、串行、差分,譬如 USB 和网络通讯更胜一筹。 异步
(1)、异步:串口通讯的发送方和接收方之间是没有统一的时钟信号的。 (2)、电平信号:串口通讯出现的时间较早,速率较低,传输的距离较近,因此干扰还不太明显,所以当时使用了电平信号传输。后期出现的传输协议都改为差分信号传输了。 (3)、串行通讯:串口通讯每次同时只能传输 1 个二进制位函数
(1)电平信号是用信号线电平减去参考线电平获得电压差,这个电压差决定了传输值是 1 仍是 0. (2)在电平信号时多少 V 表明 1,多少 V 表明 0 不是固定的,取决于电平标准。譬如 RS232电平中-3V~-15V 表示 1;+3~+15V 表示 0;TTL 电平则是+5V 表示 1,0V 表示 0. (3)无论哪一种电平都是为了在传输线上表示 1 和 0.区别在于适用的环境和条件不一样。RS232的电平定义比较大,适合干扰大、距离远的状况;TTL 电平电压范围小,适合距离近且干扰小的状况。 (4)咱们台式电脑后面的串口插座就是 RS232 接口的,在工业上用串口时都用这个,传输距离小于 15 米;TTL 电平通常用在电路板内部两个芯片之间。 (5)对编程来讲,RS232 电平传输仍是 TTL 电平是没有差别的。因此电平标准对硬件工程师更有意义,而软件工程师只要略懂便可。(把 TTL 电平和 RS232 电平混接是不能够的)
性能
(1)衡量通信性能的一个很是重要的参数就是通信速率,一般以比特率(Bitrate)来表示,即每秒钟传输的二进制位数,单位为比特每秒(bit/s)。容易与比特率混淆的概念是“波特率”。(Baudrate),它表示每秒钟传输了多少个码元。而码元是通信信号调制的概念,通信中经常使用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。如常见的通信传输中,用 0V表示数字 0,5V 表示数字 1,那么一个码元能够表示两种状态 0 和 1,因此一个码元等于一个二进制比特位,此时波特率的大小与比特率一致;若是在通信传输中,有 0V、2V、4V以及 6V分别表示二进制数 00、0一、十、11,那么每一个码元能够表示四种状态,即两个二进制比特位,因此码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。由于不少常见的通信中一个码元都是表示两种状态,人们经常直接以波特率来表示比特率。譬如每秒种能够传输 9600 个二进制位(传输一个二进制位须要的时间是 1/9600秒,也就是 104us),比特率就是 9600.但由于一个码元都是表示两种状态,因此比特率=波特率。也一般说波特率就是 9600 (2)串口通讯的波特率不能随意设定,而应该在一些值中去选择。通常最多见的波特率是 9600或者 115200.为何波特率不能够随便指定?主要是由于:第一,通讯双方必须事先设定相同的波特率这样才能成功通讯,若是发送方和接收方按照不一样的波特率通讯则根本收不到,所以波特率最好是你们熟知的而不是随意指定的。第二,经常使用的波特率通过长久发展,就造成了共识,你们经常使用就是 9600 或者 115200.优化
(1)串口通讯时,收发是一个周期一个周期进行的,没周期传输 n 个二进制位。这一个周期就叫作一个通讯单元,一个通讯单元是由:起始位+数据位+奇偶校验位+中止位组成的。 (2)起始位表示发送方要开始发送一个通讯单元;数据位是一个通讯单元中发送的有效信息位;奇偶校验位是用来校验数据位,以防止数据位出错的;中止位是发送方用来表示本通讯单元结束标志的。 (3)起始位的定义是串口通讯标准事先指定的,是由通讯线上的电平变化来反映的。 (4)数据位是本次通讯真正要发送的有效数据,串口通讯一次发送多少位有效数据是能够设定的(通常可选的有 六、七、八、9,99%状况下咱们都是选择 8 位数据位。由于咱们通常经过串口发送的文字信息都是 ASCII 码编码的,而 ASCII 码中一个字符恰好编码为 8 位。) (5)奇偶校验位是用来给数据位进行奇偶校验(把待校验的有效数据逐个位的加起来,总和为奇数奇偶校验位就为 1,总和为偶数奇偶校验位就为 0)的,能够在必定程度上防止位反转。ui
(6)中止位的定义是串口通讯标准事先指定的,是由通讯线上的电平变化来反映的。常见的有 1 位中止位,1.5 位中止位,2 位中止位等。99%状况下都是用 1 位中止位。 总结:串口通讯时由于是异步通讯,因此通讯双方必须事先约定好通讯参数,这些通讯参数包括:波特率、数据位、奇偶校验位、中止位(串口通讯中起始位定义是惟一的,因此通常不用选择) 编码
(1)单工就是单方向,双工就是双方同时收发,同时只能单方向可是方向能够改变叫半双工 (2)若是只能 A 发 B 收则单工,A 发 B 收或者 B 发 A 收(两个方向不能同时)叫半双工,A发 B 收同时 B 发 A 收叫全双工。 spa
(1)任何通讯都要有信息传输载体,或者是有线的或者是无线的。 (2)串口通讯是有线通讯,是经过串口线来通讯的。 (3)串口通讯线最少须要 2 根(GND 和信号线),能够实现单工通讯,也可使用 3 根通讯线(Tx、Rx、GND)来实现全双工。
(1)串口通讯属于基层基本性的通讯规约,它本身自己不会去协商通讯参数,须要通讯前通讯双方事先约定好通讯参数(波特率、数据位、奇偶校验位、中止位等) (2)串口通讯的任何一个关键参数设置错误,都会致使通讯失败。譬如波特率调错了,发送方发送没问题,接收方也能接收,可是接收到全是乱码···
(1)、串口通讯的发送方每隔必定时间(时间固定为 1/波特率,单位是秒)将有效信息(1或者 0)放到通讯线上去,逐个二进制位的进行发送。 (2)接收方经过定时(起始时间由读到起始位标志开始,间隔时间由波特率决定)读取通讯线上的电平高低来区分发送给个人是 1 仍是 0。依次读取数据位、奇偶校验位、中止位,中止位就表示这一个通讯单元(帧)结束,而后中间是不定长短的非通讯时间(发送方有可能紧接着就发送第二帧,也可能半天都不发第二帧,这就叫异步通讯),下来就是第二帧····· 总结:第一,波特率很是重要,波特率错了整个通讯就乱套了;数据位、奇偶校验位、中止位也很重要,不然可能认不清数据。 第三,经过串口无论发数字、仍是文本仍是命令仍是什么,都要先对发送内容进行编码,编码成二进制再进行逐个位的发送。 (3)串口发送的通常都是字符,通常都是 ASCII 码编码后的字符,因此通常设置数据位都是 8,方便恰好一帧发送 1 个字节。
串口通信的物理层有不少标准及变种,主要讲解 RS-232 标准 ,RS-232标准主要规定了信号的用途、通信接口以及信号的电平标准。由于咱们常见的市面上的开发板在串口通信那一讲都是关于RS-232标准的协议。 咱们这里就不讲啥结构体了,这些直接去看数据手册就行了,讲一下配置过程步骤,作到成竹在胸、心中有数。
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
复制代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
复制代码
/*链接 PA10 复用到 USART1_Rx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
/*链接 PA9 复用到 USART1__Tx*/
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
复制代码
STM32有不少的内置外设,这些外设的外部引脚都是与GPIO复用的。也就是说,一个GPIO若是能够复用为内置外设的功能引脚,那么当这个GPIO做为内置外设使用的时候,就叫作复用。例如串口1的发送接收引脚是PA9,PA10,当咱们把PA9,PA10不用做GPIO,而用作复用功能串口1的发送接收引脚的时候,叫端口复用。
/* GPIO初始化 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/* 配置Tx引脚为复用功能 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 ;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置Rx引脚为复用功能 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);
复制代码
串口初始化是经过 USART_Init()函数实现的。
/* 波特率设置:115200 */
USART_InitStructure.USART_BaudRate = 115200;
/* 字长(数据位+校验位):8 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
/* 中止位:1个中止位 */
USART_InitStructure.USART_StopBits = USART_StopBits_1;
/* 校验位选择:偶校验 */
USART_InitStructure.USART_Parity = USART_Parity_No;
/* 硬件流控制:不使用硬件流 */
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
/* USART模式控制:同时使能接收和发送 */
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
/* 完成USART初始化配置 */
USART_Init(USART1, &USART_InitStructure);
/* 使能串口 */
USART_Cmd(USART1, ENABLE);
/*开启中断 接收到数据产生中断 进入中断服务函数 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
复制代码
初始化须要设置的参数为:波特率,字长,中止位,奇偶校验位,硬件数据流控制,模式(收,发)。 这里面的USART_Cmd();函数很好理解,就是是能串口。USART_ITConfig();就是开启中断响应了,这里面的第二个入口参数咱们通常写的是USART_IT_RXNE
.也就是打开接收中断,即程序在发送数据结束的时候要产生中断,调到中断服务函数中。
咱们前一章说了,只要你的程序里面用了中断,就必须配置串口优先级分组。可是咱们咱们有时候会发现咱们在一些厂家的串口例程中没有配置串口中断优先级分组,这是由于他程序只有一个串口中断就没有所谓的优先级,配不配置,程序都只有个中断。
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
复制代码
这个函数很重要,既然有中断就要执行串口中断服务函数中去,执行相应的指令。
void USART1_IRQHandler(void)
{
if(USART_GetITStatus( USART1, USART_IT_RXNE ) != RESET)//获取接收中断标志位(接收到的数据必须是0x0d 0x0a结尾)
{
Res = USART_ReceiveData( USART1 );
if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,从新开始
else USART_RX_STA|=0x8000;//接收完成了
}
else //还没收到0X0D
{
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;//接收数据错误,从新开始接收
}
}
}
}
复制代码
7.1 中断服务函数名不是随便起的,USART1_IRQHandler表示串口一个中断服务函数,函数名字在启动文件startup_stm32f10x_hd.s文件中能够找到。 7.2 if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 见名知意,获得串口中断的标志位,判断是否发生串口1接收中断,若是是串口接收中断,则读取串口接受到的数据。 7.3 Res =USART_ReceiveData(USART1);将从串口1读取接收到的数据赋值给变量Res。 7.4 简单的 接 收 协 议。通 if语句 , 配 合 一 个 数 组USART_RX_BUF[],一个接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局变量,由读者自行添加。因为它起到相似寄存器的功能,这里暂且称之为寄存器)实现对串口数据的接收管理。USART_RX_BUF 的大小由 USART_Rec_Len 定义,也就是一次接收的数据最大不能超过 USART_Rec_Len 个字节。USART_RX_STA 是一个接收状态寄存器。 接收到从电脑串口调试助手发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收状态寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2 个字节组成:回车符的ASCII码是0X0D 和换行符的ASCII码是 0X0A)的第一个字节 0X0D 时,计数器将再也不增长,等待0X0A 的到来,而若是 0X0A 没有来到,则认为此次接收失败,从新开始下一次接收。若是顺利接收到 0X0A,则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其余程序清除,从而开始下一次的接收,而若是迟迟没有收到 0X0D,那么在接收数据超过 USART_Rec_Len 的时候,则会丢弃前面的数据,从新接收。
这个自定义的发送函数是咱们直接在程序中发送数据到串口调试助手,这个自定义的函数和中断服务函数无关,由于中断服务函数是用来接收数据的,就是咱们经过串口调试助向单片机发送数据,须要用到中断服务函数。换句话说,若是你只想经过单片机将数据发送到串口调试助手的话,就不须要写中断服务函数了。可是这样作法没有意义,咱们用串口是主要是接收数据的,单单发送一个数据意义不大。
/***************** 发送一个字符 **********************/
static void Uart_SendByte(uint8_t ch)
{
/* 发送一个字节数据到USART1 */
USART_SendData(USART1,ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//获取发送状态
}
/***************** 发送字符串 **********************/
void Uart_SendString(uint8_t *str)
{
uint8_t k=0;
do
{
Uart_SendByte(*(str + k) );
k++;
} while(*(str + k)!='\0');
}
/***************** 指定长度的发送字符串 **********************/
void Uart_SendStr_length(uint8_t *str,uint32_t strlen )
{
uint8_t k=0;
do
{
Uart_SendByte( *(str + k) );
k++;
} while(k < strlen);
}
复制代码
咱们这里编写了三个函数主要是功能是:向单片机发送一个字符、向单片机发送一个字符串、向单片机发送指定长度的字符串。 代码很短,很少解释。在发送字节的时候用到了获取状态函数,获取发送数据的寄存器第7位的状态,当为1是,数据传送到移位寄存器。传送完了就能够发送了。
int main(void)
{
char ch;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init();
Uart_SendString( (uint8_t *)"这条数据是来自单片机发送的数据\n" );
Uart_SendString( (uint8_t *)"输入数据并以回车键结束\n" );
while(1)
{
}
}
复制代码
首先咱们须要调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);完成中断优先级的分组设置。再调用USART1_Init函数完成 USART 初始化配置,包括 GPIO配置,USART配置,接收中断使用等等信息。接下来就能够调用字符发送函数把数据发送给串口调试助手了。最后主函数什么都不作,只是静静地等待 USART接收中断的产生,并在中断服务函数把数据回传。
保证开发板相关硬件链接正确,用 USB 线链接开发板的串口调试usb接口跟电脑,在电脑端打开串口调试助手,配置到波特率、校验位、数据位、中止位。把编译好的程序下载到开发板,此时串口调试助手便可收到开发板发过来的数据。咱们在串口调试助手发送区域输入任意字符,点击发送按钮,立刻在串口调试助手接收区便可看到相同的字符。由于我在家里手上没有串口线,因此不能演示截图了。
单片机及收到后,执行相应的指令。咱们不只仅能够将数据发送到串口调试助手,咱们还能够在串口调试助手发送数据给控制器,控制器程序根据接收到的数据进行下一步工做。首先,咱们来编写一个程序实现开发板与电脑通讯,在开发板上电时经过 USART发送一串字符串给电脑,而后开发板进入中断接收等待状态,若是电脑有发送数据过来,开发板就会产生中断,咱们在中断服务函数接收数据,并立刻把数据返回发送给电脑。
//重定向c库函数printf到串口,重定向后可以使用printf函数
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(USART1, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return (ch);
}
//重定向c库函数scanf到串口,重写向后可以使用scanf、getchar等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(USART1);
}
复制代码
在 C 语言标准库中,fputc函数是 printf 函数内部的一个函数,功能是将字符 ch写入到文件指针 f所指向文件的当前写指针位置,简单理解就是把字符写入到特定文件中。咱们使用 USART 函数从新修改 fputc函数内容,达到相似“写入”的功能。fgetc 函数与 fputc 函数很是类似,实现字符读取功能。在使用 scanf函数时须要注意字符输入格式。 还有一点须要注意的,使用 fput和 fgetc函数达到重定向 C语言标准库输入输出函数必须在 MDK的工程选项把“Use MicroLIB”勾选上,MicoroLIB 是缺省 C库的备选库,它对标准 C 库进行了高度优化使代码更少,占用更少资源。为使用 printf、scanf 函数须要在文件中包含 stdio.h头文件。
int main(void)
{
char ch;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init();
USART1_Init();
Uart_SendString( (uint8_t *)"这条数据是来自串口发送的\n" );
Uart_SendString( (uint8_t *)"输入数据并以回车键结束\n" );
while(1)
{
ch=getchar();
printf("接收到字符:%c\n",ch);
switch(ch)
{
case '1':
printf("LED灯亮");
GPIO_ResetBits(GPIOH,GPIO_Pin_12);//PH12接了一个LED灯
break;
default:
break;
}
}
}
复制代码
至此串口通信的编程详解就结束了,如今你会了吗?