第08章 USART串口
8.1 USART串口协议
8.1.1 通信接口
在STM32中,集成了很多用于通信的外设模块,比如下表所列。
通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统。
针对STM32内部没有的功能,比如蓝牙无线遥控的功能、陀螺仪加速度计测量姿态的功能,这些功能STM32没有,所以只能外挂芯片来完成。外挂的芯片,它的数据都在STM32外面。STM32获取这些数据就需要我们在这些设备之间,连接上一根或多跟通信线,通过通信线路发送或者接收数据,完成数据交换,从而实现控制外挂模块和读取1外挂模块数据的目的。
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
USART | TX、RX | 全双工 | 异步 | 单端 | 点对点 |
I2C | SCL、SDA | 半双工 | 同步 | 单端 | 多设备 |
SPI | SCLK、MOSI、MISO、CS | 全双工 | 同步 | 单端 | 多设备 |
CAN | CAN_H、CAN_L | 半双工 | 异步 | 差分 | 多设备 |
USB | DP、DM | 半双工 | 异步 | 差分 | 点对点 |
(1) USART
TX:Transmit Exchange,数据发送脚
RX:Receive Exchange,数据接收脚
(2) I2C
SCL:Serial Clock,时钟
SDA:Serial Data,数据
(3)SPI
SCLK:Serial Clock,时钟
MOSI:Master Output Slave Input,主机输出数据脚
MISO:Master Input Slave Output,主机输入数据脚
CS:Chip Select,片选,用于指定通信的对象
(4)CAN
CAN_H、CAN_L:两个引脚是差分数据脚,用两个引脚表示一个差分数据。
(5)USB
差分数据脚。
DP(D+):Data Positive
DM(D-):Data Minus
(6)全双工
指通信双方能够同时进行双向通信,一般来说,全双工的通信都有两根通信线,比如串口,一根TX发送,一根RX接收;SPI,一根MOSI发送,一根MISO接收。发送线路和接收线路互不影响,全双工。
(7)半双工
I2C、CAN、USB都只有一根数据线,CAN和USB两根差分线也是组合成为一根数据线的,就是半双工。
(8)单工
数据只能从一个设备到另一个设备,而不能反着来,比如把串口的RX去掉,就退化成单工了。
(9)时钟
发送一个波形,高电平然后低电平,接收方怎么知道你是1、0还是1、1、0、0呢?这就需要有一个时钟信号来告诉接收方,什么时候需要采集数据,时钟特性分为同步和异步。I2C和SPI有单独的时钟线,所以它们是同步的,接收方可以在时钟信号的指引下进行采样。剩下的串口、CAN和USB没有时钟线,所以需要双方约定一个采样频率,这就是异步通信,并且还要加一些帧头帧尾等等,进行采样位置的对齐。
(10)电平特性
USART、I2C、SPI都是单端信号,也就是它们引脚的高低电平都是对GND的电压差,所以单端信号通信的双方必须要共地,就是把GND接在一起,所以它们通信的引脚,还应该加一个GND引脚,不接GND是没法通信的。
CAN和UBS是差分信号,是靠两个差分引脚的电压差来传输信号的,是差分信号,在通信的时候可以不需要GND。不过USB协议里面也有一些地方需要单端信号,所以USB还是需要共地的。使用差分信号可以极大地提高抗干扰特性,所以差分信号一般传输速度和距离都会非常高,性能也是很不错的。
(11)设备特性
串口和USB属于点对点的通信,中间三个是可以在总线上挂载多个设备的。点对点通信就相当于老师找你去办公室谈话,只有两个人,直接传输数据就可以了。多设备就相当于老师在教室里,面对所有同学谈话,需要有一个寻址的过程,以确定通信的对象。
8.1.2 串口通信
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
在单片机领域,串口其实是一种最简单的通信接口,它的协议相比I2C、SPI等,已经是非常简单的了,而且一般单片机,它里面都会有串口的硬件外设,使用也是非常方便的。一般串口都是点对点的通信,所以是两个设备之间的互相通信。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力。
单片机和电脑通信,是串口的一大优势,可以接电脑屏幕、非常适合调试程序、打印信息。像I2C、SPI这些,一般都是芯片之间的通信,不会接在电脑上。
上图第一个是USB转串口模块, 上面有个芯片,型号是CH340,这个芯片可以把串口协议转换为USB协议。它一边是USB口,可以插在电脑上,另一边是串口的引脚,可以和支持串口的芯片接在一起。这样就能实现串口和电脑的通信了。
中间这个图是一个陀螺仪传感器的模块,可以测量角速度、加速度这些姿态参数,左右各有4个引脚、一边是串口的引脚、另一边是I2C的引脚。
最右边这个图是蓝牙串口模块,下面4个脚是串口通信的引脚,上面的芯片可以和手机互联,实现手机遥控单片机的功能。那这些就是串口通信,和一些使用串口通信的模块。
8.1.3 硬件电路
简单双向串口通信有两根通信线(发送端TX和接收端RX)。
复杂一点的串口通信还有其它的引脚,比如时钟引脚、硬件流控制的引脚。这些引脚STM32的串口也有,不过我们最常用的还是简单的串口通信,也就是VCC、GND、TX、RX这4个引脚。
TX与RX要交叉连接。
TX是发送,RX是接收。
当只需单向的数据传输时,可以只接一根通信线。
当电平标准不一致时,需要加电平转换芯片。
串口也是有很多电平标准的,像我们这种直接从控制器里出来的信号,一般都是TTL电平,相同的电平才能互相通信。不同的电平信号,需要加一个电平转换芯片,转接一下。
TX和RX是单端信号,它们的高低电平都是相对于GND的,所以严格上来说GND也算是通信线, 所以串口通信的TX、RX、GND是必须要接的。上面的VCC,如果两个设备都有独立供电,那VCC可以不接,如果其中一个设备没有供电,比如这里设备1是STM32,设备2是蓝牙串口模块,那就需要把STM32的VCC和蓝牙串口的VCC接在一起,STM32通过这根线,向右边的子模块供电。供电的电压也需要注意一下,要按照子模块的要求来。
8.1.4 电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+3.3V或+5V表示1,0V表示0;
单片机这种低压小型设备,使用的都是TTL电平。如果做设备需要其他的电平,那就再加电平转换芯片就可以了。
SS232电平:-3~-15V表示1,+3~+15V表示0;
RS232电平一般在大型的机器上使用,由于环境可能会比较恶劣,静电干扰比较大,所以这里电平的电压都比较大,而且允许波动的范围很大。
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)。
这里电平参考是两线压差,所以RS485的电平是差分信号,差分信号抗干扰能力非常强。使用RS485电平标准,通信距离可以达到上千米。而上面两种电平,最远距离只能达到几十米,再远就传不了了。
在软件层面,它们都属于串口,所以程序并不会有什么变化。
8.1.5 串口参数及时序
波特率:串口通信的速率;
串口一般是使用异步通信,所以双方需要约定一个通信速率,比如每个1s发送一位,那就得每隔1s接收一位,如果接收快了,就会重复接收某些位;如果接收慢了,就会漏掉某些位,所以说发送和接收必须约定好速率。这个速率参数,就是波特率。波特率本来的意思是,每秒传输码元的个数,单位是码元/s,或者直接叫波特(Baud)。另外还有一个速率表示,叫比特率,比特率的意思是每秒传输的比特数。单位是bit/s或者叫bps。在二进制调制的情况下,一个码元就是一个bit,此时波特率就等于比特率。像我们单片机的串口通信,基本都是二进制调制,也就是高电平表示1、低电平表示0、1位就是1bit,所以说这个串口的波特率经常和比特率混用。如果是多进制调制,那波特率和比特率就不一样了。反映到波形上,比如我们双方规定波特率是1000bps,那就表示1s要发1000位,每一位的时间就是1ms,发送方每隔1ms发送一位,接收方每隔1ms接收一位。这就是波特率,它决定了每隔多久发送一位。
起始位:标志一个数据帧的开始,固定为低电平;
首先,串口的空闲状态是高电平,也就是没有数据传输的时候,引脚必须要置高电平,作为空闲状态,然后需要传输的时候,必须要先发送一个起始位,这个起始位必须是低电平,来打破空闲状态的高电平,产生一个下降沿。这个下降沿,就告诉接收设备,这一帧的数据要开始了。如果没有起始位,那当我发送8个1的时候,数据线一直处于高电平,没有任何波动,这样接收方无法知道我们发送数据了。所以这里必须要有一个固定为低电平的起始位,产生下降沿,来告诉接收设备,我要发送数据了。同理在一个数据发送完成后,必须要有一个停止位。
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行;
比如想要发送一个字节是0x0F,那就把0x0F转换成二进制,就是0000 1111。然后低位先行,所以数据要从低位开始发送,也就是11110000这样,依次放在发送引脚上。
数据位有两种表示方法,一种是把校验位作为数据位的一部分,就像下图所示的时序一样,分为8位数据和9位数据,其中9位数据就是8位有效载荷和1位置校验位。另一种就是把校验位和数据位独立开,数据位就是有效载荷,校验位就是独立的1位。
校验位:用于数据验证,根据数据位计算得来;
这里串口使用的是一种叫做奇偶校验的数据验证方法,奇偶校验可以判断数据传输是不是出错了,如果数据出错了,可以选择丢弃或者要求重传。校验可以选择3种方式,无校验、奇校验和偶校验。无校验就是不需要校验位,波形就是下图左边这个。奇校验和偶校验的波形就是下图右边这个,起始位、数据位、校验位、停止位4个部分。如果使用了奇校验,那么包括校验位在内的9位数据会出现奇数个1。比如如果传输0000 1111,目前总共4个1,是偶数个,那么校验位就需要再补一个1,连通校验位就是0000 11111,总共5个1,保证1为奇数;如果数据是0000 1110,此时3个1,是奇数个,那么校验位就补一个0,连同校验位就是0000 11100,总共3个1,1的个数为奇数。发送方在发送数据后,会补一个校验位,保证1的个数为奇数;接收方在接收数据后,会验证数据位和校验位,如果1的个数还是奇数,就认为数据没有出错;如果在传输中、因为干扰、有一位由1变为0或者由0变成1了。那么整个数据的奇偶特性就会变化,接收方一验证,发现1的个数不是奇数,那就认为传输出错,就可以选择丢弃或者要求重传。这就是奇校验的差错控制方法。若果选择双方约定偶校验,那就是保证1的个数是偶数,检验方法也是一样的道理,但是奇偶校验的检出率并不是很高。比如如果有两位数据同时出错,奇偶特性不变,就校验不出来了。所以奇偶校验只能保证一定程度上的数据校验。如果想要更高的检出率,可以了解一下CRC校验。
停止位:用于数据帧间隔,固定为高电平。
同时这个停止位,也是为下一个起始位做准备的,如果没有停止位,那当我最后一位数据为0的时候,下次再发送新的一帧,就没办法产生下降沿了。这就是停止位的作用。起始位固定为0,产生下降沿、表示传输开始;停止位固定为1,把引脚恢复成高电平,方便下一次的下降沿,如果没有数据了,正好引脚也为高电平,代表空闲状态。
上面两个图就是串口发送一个字节的格式, 这个格式是串口协议规定的.串口中每一个字节都装载在一个数据帧里面,每个数据帧都有起始位、数据位和停止位组成。数据位有8个,代表一个字节的8位。在右边的数据帧里面,还可以在数据位的最后,加一个奇偶校验位,这样数据位总共就是9位,其中有效载荷是前8位、代表1个字节,校验位跟在有效载荷后面,占1位。这就是串口数据帧的整体结构。
8.1.6 串口通信实际波形
8.2 USART串口外设
8.2.1 USART简介
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器;
UART:异步收发器。
一般我们串口很少使用同步功能,所以USART和UART使用起来没有什么区别。STM32的USART同步模式,只是多了个时钟输出而已,它只支持时钟输出,不支持时钟输入,所以这个同步模式更多的是为了兼容别的协议或者特殊用途而设计的。并不支持两个USART之间进行同步通信。所以我们学习串口,主要还是异步通信。
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里;
USART外设就是串口通信的硬件支持电路。USART大体可以分为发送和接收两部分,发送部分就是将数据寄存器的一个字节数据,自动转换为协议规定的波形,从TX引脚发送出去,接收部分就是自动接收RX引脚的波形,按照协议规定,解码一个字节数据,存放在数据寄存器里,这就是USART电路的功能。当我们配置好了USART,直接读写数据寄存器,就能自动发送和接收数据了。
自带波特率发生器,最高达4.5Mbits/s;
这个波特率发生器是用来配置波特率的,它其实就是一个分频器,比如我们APB2总线给个72MHz的频率,然后波特率发生器进行一个分频,得到我们想要的波特率时钟。最后在这个时钟下,进行收发,就是我们指定的通信波特率。
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2);
这些就是STM32 SUART支持配置的参数了。
数据位长度就是上图所示参数,有8位和9位,9位是包含奇偶校验位的长度。一般不需要校验就选8位,需要校验就选9位。
在进行连续发送时,停止位长度决定了帧的间隔。我们最常用的是1位停止位,其它的很少用。
可选校验位(无校验/奇校验/偶校验);
最常用的是无校验。
以上所有的参数,都是可以通过配置寄存器来完成的。使用库函数就更简单了,直接给结构体赋值就行。
串口参数我么最常用的就是波特率(9600或115200)、数据位8位、停止位1位、无校验。一般我们都选择这种常用的参数。
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN;
同步模式就是多了个时钟CLK的输出。
硬件流控制:比如这个A设备有个TX通向B设备的RX发送数据,A设备一直在发,发的太快,B处理不过来,如果没有硬件流控制,那B就只能抛弃新数据或者覆盖原数据了。如果有硬件流控制,在硬件电路上,会多出一根线,如果B没准备好接收,就置高电平,如果准备好了,就置低电平,A接收到了B反馈的准备信号,就只会在B准备好的时候,才发数据。如果B没准备好,那数据就不会发送出去。这就是硬件流控制,可以防止因为B处理慢而导致数据丢失的问题。硬件流控制,STM32也是有的,不过我们一般不用。
DMA:这个串口支持DMA数据转运,如果有大量的数据进行收发,可以使用DMA转运数据,减轻CPU的负担。
智能卡、IrDA、LIN:这些是其它的一些协议。因为这些协议和串口非常的像,所以STM32就对USART加了一些小改动,就能兼容这么多的协议了。不过我们一般不用。智能卡应该是跟我们刷的饭卡、公交卡这些有关的;IrDA是用于红外通信的,这个红外通信就是一个红外发光管,另一边是红外接收管,靠闪烁红外光通信,并不是遥控器的那个红外通信,所以并不能模拟遥控器。LIN是局域网的通信协议。
STM32F103C8T6 USART资源: USART1、 USART2、 USART3。
总共三个独立的USART外设,可以挂载很多串口设备。USART1是APB2总线上的设备,剩下的收拾APB1总线上的设备,开启时钟的时候注意一下。在使用的时候,挂载哪个总线,影响并不是很大。
8.2.2 USART框图
左上角的引脚部分有TX和RX,这两个就是发送和接收引脚;下面的SW_RX、IRDA_OUT/IN这些就是智能卡和IrDA通信的引脚,我们不用这些引脚,所以这些引脚可以不用管。右边的IrDA、SIR这些东西也都不用管。引脚这块,TX发送脚、就是从发送移位寄存器接出去的,RX接收脚,就是从接收移位寄存器接出去的。
右上角整体这一块就是串口的数据寄存器了,发送或接收的字节就存在这里。上面有两个数据寄存器,一个是发送数据寄存器TDR(Transmit DR),另一个是接收数据寄存器RDR(Receive DR)。这两个寄存器占用同一个地址,就跟51单片机串口的SBUF寄存器一样。在程序上只表现为一个寄存器,就是数据寄存器DR(Data Register),但实际硬件中,是分成了两个寄存器,一个用于发送TDR,一个用于接收RDR,TDR是只写的,RDR是只读的。当进行写操作时,数据就写到TDR;当进行读操作时,数据就是从RDR读出来的。下面是两个移位寄存器,一个用于发送,一个用于接收,发送移位寄存器的作用就是把一个字节的数据一位一位地移出去,正好对应串口协议地波形地数据位。发送数据寄存器(TDR)和发送移位寄存器是怎么工作的呢?举个例子:比如在某个时刻给TDR写入了0x55这个数据,在寄存器里就是二进制存储,0101 0101,那么此时硬件检测到写入的数据,它就会检查当前移位寄存器是不是有数据正在移位,如果没有这个0101 0101就会立刻全部移动到发送移位寄存器,准备发送。当数据从TDR移动到移位寄存器时,会置一个标志位,叫TXE(TX Empty),发送寄存器空。我们检查这个标志位,如果置1了,就可以在TDR写入下一个数据了;当TXE标志位置1时,数据其实还没有发送出去,只要数据从TDR转移到发送移位寄存器了,TXE就会置1,我们就可以写入新的数据了,然后发送移位寄存器就会在下面这里的发生器控制的驱动下,向右移位,然后一位一位地,把数据输出到TX引脚,这里是向右移位地,所以正好和串口协议规定的低位先行,是一致的,当数据移位完成后,新的数据就会再次自动地从TDR转移到发送移位寄存器里来,如果当前移位寄存器移位还没有完成,TDR的数据就会进行等待,一但移位完成,就会立刻转移过来,有了TDR和移位寄存器的双重缓存,可以保证连续发送数据的时候,数据帧之间不会有空闲,提高了工作效率。简单来说,就是数据一旦从TDR转移到了移位寄存器里,管你有没有完成,我就立刻把下一个数据放在TDR等着,一旦移完了,新的数据就会立刻跟上。接收端这里数据也是类似的,数据从RX引脚通向接收移位寄存器,在接收器控制的驱动下,一位一位地读取RX电平,先放在最高位,然后向右移,移位8次之后就能接收一个字节了,同样,因为串口协议规定是低位先行,所以接收移位寄存器是从高位往低位这个方向移动的,之后,当一个字节移位完成之后,这一个字节的数据,就会整体地一下子转移到接收数据寄存器地RDR里,在转移地过程中,也会置一个标志位,叫RXNE(RX Not Empty),接收数据寄存器非空,当我们检测到RXNE置1之后,就可以把数据读走了,同样这里也是两个寄存器进行缓存,当数据从移位寄存器转移到RDR时,就可以直接移位接收下一帧数据了。这就USART外设整个的工作流程。
发送器控制是用来控制发送移位寄存器的工作的;接收器控制,用来控制接收移位寄存器的工作。左边有一个硬件数据流控,也就是硬件流控制,简称流控。流控有两个引脚,一个是nRTS,一个是nCTS;nRTS(Request To Send)是请求发送,是输出脚,也就是告诉别人,我当前能不能接收,nCTS(Clear To Send)是清除发送,是输入脚,也就是用于接收别人nRTS的信号的,前面加个n意思是低电平有效。这两个引脚是如何工作的呢?首先得找另一个支持流控得传偶,它的TX接到我的RX,然后我的RTX要输出一个能不能接收得反馈信号,接到对方得CTS,当我能接收的时候,RTS就置低电平,请求对方发送,对方得CTS接收到之后,就可以一直发,当我处理不过来时,比如接收数据寄存器我一直没有读,又有新的数据过来了,现在就代表我没有及时处理,那RTS就会置高电平,对方CTS接收到之后,就会暂停发送,直到这里接收数据寄存器被读走,RTS置低电平,新的数据才会继续发送。那反过来,当我的TX给对方发送数据时,我们的CTS就要接到对方的RTS,用于判断对方,能不能接收,TX和CTS是一对的,RX和RTS是一对的,CTS和RTS也要交叉连接,这就是流控的工作模式。当然我们一般不使用流控。
SCLK控制用于产生同步的时钟信号,它是配合发送移位寄存器输出的,发送移位寄存器每移位依次,同步时钟电平就跳变一个周期,时钟告诉对方,我移出去一位数据了,你看要不要我这个时钟信号来指导你接收一下。当然这个时钟只支持输出,不支持输入。所以两个USART之间,不能实现同步的串口通信。那这个时钟信号有什么用呢?第一个用途就是兼容别的协议,比如串口加上时钟之后,就跟SPI协议特别像,所以有了时钟输出的串口,就可以兼容SPI,另外这个时钟也可以做自适应波特率,比如接收设备不确定发送设备给的什么波特率,那就可以测量一下这个时钟的周期,然后再计算得到波特率,不过就需要另外写程序来实现这个功能了。这个时钟功能我们一般不用。
唤醒单元:这部分的作用是实现串口挂载多设备,串口一般是点对点的通信,点对点只支持两个设备互相通信,想法数据直接发就行;而多设备,在一条总线上,可以接多个从设备,每个设备分配一个地址,我想跟某个设备通信,就先进行寻址,确定通信对象,再进行数据首收发。这个唤醒单元就可以用来实现多设备的功能,在这里可以给串口分配一个地址,当发送指定地址时,此设备唤醒开始工作,发送别的都设备地址时,别的设备就唤醒工作,这个设备没受到地址,就会保持沉默,这样就可以实现多设备的串口通信了。这部分功能我们一般不用。
中断控制:中断申请位,就是状态寄存器这里的各种标志位,状态寄存器这里,有两个标志位比较重要,一个是TXE发送寄存器空,另一个是RXNE接收寄存器非空,这两个是判断发送状态和接收状态的必要标志位。中断输出控制这里,就是配置中断能不能通向NVIC
最下面是波特率发生器部分,波特率发生器其实就是分频器,APB时钟进行分频,得到发送和接收移位的时钟。这里时钟输入是fPCLKx(x=1或2),USART1挂载再APB2上,所以就是PCLK2的时钟,一般是72M,其它的USART都挂载在APB1,所以PCLK1的时钟,一般是36M。之后这个时钟进行分频,除以一个USARTDIV的分频系数,USARTDIV里面就是右边这样,是一个数值,并且分为了整数部分和小数部分,因为有些波特率,用72M除一个整数的话,可能除不尽,会有误差,所以这里分频系数是支持小数点后4位的,分频就更加精准,之后分频完之后,还要再除一个16,得到发送器时钟和接收器时钟,通向控制部分。然后右边,如果TE为1,就是发送器使能了,发送部分的波特率就有效,如果RE(RX Enable)为1,就是接收器使能了,接收部分的波特率就有效。
8.2.3 USART基本结构
最左边是波特率发生器,用于产生约定的通信频速率,时钟来源是PCLK2或1,经过波特率发生器分频后,产生的时钟通向发送控制器和接收控制器。发送控制器和接收控制器用来控制发送移位和接受移位,之后由发送数据寄存器和发送移位寄存器这两个寄存器的配合,将数据一位一位的移出去,通过GPIO口的复用输出,输出到TX引脚,产生串口协议规定的波形,这里画了几个右移的符号,就是代表这个移位寄存器是往右移的,是低位先行,当数据由数据寄存器转移到移位寄存器时,会置一个TXE的标志位,我们判断这个标志位,就可以知道是不是可以写下一个数据了。接收部分也是类似,RX引脚的波形,通过GPIO输入,在接收控制器的控制下,一位一位地移入接收移位寄存器,这里画了右移的符号,也是右移的,因为是低位先行,所以要从左边开始移进来,移完一帧数据后,数据就会统一转运到接收数据寄存器,在转移的同时,置一个RXNE标志位,我们检查这个标志位,就可以知道是不是收到数据了。同时这个标志位也可以去申请中断,这样就可以在收到数据时,直接进入中断函数,然后快速地读取和保存数据。右边实际上有4个寄存器,但是在软件层面,只有1个DR寄存器可以供我们读写,写入DR时,数据走上面这条路,进行发送,读取DR时,数据走下面这条路,进行接收。这就是USART进行串口数据收发的过程。右下角是一个开控制,就是配置完成之后,用Cmd开启一下外设。
8.2.4 数据帧
8.2.5 起始位侦测
这个图以及下一个图展示的是USART电路输入数据的一些策略。 对于串口来说,根据前面的介绍,串口的输出TX应该是比输入RX简单得多,输出就定时翻转TX引脚高低电平就行了。输入就复杂一些,不仅要保证输入得采样频率和波特率一致,还要保证每次输入采样的位置,要正好处于每一位的正中间,只有在每一位的正中间采样,这样的高低电平读出来,才是最可靠的,如果采样点过去考前或靠后,那有可能高低电平还正在翻转,电平还不稳定,或者稍有误差,数据就采样错了。另外,输入最好还要对噪声有一定的判断能力,如果是噪声,最好能置个标志位提醒我一下,这些就是输入数据所面临的问题。
上图:当输入电路侦测到一个数据帧的起始位之后,就会以波特率的频率,连续采样一帧数据,同时,从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐了,后面的肯定都是对齐的。为了实现这样的功能,首先输入的这部分电路对采样时钟进行了细分,它会以波特率的16倍频率进行采样,也就是在一位的时间里,可以进行16次采样。它的策略是,最开始空闲状态高电平,那采样就一致是1,在某一个位置,突然采样一个0,那说明在这两次采样之间,出现了下降沿,如果没有任何噪声,那之后应该就是起始位了。在起始位,会进行连续16次采样,没有噪声的话,这16次采样,肯定都是0,这没问题。但是实际电路还是会存在一些噪声的,所以这里即使出现下降沿了,后续也要再采样几次,以防万一。根据手册描述,这个接收电路还会在下降沿之后的第3次、5次、7次进行一批采样,在第8次、9次、10次再进行一批采样,且这两批采样,都要要求每3位里面至少有2个0,如果没有噪声,那肯定全是0,满足情况。如果有一些轻微的噪声,导致这里3位里面,有两个0,另一个是1,那也算是检测到了起始位,但是在状态寄存器里会置一个NE(Noise Error),噪声标志位,就是提醒你一下,数据我是收到了,但是有噪声,你悠着点用。如果这三位里面,只有1个0,那就不算检测到了起始位,可能前面那个下降沿是噪声导致的,这是电路就忽略前面的数据,重新开始捕捉下降沿。这就是STM32的串口,在接收过程中,对噪声的处理。如果通过了这个起始位侦测,那接收状态就由空闲,变为接收起始位,同时,第8、9、10次采样的位置,就正好是起始位的正中间,之后接收数据位时,就都在第8、9、10次进行采样,这样就能保证采样位置在位的正中间了。这就是起始位侦测和采样位置对齐的策略。
8.2.6 数据采样
在一个数据位,有16个采样时钟,由于起始位侦测已经对齐了采样时钟,所以这里就直接在第8、9、10次采样数据位。为了保证数据的可靠性、这里连续采样3次,没有噪声的理想情况下,这3次肯定全为1或者全为0,全为1就认为收到了1,全为0就认为收到了0。如果有噪声,导致3次采样不是全为1或者全为0,那就按照2:1的规则来。在这种情况下,噪声标志位NE也会置1,告诉你,我收到数据了,但是有噪声,悠着点用。
8.2.7 波特率发生器
波特率发生器就是分频器。
发送器和接收器的波特率由波特率寄存器BRR里的DIV确定;
DIV分为整数部分和小数部分,可以实现更细腻的分频。
计算公式:波特率 = fPCLK2/1 / (16 * DIV)。
举例:如果我要配置USART1为9600的波特率,那如何配置这个BRR寄存器呢?带入公式:
转换成二进制后,最终写入寄存器的就是0001 1101 0100.1100。
使用库函数配置的话,直接写波特率就可以了。
8.3 串口发送
8.3.1 硬件电路
跳线帽插在VCC和3V3这两个引脚上,选择通信的TTL电平为3.3V。
8.3.2 驱动安装
端口目录下,有这个CH340的驱动, 如果出现了COM号,并且前面图标没有感叹号,那就证明串口驱动没有问题,否则需要安装一下串口模块的驱动。
8.3.3 软件部分
(1)复制《OLED显示屏》工程并改名为《串口发送》
(2)添加驱动文件
(3)相关库函数
void USART_DeInit(USART_TypeDef* USARTx);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
/*下面两个时钟是用来配置同步时钟输出的,包括时钟是不是要输出,时钟的极性相位等参数*/
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
/*开启USART到DMA的触发通道*/
void USART_SetAddress(USART_TypeDef* USARTx, uint8_t USART_Address);
void USART_WakeUpConfig(USART_TypeDef* USARTx, uint16_t USART_WakeUp);
void USART_ReceiverWakeUpCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_LINBreakDetectLengthConfig(USART_TypeDef* USARTx, uint16_t USART_LINBreakDetectLength);
void USART_LINCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data); // 发送数据
uint16_t USART_ReceiveData(USART_TypeDef* USARTx); // 接收数据
void USART_SendBreak(USART_TypeDef* USARTx);
void USART_SetGuardTime(USART_TypeDef* USARTx, uint8_t USART_GuardTime);
void USART_SetPrescaler(USART_TypeDef* USARTx, uint8_t USART_Prescaler);
void USART_SmartCardCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_SmartCardNACKCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_HalfDuplexCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_OverSampling8Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_OneBitMethodCmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_IrDAConfig(USART_TypeDef* USARTx, uint16_t USART_IrDAMode);
void USART_IrDACmd(USART_TypeDef* USARTx, FunctionalState NewState);
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
(4)Serial.c
#include "stm32f10x.h" // Device header
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx; //串口模式设置为发送模式
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
(5)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
#endif
(6)mian.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
// Serial_SendByte(0x41);
// Serial_SendByte('A');
// uint8_t MyArray[] = {0x42,0x43,0x44,0x45};
// Serial_SendArray(MyArray,4);
// Serial_SenString("HelloWorld!\r\n"); //使用"\r\n"来进行换行
Serial_SendNumber(12345,5);
while(1)
{
}
}
8.3.4 数据模式
HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
文本模式/字符模式:以原始数据编码后的形式显示
8.3.5 printf函数的移植方法
8.3.5.1 方法1
MicaoLIB是Keil为嵌入式平台优化的一个精简库, 使用printf函数就可以用这个MicroLIB。还需要对printf函数进行重定向,将printf函数打印的东西输出到串口。因为printf默认是输出到屏幕,我们单片机没有屏幕,所以要进行重定向,步骤就是在串口模块里加上:
#include <stdio.h>
之后,重写fputc函数。
(1)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx; //串口模式设置为发送模式
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
(2)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
#endif
(3)main.h
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
// Serial_SendByte(0x41);
// Serial_SendByte('A');
// uint8_t MyArray[] = {0x42,0x43,0x44,0x45};
// Serial_SendArray(MyArray,4);
// Serial_SenString("HelloWorld!\r\n"); //使用"\r\n"来进行换行
// Serial_SendNumber(12345,5);
printf("Num=%d\r\n",666);
while(1)
{
}
}
8.3.5.1 方法2
第一种方法,printf只能有一个,如果重定向到串口1了,那串口2再用就没有了。多个串口都想用printf,这时就可以用sprintf。sprintf可以把格式化字符输出到一个字符串里,所以这里可以先定义一个字符串。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
// Serial_SendByte(0x41);
// Serial_SendByte('A');
// uint8_t MyArray[] = {0x42,0x43,0x44,0x45};
// Serial_SendArray(MyArray,4);
// Serial_SenString("HelloWorld!\r\n"); //使用"\r\n"来进行换行
// Serial_SendNumber(12345,5);
// printf("Num=%d\r\n",666);
char String[100];
sprintf(String,"Num=%d\r\n",666); //第一个参数是指定打印输出的位置,目前"Num=%d\r\n"这个格式化的字符串在String里
/*sprintf可以指定打印位置,不涉及重定向的东西,所以每个串口都可以用sprintf进行格式化打印*/
Serial_SenString(String);
while(1)
{
}
}
8.3.5.1 方法3
sprintf每次都得先定义字符串,再打印到字符串,再发送字符串。所以可以对sprintf函数进行封装这个过程。
(1)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx; //串口模式设置为发送模式
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
void Serial_Printf(char *format,...) //format用来接收格式化字符串,后面...这部分用来接收后面的可变参数列表
{
char String[100];
va_list arg; //定义一个参数列表变量,va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表,放在arg里面
vsprintf(String,format,arg);
va_end(arg); //释放参数表
Serial_SenString(String); //把String发送出去
}
(2)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
void Serial_Printf(char *format,...);
#endif
(3)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
// Serial_SendByte(0x41);
// Serial_SendByte('A');
// uint8_t MyArray[] = {0x42,0x43,0x44,0x45};
// Serial_SendArray(MyArray,4);
// Serial_SenString("HelloWorld!\r\n"); //使用"\r\n"来进行换行
// Serial_SendNumber(12345,5);
// printf("Num=%d\r\n",666);
// char String[100];
// sprintf(String,"Num=%d\r\n",666); //第一个参数是指定打印输出的位置,目前"Num=%d\r\n"这个格式化的字符串在String里
// /*sprintf可以指定打印位置,不涉及重定向的东西,所以每个串口都可以用sprintf进行格式化打印*/
// Serial_SenString(String);
Serial_Printf("Num=%d\r\n",666);
while(1)
{
}
}
8.3.6 显示汉字的方法
8.3.6.1 utf-8不乱码的方案
这里直接写汉字,编译器有时候会报错,需要进行以下配置:
--no-multibyte-chars
8.3.6.2 直接使用GB2312编码
编写程序时,,直接使用GB2312编码,串口使用GBK输出。
8.4 串口发送+接收
8.4.1 硬件电路
8.4.2 软件部分
8.4.2.1 查询方法
适用于程序比较简单情况。
(1)复制《串口发送》工程并改名为《串口发送+接收》
(2)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //接收引脚选择上拉输入模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式设置为发送模式和接收模式同时开启
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
void Serial_Printf(char *format,...) //format用来接收格式化字符串,后面...这部分用来接收后面的可变参数列表
{
char String[100];
va_list arg; //定义一个参数列表变量,va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表,放在arg里面
vsprintf(String,format,arg);
va_end(arg); //释放参数表
Serial_SenString(String); //把String发送出去
}
(3)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
void Serial_Printf(char *format,...);
#endif
(4)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
while(1)
{
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET) //if成立,就说明收到数据了
{
RxData = USART_ReceiveData(USART1);
OLED_ShowHexNum(1,1,RxData,2);
//无需手动清零
}
}
}
(5)结果
8.4.2.2 中断方法
(1)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:开启中断
第5步:开启RXNE标志位到NVIC的输出
第6步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //接收引脚选择上拉输入模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式设置为发送模式和接收模式同时开启
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启中断*/
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //RXNE标志位一旦置1,就会向NVIC申请中断
/*第5步::开启RXNE标志位到NVIC的输出*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
/*第6步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
/*printf重定向函数*/
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
/*sprintf函数的封装*/
void Serial_Printf(char *format,...) //format用来接收格式化字符串,后面...这部分用来接收后面的可变参数列表
{
char String[100];
va_list arg; //定义一个参数列表变量,va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表,放在arg里面
vsprintf(String,format,arg);
va_end(arg); //释放参数表
Serial_SenString(String); //把String发送出去
}
/*自动清除标志位函数*/
uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除一下标志位
}
}
(2)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
void Serial_Printf(char *format,...);
uint8_t Serial_GetRxFlag(void);
uint8_t Serial_GetRxData(void);
#endif
(3)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
OLED_ShowString(1,1,"RxData:");
Serial_Init();
while(1)
{
if(Serial_GetRxFlag()== 1) //if成立,就说明收到数据了
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1,8,RxData,2);
//无需手动清零
}
}
}
(4)结果
8.5 USART串口数据包
8.5.1 HEX数据包
数据包的作用是,把一个个单独的数据给打包起来,方便我们进行多字节的数据通信。比如说,我们有一个陀螺仪传感器,需要用串口发送数据到STM32,陀螺仪的数据,比如X轴一个字节,Y轴一个字节,Z轴一个字节,总共3个数据需要连续不断地发送,当像这样,XYZXYZ连续发送的时候,就会出现一个问题,就是接收方,它不知道这数据哪个对应X、哪个对应Y、哪个对应Z,因为接收方可能会从任意位置开始接收,所以会出现数据错位的现象。这时候我们就需要研究一种方式,把这个数据进行分割,把XYZ这一批数据分割开,分成一个个数据包,这样再接收的时候,就知道了,第一个数据是X,第二个数据是Y,第三个是Z。这就是数据包的任务,就是把属于同一批的数据进行打包和分割,方便接收方进行识别。有关分割打包的方法,可以自己发挥想象力来设计,只要逻辑行得通就行。比如我可以设计,在这个XYZXYZ数据流中,数据包的第一个数据,也就是X的数据包,它的最高位置1,其余数据包,最高位都置0。当我接收到数据之后,判断一下最高位,如果是1,那就是X数据,然后紧跟着的两个数据就是YZ。这就是一种可行的分割方法,这种方法是把每个数据的最高位当作标志位来进行分割的。实际也有应用的例子,比如UTF8的编码方法,和这就是类似的,不过它那个编码更高级一些。
我们串口数据包,通常使用的是额外添加包头包尾这种方式。比如以下两种数据包格式。
包头包尾和数据载荷重复的问题:这里定义0xFF为包头,FE为包尾,如果传输的数据本身就是0x
FF和0xFE怎么办呢?这个问题确实存在,如果数据和包头包尾重复,可能会引起误判,对应这个问题,有如下几种解决方法:
第一种:限制载荷数据的范围,如果可以的话,我们可以在发送的时候,对数据进行限幅,比如XYZ3个数据,变化范围都可以是0~100,那就好办了,我们可以在载荷中只发送0~100的数据,这样就不会和包头包尾重复了。
第二种:如果无法避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要我们通过包头包尾对齐了数据,我们就可以严格知道,哪个数据应该是包头包尾,哪个数据应该是载荷数据,在接收载荷数据的时候,我们并不会判断它是否是包头包尾。而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐。这样,在经过几个数据包的对齐之后,剩下的数据包,应该就不会出现问题了。
第三种:增加包头包围的数量,并且让它尽量呈现出载荷输出出现不了的状态,比如我们使用FF、FE作为包头,FD、FC作为包尾,这样也可以避免载荷数据和包头包尾重复的情况发生。
第二个问题是,包头包尾并不是全部都需要的,比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是,一个包头FF加4个数据,当检测到FF时开始接收,收够4个字节后,置标志位,一个数据包接收完成,不过这样的话,载荷和包头重复的问题会更严重一些。
第三个问题,就是固定包长和可变包长的选择问题,对于HEX数据包来说,如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误。如果又会重复,又选择可变包长,那数据就容易乱套了。如果载荷不会和包头包尾重复,数据长度可以选择可变包长。
最后一个问题就是各种数据转换为字节流的问题,这里数据包都是一个字节一个字节组成的,如果你想发送16位的整型数据,32位的整型数据,float、double甚至是结构体,其实都没问题,因为它们内部其实都是由一个字节一个字节组成的,只需要用一个uint8_t的指针,指向它,把它们当作一个字节数组发送就行了。
(1)固定包长,包含包头包尾
每个数据包的长度不变,数据包的前面是包头,后面是包尾。
定义oxFF位包头, 在4个字节之后,加一个包尾,比如定义0xFE为包尾。当我接收到0xFF之后,我就知道了,一个数据包来了,接着我再接收到的4个字节,就当作数据包的第1、2、3、4个数据,存在一个数组里,最后跟一个包尾,当我收到0xFE之后,就可以置一个标志位,告诉程序,我收到了一个数据包。然后新的数据包过来,再重复之前的过程。这样就可以在一个连续不断的数据流中分割出我们想要的数据包了。
(2)可变包长,含包头包尾
每个数据包的长度可以是不一样的,数据包的前面是包头,后面是包尾。
数据包的格式,可以是用户根据需求,自己规定的,也可以是买一个模块,别的开发者规定的。
8.5.2 文本数据包
实际上每个文本最后其实都还是一个字节的HEX数据。在载荷数据中间可以出现除了包头包尾的任意字符。所以文本数据包基本不用担心再和包头包尾重复的问题。
文本数据包通常会以换行作为包尾,这样在打印的时候,就可以一行一行地显示了。
(1)固定包长,含包头包尾
(2)可变包长,含包头包尾
8.5.3 HEX数据包接收
每收到一个字节,程序都会进一遍中断,在中断函数里,我们可以拿到这一个字节,但拿到之后,我们就得退出中断了,所以每拿到一个数据都是一个独立的过程,而对于数据包来说,它具有前后关联性,包头之后是数据、数据之后是包尾,对于包头、数据和包尾这3种状态。我们都需要有不同的处理逻辑。所以在程序中,我们需要设计一个能够记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维,就叫做“状态机”。在这里我们就使用状态机的思维来接收一个数据包。
最开始,S=0, 收到一个数据,进中断,根据S=0,进入第一个状态的程序,判断数据是不是包头FF,如果是FF则代表收到包头,之后置S=1,退出中断,结束。这样下次再进中断,根据S=1,就可以进行接收数据的程序了。那在第一个状态,如果收到的不是FF,就证明数据包没有对齐,我们应该等待数据包包头的出现,这时候状态仍然是0,下次进中断,就还是判断包头的逻辑,直到出现FF,才能转到下一个状态。那之后,出现了FF,我们就可以转移到接收数据的状态了,这时,再收到数据,我们就直接把它存在数组中,另外再用一个变量,记录收了多少个数据,如果没收够4个数据,就一直是接收状态,如果收够了,就置S=2。下次中断时,就可以进入下一个状态了,那最后一个状态就是等待包尾了,判断数据是不是FE,正常情况下应该是FE。这样就可以置S=0回到最初的状态,开始下一个轮回。当然也有可能这个数据不是FE,比如数据和包头重复,导致包头位置判断错了,那这个包尾位置就有可能不是FE,这时候就可以进入重复等待包尾的状态,直到接收到真正的包尾,这样加入包尾的判断,更能预防因数据和包头重复造成的错误。
状态机使用的基本步骤:
先根据项目要求定义状态,画几个圈,然后考虑不好各个状态在什么情况下会进行转移,如何转移,画好线和转移条件,然后根据这个图来进行编程。
8.5.4 文本数据包接收
8.6 串口收发HEX数据包
8.6.1 硬件电路
8.6.2 软件部分
(1)复制《串口发送+接收》并改名为《串口收发HEX数据包》
(2)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
uint8_t Serial_TxPacket[4]; //用于缓存发送的数据包
uint8_t Serial_RxPacket[4]; //用于缓存接收的数据包
uint8_t Serial_RxFlag; //缓存状态标志位
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:开启中断
第5步:开启RXNE标志位到NVIC的输出
第6步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //接收引脚选择上拉输入模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式设置为发送模式和接收模式同时开启
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启中断*/
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //RXNE标志位一旦置1,就会向NVIC申请中断
/*第5步::开启RXNE标志位到NVIC的输出*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
/*第6步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
/*printf重定向函数*/
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
/*sprintf函数的封装*/
void Serial_Printf(char *format,...) //format用来接收格式化字符串,后面...这部分用来接收后面的可变参数列表
{
char String[100];
va_list arg; //定义一个参数列表变量,va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表,放在arg里面
vsprintf(String,format,arg);
va_end(arg); //释放参数表
Serial_SenString(String); //把String发送出去
}
/*自动清除标志位函数*/
uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
/*发包函数:调用这个函数,Serial_TxPacket的数据自动加上包头包尾发出去*/
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket,4);
Serial_SendByte(0xFE);
}
/*接收中断函数*/
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //状态变量,0、1、2
static uint8_t pRxPacket = 0;
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if(RxState == 0) //进入等待包头的程序
{
if(RxData == 0xFF)
{
RxState = 1; //接收到0xFF,转移状态
pRxPacket = 0; //清零,为下一次进入接收数据程序做准备
}
}
else if(RxState == 1) //进入接收数据的程序
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket++; //每进一次接收状态,数据就转一次缓存数组,同时寸的位置++
if(pRxPacket>=4) //4个载荷数据已经收完了,这时就可以转移到下一个状态了
{
RxState = 2;
}
}
else if(RxState == 2) //进入等待包尾的程序
{
if(RxData == 0xFE)
{
RxState = 0; //接收到包尾,回到最初的状态
Serial_RxFlag = 1; //置一个接收标志位
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除一下标志位
}
}
(3)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
void Serial_Printf(char *format,...);
uint8_t Serial_GetRxFlag(void);
void Serial_SendPacket(void);
#endif
(4)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
uint8_t KeyNum;
int main(void)
{
OLED_Init(); // 初始化OLED屏幕
Key_Init();
Serial_Init();
OLED_ShowString(1,1,"TxPacket");
OLED_ShowString(3,1,"RxPacket");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
Serial_TxPacket[0]++;
Serial_TxPacket[1]++;
Serial_TxPacket[2]++;
Serial_TxPacket[3]++;
Serial_SendPacket();
OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);
OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);
OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);
OLED_ShowHexNum(2,10,Serial_TxPacket[3],2);
}
if(Serial_GetRxFlag()==1) //收到了数据包
{
OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);
OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);
OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);
OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);
}
}
}
8.7 串口收发文本数据包
8.7.1 硬件电路
8.7.2 软件部分
(1)复制《串口收发HEX数据包》工程并改名为《串口收发文本数据包》
(2)Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
char Serial_RxPacket[100]; //用于缓存接收的数据包,单条指令最长不能超过100个字符
uint8_t Serial_RxFlag = 0; //缓存状态标志位
/*串口初始化函数
第1步:开启时钟,把需要用的USART和GPIO的时钟打开
第2步:GPIO初始化,把TX配置成复用输出,RX配置成输入
第3步:配置USART,直接使用一个结构体,就可以把这里所有参数配置好了
第4步:开启中断
第5步:开启RXNE标志位到NVIC的输出
第6步:如果只需要发送的功能,就直接开启USART,初始化就结束了;
如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就可以了
*/
void Serial_Init(void)
{
/*第1步:开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //开启挂载再APB2总线上的USART的时钟控制
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //USRTA1是复用再PA9和PA10上的
/*第2步:GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //接收引脚选择上拉输入模式
/*引脚模式:TX引脚是USART外设控制的输出脚,所以要选择复用推挽输出,
RX引脚是USART引脚外设的数据输入脚,所以要选择输入模式(输入模式并不分普通输入、复用输入),
一般RX配置成浮空输入或者上拉输入,因为串口波形空闲状态是高电平,所有不使用下拉输入*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; //这个案例只需要发送
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*第3步:配置USART*/
USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600; //配置波特率9600
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制:不使用流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //串口模式设置为发送模式和接收模式同时开启
USART_InitStruct.USART_Parity = USART_Parity_No; //校验位无校验
USART_InitStruct.USART_StopBits = USART_StopBits_1; //停止位1位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //字长选择8位
USART_Init(USART1,&USART_InitStruct);
/*第4步::开启中断*/
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //RXNE标志位一旦置1,就会向NVIC申请中断
/*第5步::开启RXNE标志位到NVIC的输出*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
/*第6步::开启USART*/
USART_Cmd(USART1,ENABLE);
}
/*发送数据函数*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET); //获取TXE标志位,由手册可知不需要手动清零
}
/*发送数组函数*/
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
Serial_SendByte(Array[i]);
}
/*发送字符串函数*/
void Serial_SenString(char *String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++) //字符串自带一个结束标志位"\0",使用字符串标志位来结束循环
Serial_SendByte(String[i]);
}
/*求一个数的指数函数,用于分离一个数的每一位做准备*/
uint32_t Serial_Pow(uint32_t x,uint32_t y)
{
uint32_t Result = 1;
while(y--)
{
Result *= x; //x的y次方
}
return Result;
}
/*发送数字函数,在电脑频幕上显示字符形式的数字*/
void Serial_SendNumber(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+'0'); //提取数字中的每一位,后面+'0'是ASCII的偏移量
}
}
/*printf重定向函数*/
int fputc(int ch,FILE *f)
/*fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个一个打印的
把fputc函数重定向到了串口,那printf自然就输出到串口了,这样printf就移植好了*/
{
Serial_SendByte(ch); //把fputc重定向到串口
return ch;
}
/*sprintf函数的封装*/
void Serial_Printf(char *format,...) //format用来接收格式化字符串,后面...这部分用来接收后面的可变参数列表
{
char String[100];
va_list arg; //定义一个参数列表变量,va_list是一个类型名,arg是变量名
va_start(arg,format); //从format位置开始接收参数表,放在arg里面
vsprintf(String,format,arg);
va_end(arg); //释放参数表
Serial_SenString(String); //把String发送出去
}
/*自动清除标志位函数*/
//uint8_t Serial_GetRxFlag(void)
//{
// if(Serial_RxFlag == 1)
// {
// Serial_RxFlag = 0;
// return 1;
// }
// return 0;
//}
/*接收中断函数*/
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //状态变量,0、1、2
static uint8_t pRxPacket = 0;
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if(RxState == 0) //进入等待包头的程序
{
if(RxData == '@'&& Serial_RxFlag == 0)
{
RxState = 1; //接收到'@',转移状态
pRxPacket = 0; //清零,为下一次进入接收数据程序做准备
}
}
else if(RxState == 1) //进入接收数据的程序
{
if(RxData == '\r')
{
RxState = 2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket++; //每进一次接收状态,数据就转一次缓存数组,同时寸的位置++
}
}
else if(RxState == 2) //进入等待包尾的程序
{
if(RxData == '\n')
{
RxState = 0; //接收到包尾,回到最初的状态
Serial_RxPacket[pRxPacket] = '\0'; //加一个字符串的结束标志位
Serial_RxFlag = 1; //置一个接收标志位
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除一下标志位
}
}
(3)Serial.h
#ifndef __SERIAL_
#define __SERIAL_
#include <stdio.h>
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SenString(char *String);
uint32_t Serial_Pow(uint32_t x,uint32_t y);
void Serial_SendNumber(uint32_t Number,uint8_t Length);
void Serial_Printf(char *format,...);
//uint8_t Serial_GetRxFlag(void);
#endif
(4)main.c
#include "stm32f10x.h" // Device header
#include "Delay.h" // 调用延时头文件
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
#include "LED.h"
#include <string.h>
int main(void)
{
LED_Init();
OLED_Init(); // 初始化OLED屏幕
Serial_Init();
OLED_ShowString(1,1,"TxPacket");
OLED_ShowString(3,1,"RxPacket");
while(1)
{
if(Serial_RxFlag == 1)
{
OLED_ShowString(4,1," "); //擦除第4行,因为如果前面的数据包比后面的长,不擦除的话,显示后面数据包的时候将遗留前一个数据包的后半部分
//注意这里不能用tab键替换空格,否则OLED显示会出现乱码
OLED_ShowString(4,1,Serial_RxPacket);
if(strcmp(Serial_RxPacket,"LED_ON")==0)
{
LED1_ON();
Serial_SenString("LED_ON_OK\r\n");
OLED_ShowString(2,1," "); //擦除第2行,因为如果前面的数据包比后面的长,不擦除的话,显示后面数据包的时候将遗留前一个数据包的后半部分
OLED_ShowString(2,1,"LED_ON_OK");
}
else if(strcmp(Serial_RxPacket,"LED_OFF")== 0)
{
LED1_OFF();
Serial_SenString("LED_OFF_OK\r\n");
OLED_ShowString(2,1," "); //擦除第2行,因为如果前面的数据包比后面的长,不擦除的话,显示后面数据包的时候将遗留前一个数据包的后半部分
OLED_ShowString(2,1,"LED_OFF_OK");
}
else
{
Serial_SenString("ERROR_COMMAND\r\n");
OLED_ShowString(2,1," "); //擦除第2行,因为如果前面的数据包比后面的长,不擦除的话,显示后面数据包的时候将遗留前一个数据包的后半部分
OLED_ShowString(2,1,"ERROR_COMMAND");
}
Serial_RxFlag = 0;
}
}
}
8.8 FlyMcu串口下载
先点搜索串口,找到我们串口通信的COM号
把STM32芯片上跳线帽拔下来 插在右边两个针脚(更换后如下图所示),配置BOOT0为1,按一下复位键,让程序重新开始运行。这样芯片就进入BootLoader程序了,进入BootLoader程序之后,STM32执行的程序就是、不断接收USART1的数据,刷新到主闪存。
然后回到FlyMcu软件,点击开始编程。
此时,程序通过 BootLoader刷新到主闪存里了,但程序还没开始运行,还处于主闪存刷新状态。
把跳线帽换回到原位置。然后按一下复位。程序开始运行。
为了解决更换跳线帽这个问题,需要配置一键下载电路, 来配置DTR和RTS进而配置BootLoader。
使用上图方案需要更改第一次跳线帽和按第一次复位按钮。
BootLoader模式下才可以下载。
8.9 STLINK Utility
STLINK连接好后,打开STLINK Utility。
STLINK固件更新功能: