赞
踩
通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统。比如STM32芯片里面集成了很多功能模块,如定时器计数、PWM输出、AD采集等等,这些都是芯片内部的电路,它们的配置寄存器、数据寄存器都在芯片里面,操作简单,直接读写就行。但是有些功能STM32内部没有,如想要蓝牙无线遥控的功能、想要陀螺仪加速度计测量姿态的功能,只能外挂芯片完成,数据都在STM32外面,这就需要通信线路发送或者接收数据完成数据交换。
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。STM32里的通信协议有如下表:(引脚列出的只是最简单最常用的值得注意的)
全双工为输入输出可同时进行,半双工为既可以输入也可以输出但不能同时进行,单工指只能输出或输入。
I2C和SPI有时钟线SCL为同步通信,没时钟线的为异步通信,需要双方约定一个采样频率,且需要加一些帧头帧尾等进行采样位置的对齐。
电平特性,单端就是它们引脚的高低电平都是对GND的电压差,所以单端信号通信的双方必须要共地,就是把GND接在一起。CAN和USB为差分数据通信,靠两个差分引脚的电压差来传输信号的,在通信时可以不用GND,不过USB协议里有些地方要单端信号,它还是需要共地的。差分信号有好的抗干扰特性,所以一般它的传输速度和距离都会比较高性能也不错。
USART和USB属于点对点通信,中间三个可挂载多个设备,需要一个寻址过程(设备编号)。
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力。主要特点就是用在单片机和电脑之间,其他如I2C、SPI等较复杂的通信协议多用在芯片与芯片之间,陀螺仪加速度计测量姿态芯片与STM32、MPU6050与STM32。
简单双向串口通信有两根通信线(发送端TX和接收端RX)
TX与RX要交叉连接
当只需单向的数据传输时,可以只接一根通信线。
当电平标准不一致时,需要加电平转换芯片。串口有很多电平标准,像我们这种直接从控制器里面出来的信号一般都是TTL电平,相同的电平才能互相通信,几种点评标准如下介绍。
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
(1)TTL电平:+3.3V或+5V表示1,0V表示0。低压小型设备,如单片机。
(2)RS232电平:-3到-15V表示1,+3到+15V表示0。一般在大型机器使用,由于环境比较恶劣静电干扰比较大,所以电压较大且允许波动的范围较广。
(3)RS485电平:两线压差+2到+6V表示1,-2到-6V表示0(差分信号)。通信距离可达上千米,上面两种最远几十米。
串口中,每个字节都装载在一个数据帧(10或11位)里,每个数据帧都由起始位、数据位和停止位加粗样式组成,数据位有8个代表一个字节的8位。右图数据位9位多了一个奇偶校验位,跟在有效载荷(字节)后面。参数如下:
①波特率:串口通信的速率。波特率本来的意思是每秒传输码元的个数,单位是码元/s,或者直接叫波特(Baud),还有个速率叫比特率,每秒传输的比特数,单位是bit/s,或者是bps。在二进制调制下,一个码元就是一个bit,此时波特率等于比特率,单片机的串口通信基本都是二进制调制(高电平表示1,低电平表示0,一位就是1bit),所以串口的波特率经常会和比特率混用。
②起始位:标志一个数据帧的开始,固定为低电平。空闲状态为高电平,起始位产生下降沿,来告诉设备要开始发送数据了
③数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行。
④校验位:用于数据验证,根据数据位计算得来。这里串口用的是奇偶校验的数据验证方法,可以判断数据传输是否出错,如果出错可选择丢弃或者重传。
可选择三种方式,无校验、奇校验、偶校验。
奇校验,包括校验位在内的9个数据位会出现奇数个1,根据8位数据情况奇校验位补0或1,保证1的个数位奇数,接收方接收数据时,会验证数据位和校验位,检出率不高比如有两位同时出错,只校验奇偶特性是检验不出的。
偶校验同理,只能保证一定检出率。如果要更高检出率可以使用CRC校验。
⑤停止位:用于数据帧间隔,固定为高电平。也是为下一个起始位做准备(切换到高电平空闲状态)
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器。同步模式只是多了个时钟输出,只支持时钟输出不支持时钟输入,一般很少用,所以USART和UART基本相同。同步模式更多的是为了兼容别的协议或者特殊用途而设计的。
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
自带波特率发生器,最高达4.5Mbits/s。
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)。
可选校验位(无校验/奇校验/偶校验)。
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN。
STM32F103C8T6 USART资源: USART1、 USART2、 USART3。
左上角TX、RX为发送和接收引脚,SW_RX、IRDA_OUT/IN是智能卡和IrDA通信的引脚(暂时不用)。TX由发送移位寄存器发出,RX通向接收移位寄存器。
灰色部分,TDR和RDR就是串口的数据寄存器了,发送和接收的字节存在这里,两个寄存器占用同一个地址(与51单片机串口的SBUF寄存器一样),所以在程序上只表现为一个寄存器DR。发送数据寄存器只写,接收数据寄存器只读。
比如某时刻在TXD里写入0x55,二进制为01010101,此时硬件检测到写入数据了,就会检查当前发送移位寄存器是否有数据在移位,如果没有那么01010101就会全部移动到寄存器准备发送,这时会置一个标志位TXE(TX Empty)发送寄存器空,检查这个标志位如果置1了,就可在TDR写入下一个数据(注意TXE置1时数据其实还没发送出去),然后发送移位寄存器在下面发送器控制的驱动下开始向右移位,一位一位地把数据送到TX引脚(向右移位与串口规定的低位先行是一致的)。
图下半部分主要是控制部分和增强功能。硬件数据流控,也叫硬件流控制,简称流控,之前介绍过,引脚nRTS(request to send)是请求发送,为输出脚,接到外部设备的CTS用于告诉外部设备现在自己是否可以接收;引脚nCTS(clear to send)是清除发送,是输入脚,接到外部设备的RTS用于接收别人nRTS的信号,查看别人是否可以接收(也就是要求对方也要有流控引脚)。”n“的意思是低电平有效。(一般不用,了解一下)
SCLK控制和SCLK这部分用于产生同步的时钟信号,配合发送移位寄存器输出的,发送寄存器每移位一次,同步时钟电平就跳变一个周期,时钟告诉对方我移出一位数据了,只支持输出不支持输入,所以两个USART之间不能实现同步串口通信。它的第一个用途是,兼容别的协议,比如串口加上时钟后跟SPI协议很像,有了时钟输出的串口就可兼容SPI;这个时钟也可做自适应波特率,比如接收设备不确定发送设备给的什么波特率,就可测量下此时钟周期计算得到波特率(一般不用,了解一下)
唤醒单元,作用是实现串口挂载多设备。之前说过串口一般为点对点通信(只支持两个设备,直接发收数据就行),而多设备通信在一条总线可接多个从设备,每个设备分配一个地址,通信要先进行寻址确定通信对象,再进行数据收发。唤醒单元可使其获得这种功能,在USART地址处给串口分配一个地址,当发送指定地址时此设备唤醒开始工作,当发送别的设备地址时别的设备工作,没收到地址的设备不唤醒保持沉默。
中断(输出)控制,申请中断位就是状态寄存器SR的各种标志位,其中TXE发送寄存器空和RXNE接收寄存器非空比较重要,是必要的。中断控制就是配置中断是否可以通向NVIC。
最下面为波特率发生器部分,波特率发生器其实就是分频器,APB时钟进行分频,得到发送和接收移位的时钟。时钟输入是fPCLKx(x=1或2),USART1挂载在APB2上所以为PCLK2时钟,一般72MHz;其他的USART都挂载在APB1,为PCLK1的时钟,一般为36MHz。时钟除以一个USARTDIV分频系数进行分频,USARTDIV为虚线框内为一个数值,分为DIV_Mantissa整数部分和DIV_Fractior小数部分,分频更加精准。
分频完还要再除以16,得到发送器时钟和接收器时钟,通向发送器和接收器控制部分。如果TE(TX Enable)为1,即发送器使能,发送部分的波特率有效,如果RE(RX Enable)为1,就是接收器使能,接收部分的波特率有效。
数据帧下的时钟波就是之前说过的同步时钟输出的功能,在每个数据位的中间都有一个时钟上升沿,时钟频率与数据速率一样,接收端可以在时钟上升沿处进行采样,这样可以精准定位每一位数据,时钟的最后一位(虚线)可通过LBCL位控制要不要输出,时钟的极性、相位等也可通过配置寄存器配置。
空闲帧从头到尾都是1,断开帧从头到尾都是0,它们用于局域网协议,串口用不到,可了解下。
8位字长也可以设置有无校验位,一般为了发送完整字节都选择无校验。9位字长常选择有校验。
以上为不同停止位的波形变化。STM32的串口可配置停止位长度为0.5,1,1.5,2四种,本质是时长不同。一般选择1位。
串口的输出TX比输入RX简单很多,输出就定时翻转TX引脚高低电平;但是输入要保证采样频率和波特率一致,还要保证每次输入采样的位置,要正好处于每一位正中间,这样读进来的电平最可靠,另外输入要对噪声有一定判断能力,如果是噪声最好能置个标志位提醒一下。
当输入电路侦测到一个数据帧的起始位后,就会以波特率的频率连续采样一帧数据,同时从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐后面就肯定对齐了。
首先输入电路对采样时钟进行了细分,会以波特率的16倍频率进行采样,即在1位的时间里进行16次采样。策略是最开始空闲状态高电平,那采样就一直是1,在某个位置突然采到0就说明在两次采样之间出现了下降沿,如果没有任何噪声那么之后应该就是起始位了,在起始位会进行连续16次采样,没有噪声的话16次都是0。
实际上会有噪声,所以即使出现下降沿后续也要再采样几次,以防万一,根据手册接收电路还会在下降沿出现之后的第3次、5次、7次进行一批采样,在第8次、9次、10次再进行一批采样,且这两批采样都要要求每三位里面至少有2个0(如图),才算检测到起始位。如果三位都是0那就无噪声,检测到起始位;如果有两个0,也算检测到起始位但会在状态寄存器里会置一个NE(Noise Error)噪声标志位,用于提醒数据是收到了但是有噪声,用的时候注意下;如果三位只有一个0,就不算检测到起始位,重新捕捉下降沿。
如果通过了起始位侦测,接收状态就由空闲变为接收起始位,同时第8、9、10次采样的位置正好是起始位的正中间,之后接收数据位时就都在第8、9、10次进行采样,保证再正中间。
从1到16是一个数据位的时间长度,即一个数据位有16个采样时钟,由于起始位侦测已经对齐了采样时钟,所以这里就直接在第8、9、10次采样数据位。为了保证数据可靠性连续采样3次(8、9、10),如果没有噪声时,3次都为1或0,那么就认为采样的为1或0;如果有噪声造成3次中既有0也有1,按照2:1规则来,按出现2次的,这种情况NE噪声标志位也会置1。
比如要配置USART为9600的波特率,那么BRR寄存器的配置为:代入公式9600=72M/(16xDIV),解得DIV=468.75,二进制数为111010100.11,所以写入DIV整数部分的是111010100(前面未满的位补零),DIV小数部分写入的是11(后面未满的位补零)。不过用库函数就很方便。
Serial.c
#include "stm32f10x.h" // Device header
//1、开启时钟,打开USART和GPIO时钟都打开
//2、GPIO初始化,把TX配置成复用输出,RX配置为输入(本代码没用到接收功能)
//3、配置USART,使用一个结构体即可配置所有相关参数
//4、如果只需要发送功能,直接开启USART初始化就结束了
//(5、如果需要接收功能,可能需要配置中断,在开启USART之前加上ITConfig和NVIC的代码就行)
//初始化完成后,如果需要发送数据调用一个发送函数就行,接收数据同理,如果要获取发送和接收标志位也是调用一个函数
void Serial_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//USART1为APB2总线上的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP ;//复用推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//供串口外设TX脚使用
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate= 9600;//Init函数内部会自动计算需要的分频系数写入BRR寄存器
USART_InitStructure.USART_HardwareFlowControl= USART_HardwareFlowControl_None;//无流控
USART_InitStructure.USART_Mode= USART_Mode_Tx;//输出发送
USART_InitStructure.USART_Parity= USART_Parity_No;//Odd奇、Even偶、No无校验
USART_InitStructure.USART_StopBits= USART_StopBits_1;//停止位长度
USART_InitStructure.USART_WordLength= USART_WordLength_8b;//字长8位
USART_Init(USART1,&USART_InitStructure);
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART1,Byte);
while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);//等待置1
//该标志位不需要手动清零,下一次SendData这个标志位会自动清零
}
void Serial_SendArray(uint8_t* Array,uint16_t Length){
//第一个参数为uint8_t*类型指向数组首地址,传递数组需要用到指针
//第二个参数由于数组无法判断是否结束,用Length
uint16_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char* String){//uint8_t也可以,由于字符串自带结束标志位0,不用Length了
uint8_t i;
for(i=0;String[i] != '\0';i++){//可以把字符串当作一个数组
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X,uint32_t Y){//次方函数X**Y
uint32_t Result = 1;
while(Y--){
Result *= X;
}
return Result;
}
void Serial_SendNumer(uint32_t Number,uint8_t Length){
//首先要把需要发送的Number的个位、十位、百位、千位等每一位以十进制拆分开
//然后转换为字符数字对应的数据,依次发送出去
//以十进制拆分:比如一个数字为1234,取千位1:1234/10**3=1.234;1.234%10=1
uint8_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+0x30);
//之所以是Length-i-1是因为从高位开始取的(如1234依次取千位、百位、十位、个位依次发送)
//之所以+0x30是因为要用字符(文本)显示,ASCII码表里字符0对应0x30。其实也可写成+'0'
}
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int main(void){
OLED_Init();
Serial_Init();
Serial_SendByte(0x22);
//程序的逻辑是上电后,初始化串口,再利用STM32串口发送一个0x22。
//调用Serial_SendByte(0x22)后TX引脚会产生一个0x22对应的波形,这个波形可以发送给其他支持串口的模块
//这里使用USB转串口模块发送给电脑端,需要一个串口助手软件查看
uint8_t MyArray[]={0x42,0x43,0x44,0x45};
Serial_SendArray(MyArray,4);
Serial_SendString("Hello world!\n");
Serial_SendNumer(1234 , 4);
while(1){
}
}
串口助手软件里接收模式选择HEX模式,就是以原始数据的形式显示,如发送0x41,显示为41;如果想显示字符串可选择文本模式,以字符形式显示,如发送0x41显示为A。
HEX模式也叫做十六进制模式或二进制模式,以原始数据显示,只能显示一个个十六进制数;
文本模式也叫字符模式,通过原始数据查找字符集编码成一个字符,如ASCII码表是最简单的字符集,如果想显示和存储汉字的话需要汉字字符集,如GB2312、GBK、GB18030等。为了标准化不同国家的字符集,出现了Unicode字符集,Unicode最常用的传输形式是UTF8。
0x41到A叫做译码,A到0x41叫做编码。
Serial.c
#include "stm32f10x.h"
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void Serial_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
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_Init(USART1,&USART_InitStructure);
//对于串口接收可使用查询和中断两种方法,如果使用查询到此初始化就结束
//查询流程:在主函数里不断判断RXNE标志位,置1说明收到数据了,
//再调用USART_ReceiveData读取DR(与上面的Serial_SendByte类似)
//下面我们程序中实现下中断的方法:
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启RXNE标志位到NVIC的输出
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//NVIC分组
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通道
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte){
USART_SendData(USART1,Byte);
while (USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}
void Serial_SendArray(uint8_t* Array,uint16_t Length){
uint16_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char* String){
uint8_t i;
for(i=0;String[i] != '\0';i++){
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X,uint32_t Y){
uint32_t Result = 1;
while(Y--){
Result *= X;
}
return Result;
}
void Serial_SendNumer(uint32_t Number,uint8_t Length){
uint8_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Number/Serial_Pow(10,Length-i-1)%10+0x30);
}
}
//RXNE标志位一但置1,就会向NVIC申请中断,之后就会在中断函数里接收数据
//其实就是在中断里面对数据进行了一次转存,最终还要扫描查询RxFlag来接收数据
//放在这里转运一个字节意义看着不大,但是为下节多字节数据包接收作铺垫
void USART1_IRQHandler(void){//中断函数名是固定的
if (USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET){
Serial_RxData= USART_ReceiveData(USART1);
Serial_RxFlag= 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
//标志位,如果读取了DR就会自动清零,如果没读取DR,就需要手动清零。这里清零下也不影响
}
}
uint8_t Serial_GetRxFlag(void){//实现读后自动清除功能
if(Serial_RxFlag == 1){
Serial_RxFlag=0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void){//封装接收数据函数
return Serial_RxData;
}
main.c
//main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void){
OLED_Init();
OLED_ShowString(1,1,"RxData:");
Serial_Init();
while(1){
if (Serial_GetRxFlag()==1){
RxData =Serial_GetRxData();//接收数据
Serial_SendByte(RxData);//把接受到的这一数据回传到电脑
OLED_ShowHexNum(1,8,RxData,2);
}
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。