STM32-USART

本内容基于江协科技STM32视频学习之后整理而得。

文章目录

  • [1. 串口通信协议](#1. 串口通信协议)
    • [1.1 通信接口](#1.1 通信接口)
    • [1.2 串口通信](#1.2 串口通信)
    • [1.3 硬件电路](#1.3 硬件电路)
    • [1.4 电平标准](#1.4 电平标准)
    • [1.5 串口参数及时序](#1.5 串口参数及时序)
    • [1.6 串口时序](#1.6 串口时序)
  • [2. USART串口通信](#2. USART串口通信)
    • [2.1 USART简介](#2.1 USART简介)
    • [2.2 USART框图](#2.2 USART框图)
    • [2.3 USART基本结构](#2.3 USART基本结构)
    • [2.4 数据帧](#2.4 数据帧)
    • [2.5 数据帧-配置停止位](#2.5 数据帧-配置停止位)
    • [2.6 起始位侦测](#2.6 起始位侦测)
    • [2.7 数据采样](#2.7 数据采样)
    • [2.8 波特率发生器](#2.8 波特率发生器)
    • [2.9 数据模式](#2.9 数据模式)
  • [3. USART库函数和代码](#3. USART库函数和代码)
    • [3.1 USART库函数](#3.1 USART库函数)
    • [3.2 9-1串口发送](#3.2 9-1串口发送)
      • [3.2.1 硬件连接](#3.2.1 硬件连接)
      • [3.2.2 运行结果](#3.2.2 运行结果)
      • [3.2.3 代码实现流程](#3.2.3 代码实现流程)
      • [3.2.4 代码](#3.2.4 代码)
    • [3.3 9-2 串口发送+接收](#3.3 9-2 串口发送+接收)
      • [3.3.1 硬件连接](#3.3.1 硬件连接)
      • [3.3.2 运行结果](#3.3.2 运行结果)
      • [3.3.3 代码实现流程](#3.3.3 代码实现流程)
      • [3.3.4 代码](#3.3.4 代码)
  • [4. USART串口数据包](#4. USART串口数据包)
    • [4.1 HEX数据包](#4.1 HEX数据包)
    • [4.2 文本数据包](#4.2 文本数据包)
    • [4.3 HEX数据包接收](#4.3 HEX数据包接收)
    • [4.4 文本数据包接收](#4.4 文本数据包接收)
  • [5. 代码](#5. 代码)
    • [5.1 9-3串口收发HEX数据包](#5.1 9-3串口收发HEX数据包)
      • [5.1.1 硬件连接](#5.1.1 硬件连接)
      • [5.1.2 运行结果](#5.1.2 运行结果)
      • [5.1.3 代码流程](#5.1.3 代码流程)
      • [5.1.4 代码](#5.1.4 代码)
    • [5.2 9-4串口收发文本数据包](#5.2 9-4串口收发文本数据包)
      • [5.2.1 硬件连接](#5.2.1 硬件连接)
      • [5.2.2 运行结果](#5.2.2 运行结果)
      • [5.2.3 代码实现流程](#5.2.3 代码实现流程)
      • [5.2.4 代码](#5.2.4 代码)
  • [6. FlyMcu和STLINK Utility](#6. FlyMcu和STLINK Utility)
    • [6.1 串口下载的原理:](#6.1 串口下载的原理:)
    • [6.2 每次下载都要切换跳线帽,怎么解决](#6.2 每次下载都要切换跳线帽,怎么解决)

1. 串口通信协议

1.1 通信接口

  • 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
名称 引脚 双工 时钟 电平 设备
USART TX(发送)、RX(接收) 全双工 异步 单端 点对点
I2C SCL(时钟)、SDA(数据) 半双工 同步 单端 多设备
SPI SCLK(时钟)、MOSI(主机输出数据脚)、MISO(主机输入数据脚)、CS(片选,用于指定通信的对象) 全双工 同步 单端 多设备
CAN CAN_H、CAN_L(差分数据脚,用两个引脚表示一个差分数据) 半双工 异步 差分 多设备
USB DP、DM(差分数据脚) 半双工 异步 差分 点对点
  • 全双工:指通信双方能同时进行双向通信,有两根通信线
  • 半双工:有一根数据线
  • 单工:数据只能从一个设备到另一个设备,不能反着来。
  • I2C和SPI有单独的时钟线,因此是同步的,接收方可以在时钟信号的指引下进行采样。
  • USART和CAN及USB没有时钟线,需要双方约定一个采样频率,因此是异步通信。并且需要加一些帧头帧尾等,进行采样位置的对齐。
  • 单端电平:引脚的高低电平都是对GND的电压差。因此单端信号通信的双方必须要供地,就是把GND接在一起。因此USART、I2C、SPI的引脚还要加一个GND引脚。
  • CAN、USB是靠两个差分引脚的电压差来传输信号的,是差分信号。在通信的时候,不需要GND。但USB协议里有一些也是需要单端信号的,因此USB还是需要GND的。使用差分信号可以极大地提高抗干扰特性,所以差分信号一般传输速度和距离都会非常高。
  • USART和USB是点对点的通信(老师面对一个学生),I2C、SPI和CAN是可以在总线上挂载多个设备的(就像老师面对多个学生),需要有一个寻址的过程,以确定通信的对象。

1.2 串口通信

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机地应用范围,增强了单片机系统的硬件实力。
  • 第一个是:USB转串口模块,内部有个芯片CH340,可以把串口协议转换为USB协议。一边是USB串口,接在电脑上,另一边是串口的引脚,可以和支持串口的芯片接在一起。
  • 中间是陀螺仪传感器的模块,可以测量角速度、加速度等姿态参数,一边是串口的引脚,一边是I2C的引脚。
  • 右边是蓝牙串口模块,下面4个引脚是串口通信的引脚。

1.3 硬件电路

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片。

TX和RX是单端信号,其高低电平都是相对于GND的,因此GND是必须接的。

如果两个设备都有独立供电,则VCC可以不接。若其中一个设备没有独立供电,就需要将两个设备的VCC接在一起(STM32有供电,蓝牙串口没有独立供电,所以就就需要将蓝牙串口的VCC和STM32的VCC接在一起。)

一个设备用TX发送高低电平,另一个设备用RX接收高低电平。在线路中使用TTL电平。所以如果线路对地是3.3V,就代表发送了逻辑1,如果线路对地是0V,就代表发送了逻辑0。

1.4 电平标准

  • 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

  • TTL电平:+3.3V或+5V表示1,0V表示0

  • RS232电平:-3~-15V表示1,+3~+15V表示0

    一般在大型机器上使用

  • RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)

    抗干扰能力强,使用RS485电平标准,通信距离可以达到上千米。

1.5 串口参数及时序


串口中每一个字节都装载在一个数据帧里面,每个数据帧都由起始位、数据位和停止位组成,第一个图数据位有8个,代表一个字节的8位。第二个图的数据位是9位,可以在数据位的最后加一个奇偶校验位。其中有效载荷是前8位,代表一个字节,校验位跟在有效载荷后面,占1位。

  • 波特率:串口通信的速率。决定了每隔多久发送一位。

  • 起始位 :标志一个数据帧的开始,固定为低电平。

    串口的空闲状态是高电平,也就是没有数据传输的时候,引脚必须要置高电平,作为空闲状态。需要传输时,必须要先发送一个起始位,该起始位必须是低电平,来打破空闲状态的高电平,产生一个下降沿。该下降沿,就告诉接收设备,这一帧数据要开始了。

  • 数据位 :数据帧的有效载荷,1为高电平,0为低电平。低位先行。

    如要发送一个字节0x0F,把0F转换为二进制,就是0000 1111,低位先行,所以数据从低位开始发送。就是1111 0000依次放在发送引脚上。

    第一种:将校验位作为数据位的一部分,其中9位数据(8位有效载荷和1位校验位)

    第二种:将数据位和校验位独立开,数据位就是有效载荷,校验位就是独立的1位。

  • 校验位 :用于数据验证,根据数据位计算得来。

    奇偶校验的数据验证方法,可以判断数据传输是不是出错了。如果数据出错了,可以选择丢弃或要求重传。校验可以选择3种方式:无校验、奇校验、偶校验。

    如果使用了奇校验,则包括校验位在内的9位数据会出现奇数个1。发送方在发送数据后,会补一个校验位,保证1的个数为奇数。接收方,在接收数据后,会验证数据位和校验位。

    偶校验:就是保证1的个数是偶数。

    奇偶校验只能保证一定程度上的数据校验。如果想要更高的检出率,可以用CRC校验,

  • 停止位:用于数据帧间隔,固定为高电平。也是为下一个起始位做准备的。

1.6 串口时序

2. USART串口通信

2.1 USART简介

  • USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
  • 自带波特率发生器,最高达4.5Mbits/s。一般设置为9600或115200。
  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
  • 可选校验位(无校验/奇校验/偶校验)
  • 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
  • STM32F103C8T6USART资源: USART1、 USART2、 USART3

USART1是APB2总线的设备,USART2、 USART3是APB1总线的设备。

2.2 USART框图

(1)设置:波特率,115200,8n1(数据位:8,无校验,1个停止位)

(2)发送:val -->TDR-->移位寄存器-->逐位发送

  • TX和RX是接收和发送的引脚;SW_DX、IRDA_OUT/IN是智能卡和IrDA通信的引脚。
  • 写操作:把数据写入发送数据寄存器;
  • 读操作:从接收数据寄存器里读出;
  • 发送数据寄存器(TDR)和接收数据寄存器(RDR):占用同一个地址,在程序上,只表现为一个寄存器,就是数据寄存器DR,但实际硬件中是分成了两个寄存器,一个用于发送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和移位寄存器的双重缓存,可以保证连续发送数据的时候,数据帧之间不会有空闲。
  • 接收数据寄存器RDR和接收移位寄存器 :数据从RX引脚通向接收移位寄存器,
    在接收器控制的驱动下,一位一位地读取RX电平,先放在最高位,然后向右移,移位8次之后,就能接收一个字节了。因为串口协议规定是低位先行,所以接收移位寄存器是从高位往低位这个方向移动的,当一个字节移位完成后,这一个字节的数据就会整体地转移到接收数据寄存器RDR里来,在转移的过程中,也会置一个标志位RXNE(RX Not Empty 接收数据寄存器非空),当检测到RXNE置1后,就可以把数据读走了。这里也是两个寄存器进行缓存,当数据从移位寄存器转移到RDR时,就可以直接移位接收下一帧数据了。
  • 发送器控制:就是用来控制发送移位寄存器的工作的,
  • 接收器控制:就是用来控制接收移位寄存器的工作。
  • 硬件数据流控:如果发送设备发的太快,接收设备来不及处理,就会出现丢弃或覆盖数据的现象,有了留流控,就可以避免这个问题。nRTS是请求发送,是输出脚,也就是告诉别人,当前能不能接收。nCTS是清除发送,是输入脚,用于接收别人nRTS信号的。n代表低电平有效。这两个引脚得找另一个支持流控的串口,它的TX接到我的RX,然后我的RTS要输出一个能不能接收的反馈信号,接到对方的CTS,当我能接收的时候,RTS就置低电平,请求对方发送,对方的CTS接收到之后,就可以一直发。当我处理不过来时,比如接收数据寄存器一直没有读,又有新的数据进来了,现在代表我没有及时处理,那RTS就会置高电平,对方CTS接收到之后,就会暂停发送,直到这里接收数据寄存器被读走。RTS置低电平,新的数据才会继续发送。那反过来,当我的TX给对方发送数据时,我们CTS就要接到对方的RTS,用于判断对方,能不能接收。TX和CTS是一对的,RX和RTS是一对的。CTS和RTS也要交叉连接。(一般不用,了解即可)
  • SCLK:用于产生同步的时钟信号,是配合发送移位寄存器输出的,发送寄存器每移位一次,同步时钟电平就跳变一个周期。时钟告诉对方,我移出去一位数据了。你看要不要让我这个时钟信号来指导你接收一下?这个时钟只支持输出,不支持输入。所以两个USART之间不能实现同步的串口通信。时钟的作用:第一个用途是兼容别的协议,比如串口加上时钟后,就跟SPI协议特别像,所以有了时钟输出的串口,就可以兼容SPI。另外这个时钟也可以做自适应波特率,比如接收设备不确定发送设备给的什么波特率,那就可以测量一下这个时钟的周期,然后再计算得到波特率。(一般不用,了解即可)
  • 唤醒单元:用于实现串口挂载多设备。串口一般是点对点的通信,点对点只支持两个设备互相通信,想发数据直接发就行。多设备是在一条总线上可以接多个从设备,每个设备分配一个地址,若想跟某个设备通信,就先进行寻址,确定通信对象,再进行数据收发。
  • USART地址:可以给串口分配一个地址,当发送指定地址时,此设备唤醒开始工作;当发送别的设备地址时,别的设备就唤醒工作。
  • USART中断控制:就是配置中断是不能通向NVIC。中断申请位就是状态寄存器里的各种标志位。其中TXE是发送寄存器空,RXNE是接收寄存器非空,是判断发送状态和接收状态的必要标志位。
  • 波特率发生器 :就是分频器,APB时钟进行分频得到发送和接收移位的时钟,
    时钟输入是fPCLKx(x=1或2)。USART1挂载再APB2,所以就是PCLK2的时钟,一般是72M;其他的USART挂载再APB1,所以就是PCLK1的时钟,一般是36M,之后这个时钟进行一个分频,除一个USARTDIV的分频系数,分频系数是支持小数点后4位的,分频就更加精准,分频之后还要再除个16,得到发送器时钟和接收器时钟,通向控制部分。
    如果TE=1,就是发送器使能了,发送部分的波特率就有效;如果RE=1,就是接收器使能了,接收部分的波特率就有效,

2.3 USART基本结构

通过GPIO的复用输出,输出到TX引脚,

2.4 数据帧

  • 字长:即数据位长度,包含校验位,
  • 9位字长:是TX发送或RX接收的数据帧格式。
    9位和8位都可是有校验和无校验的格式,但一般选9位有校验,8位无校验格式。
  • 时钟:就是同步时钟输出的功能,在每个数据位的中间,都有一个时钟上升沿,时钟的频率和数据速率是一样的。接收端可以在时钟上升沿进行采样,这样就可以精准定位每一位数据。
  • 空闲帧和断开帧是局域网协议用的。

2.5 数据帧-配置停止位

发送器:

可以配置停止位长度为0.5、1、1.5、2四种,

2.6 起始位侦测

接收器:

  • 当输入电路侦测到一个数据帧的起始位后,就会以波特率的频率,连续采样一帧数据。同时,从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐了,后面就肯定对齐的。
  • 首先输入的电路对采样时钟进行了细分,会以波特率的16倍频率进行采样,也就是在一位的时间里,可以进行16次采样。
    • 其策略是:最开始空闲状态高电平,则采样就一直是1,在某个位置采到0,就说明,在该两次采样之间出现了下降沿。如果没有任何噪声,那之后就应该是起始位了,在起始位,会进行16次采样,没有噪声的话,这16次采样,肯定都是0,满足情况。如果有一些轻微的噪声,导致3位里面只有两个0,另一个是1,但是在状态寄存器里会置一个NE(噪声标志位),就是提醒一下,数据收到了,但是有噪声;如果3位里有1个0 ,就不算检测到了起始位,可能前面那个下降沿是噪声导致的,这时电路就忽略前面的数据,重新开始捕捉下降沿。如果通过了起始位侦测,那接收状态就由空闲,变为接收起始位。同时,第8、9、10次采样的位置,就正好是起始位的正中间,之后,接收数据位时,就都在第8、9、10次,进行采样,这样就能保证采样位置在位的正中间了,这就是起始位侦测和采样位置对齐的策略。

2.7 数据采样

从1到16是一个数据位的时间长度,在一个数据位,有16个采样时钟。由于起始位侦测已经对齐了采样时钟,所以,这里就直接在第8、9、10次采样数据位。为了保证数据的可靠性,连续采样3次,没有噪声的理想情况下,这3次肯定全为1或全为0,全为1就认为收到了1,全为0就认为收到了0;如果有噪声,导致3次采样不是全为1或者全为0,那就按照2:1的规则来,2次为1,就认为收到了1,2次为0,就认为收到了0,在这种情况下,噪声标志位NE也会置1,表示有噪声。

2.8 波特率发生器

  • 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
  • 计算公式:波特率=fPCLK2/1/(16 * DIV)

16是因为内部有个16倍波特率的采样时钟。

若要配置USART1位9600的波特率,则9600 = 72M / (16 * DIV),得DIV = 468.75。写入寄存器还要转换为二进制1 1101 0100.11,

2.9 数据模式

  • HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
  • 文本模式/字符模式:以原始数据编码后的形式显示


3. USART库函数和代码

3.1 USART库函数

c 复制代码
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);

// 开启USART到DMA的触发通道
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);

// USART_SendData:发送数据,写DR寄存器;USART_ReceiveData:接收数据,读DR寄存器
// DR寄存器内部有4个寄存器,控制发送与接收
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

// 标志位相关函数
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);

3.2 9-1串口发送

3.2.1 硬件连接

写一个串口的模块,通过串口通信,把一些数据发送到电脑上的串口助手来显示,

采用USB转串口模块将STM32的串口引脚接到电脑上,之后电脑端可以打开串口助手的软件,选择一下串口号、波特率、数据位,接收模式选择为HEX模式后,打开串口,按一下STM32的复位键,程序就会在每次上电后通过串口发送一批数据。当接收模式切换为文本模式后,再按一下复位键,这时软件就会对刚才的数据进行文本映射,找到每个数据对应的字符,以字符串的形式显示出来。

RXD和TXD接在PA9和PA10引脚(PA9是USART1_TX,PA10是USART1_RX)。

3.2.2 运行结果

c 复制代码
//	传送一个字节
	Serial_SendByte(0x41);
	
//	传送数组
	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
	Serial_SendArray(MyArray, 4);
c 复制代码
// 传送字符串,\r\n是换行
Serial_SendString("Hello World!\r\n");

// 传送数字
 Serial_SendNumber(12345,5);
 printf("\r\nNum=%d\r\n",666);

// 多个串口使用printf函数,
// sprintf可以把格式化字符输出到一个字符串里
char String[100];  // 定义字符串
sprintf(String,"Num=%d\r\n",222); // 打印字符串
Serial_SendString(String); // 发送字符串

Serial_Printf("Num=%d\r\n",333);

3.2.3 代码实现流程

  1. 串口代码:
    1. 配置USART:
      1. 开启时钟,把需要用的USART和GPIO的时钟打开
      2. GPIO初始化,把TX配置成复用输出,RX配置成输入
      3. 配置USART ,使用结构体
      4. 若只需要发送的功能,直接开启USART (USART_Cmd),初始化就结束了
    2. 编写函数:发送数据,发送一个字节
    3. 编写函数:发送数组
    4. 编写函数:发送字符串
    5. 编写函数:发送数字
    6. 编写函数:封装sprintf
  2. main.c:测试在串口代码中编写的函数

3.2.4 代码

  1. 串口代码:
c 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>


void Serial_Init(void)
{
/*
1. 开启时钟,把需要用的USART和GPIO的时钟打开
2. GPIO初始化,把TX配置成复用输出,RX配置成输入
3. 配置USART ,使用结构体
4. 若只需要发送的功能,直接开启USART,初始化就结束了
	如果还需要接收的功能,可能还需要配置中断;
    那就在开启USART之前,再加上ITConfig和NVIC的代码就行
*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	// TX引脚是USART外设控制的输出脚,选复用推挽输出
	// RX引脚是USART外设数据输入脚,选输入模式,配置浮空输入或上拉输入
	// 因为串口波形空闲状态是高电平,所以不使用下拉输入,
	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);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600; // 波特率:9600
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 硬件流控制,无流控
	USART_InitStructure.USART_Mode = USART_Mode_Tx; // 只有发送模式
	USART_InitStructure.USART_Parity = USART_Parity_No;  // 校验位:无校验
	USART_InitStructure.USART_StopBits = USART_StopBits_1; // 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);
	// 要等待TXE置1,因此套一个while循环
	// 如果TXE标志位 == RESET,就一直循环,直到SET,结束等待
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // TXE:发送数据寄存器空标志位
}

/*
发送数组的函数
是一个uint8_t的指针类型,指向待发送数组的首地址
传送数组需要使用指针
由于数组无法判断是否结束,需要传递一个Length
*/
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]);
	}
}

/*
发送一个数字,需要将十位、百位、小数查分开,
转换成字符数字对应的数据,依次发送出去
*/
// 次方函数,x^y
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y --)
	{
		Result *= X;
	}
	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 + 0x30) ; // 以字符的形式显示,需要加一个偏移,0x30是0
	}
}

// fputc函数,是printf函数的底层,将其重定向到串口
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

// 封装sprintf
// char *format接收格式化字符串
// ...用来接收后面的可变参数列表
void Serial_Printf(char *format, ...)
{
	
	char String[100]; // 定义输出的字符串
	va_list arg; // 定义一个参数列表变量
	va_start(arg, format); // 从format位置开始接收参数表,放在arg里面
	vsprintf(String, format, arg); // 打印位置是String,
								   // 格式化字符串是format,
								   // 参数表是arg
	va_end(arg); // 释放参数表
	Serial_SendString(String); // 把String发送出去
}
  1. main.c:测试在串口代码中编写的函数
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"

int main(void)
{
	OLED_Init();
	Serial_Init();
	
 
//	传送一个字节
//	调用该函数后的逻辑:上电后,初始化串口,再用串口发送一个0x41,
//	调用该函数后,TX引脚产生一个0x41对应的波形,该波形可以发送给其他支持串口的模块,
//	也可以通过USB转串口的模块,发送到电脑端,
//	该程序是在电脑端接收数据,
	
//	Serial_SendByte(0x41);
	
//	传送数组
//	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
//	Serial_SendArray(MyArray, 4);
	
	// 传送字符串,\r\n是换行
	
	Serial_SendString("Hello World!\r\n");
	
	
	// 传送数字
	
	 Serial_SendNumber(12345,5);
		
	 printf("\r\nNum=%d\r\n",666);
	
	// 多个串口使用printf函数,
	// sprintf可以把格式化字符输出到一个字符串里
	
	char String[100];  // 定义字符串
	sprintf(String,"Num=%d\r\n",222); // 打印字符串
	Serial_SendString(String); // 发送字符串
	
	Serial_Printf("Num=%d\r\n",333);
    
	//Serial_Printf("你好,世界");
	
	while(1)
	{
		
			
		
	}
}

3.3 9-2 串口发送+接收

3.3.1 硬件连接

在上一节代码的基础上添加接收功能。

main函数流程:判断是否收到数据,如果收到数据,则读取数据,将数据回传到电脑,并且也在OLED上显示一下。

串口助手:发送模式和接收模式都选择为HEX模式,在发送区写一个数据41,点击发送,OLED显示接收到的数据41,接收区也显示41。若将接收模式切换为文本模式,则接收区显示数据41对应的字符文本A。

3.3.2 运行结果


3.3.3 代码实现流程

  1. 串口代码:
    1. 配置USART:
      1. 开启时钟,把需要用的USART和GPIO的时钟打开
      2. GPIO初始化,把TX配置成复用输出,RX配置成输入
      3. 配置USART ,使用结构体
      4. 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
    2. 编写函数:发送数据,发送一个字节
    3. 编写函数:发送数组
    4. 编写函数:发送字符串
    5. 编写函数:发送数字
    6. 编写函数:封装sprintf
    7. 编写USART1中断函数
  2. main.c:
    1. 串口初始化;
    2. 判断接收标志位Serial_RxFlag==1,说明接收到数据了,
    3. 就可以再次发送数据,并在OLED上显示

3.3.4 代码

  1. 串口代码
c 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

void Serial_Init(void)
{
/*
1. 开启时钟,把需要用的USART和GPIO的时钟打开
2. GPIO初始化,把TX配置成复用输出,RX配置成输入
3. 配置USART ,使用结构体
4. 若只需要发送的功能,直接开启USART,初始化就结束了
	如果还需要接收的功能,可能还需要配置中断;
    那就在开启USART之前,再加上ITConfig和NVIC的代码就行
	
*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	// TX引脚是USART外设控制的输出脚,选复用推挽输出
	// RX引脚是USART外设数据输入脚,选输入模式,配置浮空输入或上拉输入
	// 因为串口波形空闲状态是高电平,所以不使用下拉输入,
	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; // 波特率: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; // 1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位字长
	USART_Init(USART1, &USART_InitStructure);
	
	// 串口接收,可以使用查询和中断两种方法,
	// 中断方式:一旦RXNE置1了,就会向NVIC申请中断,之后可以在中断函数(USART1_IRQHandler)里接收数据
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	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);
	USART_Cmd(USART1,ENABLE);
}

// 发送数据函数
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	// 要等待TXE置1,因此套一个while循环
	// 如果TXE标志位 == RESET,就一直循环,直到SET,结束等待
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // TXE:发送数据寄存器空标志位
}

/*
发送数组的函数
是一个uint8_t的指针类型,指向待发送数组的首地址
传送数组需要使用指针
由于数组无法判断是否结束,需要传递一个Length
*/
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]);
	}
}

/*
发送一个数字,需要将十位、百位、小数查分开,
转换成字符数字对应的数据,依次发送出去
*/
// 次方函数,x^y
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y --)
	{
		Result *= X;
	}
	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 + 0x30) ; // 以字符的形式显示,需要加一个偏移,0x30是0
	}
}

// fputc函数,是printf函数的底层,将其重定向到串口
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

// 封装sprintf
// char *format接收格式化字符串
// ...用来接收后面的可变参数列表
void Serial_Printf(char *format, ...)
{
	// 定义输出的字符串
	char String[100];
	// 定义一个参数列表变量
	va_list arg;
	// 从format位置开始接收参数表,放在arg里面
	va_start(arg, format);
	// 打印位置是String,格式化字符串是format,参数表是arg
	vsprintf(String, format, arg);
	// 释放参数表
	va_end(arg);
	// 把String发送出去
	Serial_SendString(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; // 读完之后,标志位置1,
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);// 清除标志位
	}
}
  1. main.c
c 复制代码
#include "stm32f10x.h"                  // Device header
#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)
	{
//		// 查询的流程是,在主函数里不断判断RXNE标志位,如果置1,则说明收到数据了
//		// 再调用ReceiveData,读取DR寄存器
//		if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
//		{
//			RXData = USART_ReceiveData(USART1);
//			OLED_ShowHexNum(1, 1, RXData, 2);
//		}
		if (Serial_GetRxFlag() == 1)
		{
			RXData = Serial_GetRxData();
			Serial_SendByte(RXData);
			OLED_ShowHexNum(1, 8, RXData, 2);
		}
	}
}

4. USART串口数据包

4.1 HEX数据包

  • 固定包长,含包头包尾
  • 可变包长,含包头包尾
  • 优点:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据
  • 缺点:灵活性不足、载荷容易和包头包尾重复
  • 解决方法:第一个就是限制载荷的范围,如果可以的话,在发送的时候,对数据进行限幅。第二种,如果无法避免载荷数据和包头包尾重复,就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要通过包头包尾对齐了数据,就可以知道哪个数据应该是包头包尾,哪个数据应该是载荷数据。在接收载荷数据的时候,并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐。第三种,就是增加包头包尾的数量,并且让它尽量呈现载荷数据出现不了的状态。
    包头包尾并不是都需要的,可以只要包头,这样数据包的格式就是一个包头FF,加4个数据。

4.2 文本数据包

  • 固定包长,含包头包尾
  • 可变包长,含包头包尾
  • 优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合;
  • 缺点:解析效率低

4.3 HEX数据包接收

使用状态机的方法来接收一个数据包,状态机是多标志位。

4.4 文本数据包接收

5. 代码

5.1 9-3串口收发HEX数据包

5.1.1 硬件连接

  • OLED上前两行显示TX数据包TxPacket,后面两行显示RX数据包RxPacket。
  • PB1接一个按键,用于控制。
  • 串口助手:接收模式和发送模式都选择HEX模式。
  • 按一下按键,执行发送,OLED第二行显示,发送的数据包,串口助手(接收区)显示接收到的数据包。

数据包:以FF为包头,以FE为包尾,中间固定4个字节为数据。如:FF 01 02 03 04 FE

5.1.2 运行结果

发送:


接收:

5.1.3 代码流程

  1. 串口代码:
    1. 配置USART:
      1. 开启时钟,把需要用的USART和GPIO的时钟打开
      2. GPIO初始化,把TX配置成复用输出,RX配置成输入
      3. 配置USART ,使用结构体
      4. 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
    2. 编写函数:发送数据,发送一个字节
    3. 编写函数:发送数组
    4. 编写函数:发送字符串
    5. 编写函数:发送数字
    6. 编写函数:封装sprintf
    7. 编写函数:TxPacket数组的4个数据,自动加上包头包尾发送出去
    8. 编写USART1接收中断函数:HEX接收数据包,用状态机来执行接收逻辑,接收数据包,把载荷数据存在RxPacket数组里。
  2. main.c:
    1. 按下按键执行发送,在OLED的前两行显示发送的数据,并在串口助手的接收区显示接收到的数据包;
    2. 在串口助手的发送区,发送数据包(如: FF 06 07 08 09 FE),如果Serial_GetRxFlag() == 1,表示接收到了数据包,并在OLED的后两行显示接收到的数据。

5.1.4 代码

串口代码:

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;// 收到一个数据包就置Serial_RxFlag

void Serial_Init(void)
{
/*
1. 开启时钟,把需要用的USART和GPIO的时钟打开
2. GPIO初始化,把TX配置成复用输出,RX配置成输入
3. 配置USART ,使用结构体
4. 若只需要发送的功能,直接开启USART,初始化就结束了
	如果还需要接收的功能,可能还需要配置中断;
    那就在开启USART之前,再加上ITConfig和NVIC的代码就行
	
*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	// TX引脚是USART外设控制的输出脚,选复用推挽输出
	// RX引脚是USART外设数据输入脚,选输入模式,配置浮空输入或上拉输入
	// 因为串口波形空闲状态是高电平,所以不使用下拉输入,
	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; // 波特率: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; // 1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位字长
	USART_Init(USART1, &USART_InitStructure);
	
	// 中断方式:一旦RXNE置1了,就会向NVIC申请中断,之后可以在中断函数(USART1_IRQHandler)里接收数据
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	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);
	USART_Cmd(USART1,ENABLE);
	
	
}

// 发送数据函数
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	// 要等待TXE置1,因此套一个while循环
	// 如果TXE标志位 == RESET,就一直循环,直到SET,结束等待
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // TXE:发送数据寄存器空标志位
	
}

/*
发送数组的函数
是一个uint8_t的指针类型,指向待发送数组的首地址
传送数组需要使用指针
由于数组无法判断是否结束,需要传递一个Length

*/
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]);
	}
}

/*
发送一个数字,需要将十位、百位、小数查分开,
转换成字符数字对应的数据,依次发送出去

*/
// 次方函数,x^y
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y --)
	{
		Result *= X;
	}
	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 + 0x30) ; // 以字符的形式显示,需要加一个偏移,0x30是0
	}
}

// fputc函数,是printf函数的底层,将其重定向到串口
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

// 封装sprintf
// char *format接收格式化字符串
// ...用来接收后面的可变参数列表
void Serial_Printf(char *format, ...)
{
	// 定义输出的字符串
	char String[100];
	// 定义一个参数列表变量
	va_list arg;
	// 从format位置开始接收参数表,放在arg里面
	va_start(arg, format);
	// 打印位置是String,格式化字符串是format,参数表是arg
	vsprintf(String, format, arg);
	// 释放参数表
	va_end(arg);
	// 把String发送出去
	Serial_SendString(String);
	
}

// 调用该函数后,TxPacket数组的4个数据,
// 就会自动加上包头包尾发送出去
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket,4);
	Serial_SendByte(0xFE);
}


// 读后自动清除
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}

/*
HEX接收数据包:
接收中断函数,用状态机来执行接收逻辑,接收数据包,
然后把载荷数据存在RxPacket数组里,
*/
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;
	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;
				pRxPacket = 0;
			}
			
		}
		else if (RxState == 1) // 接收数据
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			if (pRxPacket >= 4)
			{
				RxState = 2;
			}
		}
		else if (RxState == 2) // 等待包尾
		{
			if (RxData == 0xFE) // 收到包尾
			{
				RxState = 0;
				Serial_RxFlag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);// 清除标志位
	}
}
  1. main.c代码:
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();
	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);
		}
	}
}

5.2 9-4串口收发文本数据包

5.2.1 硬件连接

  • 在PA1连接一个LED。
  • 串口助手:接收模式和发送模式都选择文本模式。
  • 数据包:以@符号为包头,中间是数据,数据也是规定好的指令,如写LED_ON,以换行符为包尾,这里一定要打个换行,换行也是字符。如:@LED_ON
  • OLED前两行显示发送数据包,后两行显示接收数据包

5.2.2 运行结果

  • 在串口助手的发送区写:@LED_ON (打换行符),OLED显示接收到"LED_ON",并且LED亮。然后STM32回传一个字符串"LED_ON_OK"(即发送该字符串),OLED第二行显示"LED_ON_OK",串口接收区显示LED_ON_OK,
  • 如果要熄灭LED,则串口助手发送区写@LED_OFF(打换行符),LED熄灭,STM32回传一个字符串LED_OFF_OK(即发送该字符串),OLED第二行显示"LED_OFF_OK",接收区显示LED_OFF_OK。
  • 如果发送其他指令,STM32也能收到,但会返回ERROR_COMMAND,错误指令。

灯亮:


灯灭:

5.2.3 代码实现流程

  1. 串口代码:
    1. 配置USART:
      1. 开启时钟,把需要用的USART和GPIO的时钟打开
      2. GPIO初始化,把TX配置成复用输出,RX配置成输入
      3. 配置USART ,使用结构体
      4. 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
    2. 编写函数:发送数据,发送一个字节
    3. 编写函数:发送数组
    4. 编写函数:发送字符串
    5. 编写函数:发送数字
    6. 编写函数:封装sprintf
    7. 编写USART1接收中断函数:HEX接收数据包,用状态机来执行接收逻辑,接收数据包,把载荷数据存在RxPacket数组里。

5.2.4 代码

串口代码:

c 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

// 定义接收缓存区的数组,只存储发送或接收的载荷数据,包头包尾就不存了
char Serial_RxPacket[100];
uint8_t Serial_RxFlag;// 收到一个数据包就置Serial_RxFlag

void Serial_Init(void)
{
/*
1. 开启时钟,把需要用的USART和GPIO的时钟打开
2. GPIO初始化,把TX配置成复用输出,RX配置成输入
3. 配置USART ,使用结构体
4. 若只需要发送的功能,直接开启USART,初始化就结束了
	如果还需要接收的功能,可能还需要配置中断;
    那就在开启USART之前,再加上ITConfig和NVIC的代码就行
*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	// TX引脚是USART外设控制的输出脚,选复用推挽输出
	// RX引脚是USART外设数据输入脚,选输入模式,配置浮空输入或上拉输入
	// 因为串口波形空闲状态是高电平,所以不使用下拉输入,
	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; // 波特率: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; // 1位停止位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位字长
	USART_Init(USART1, &USART_InitStructure);
	
	// 中断方式:一旦RXNE置1了,就会向NVIC申请中断,之后可以在中断函数(USART1_IRQHandler)里接收数据
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	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);
	USART_Cmd(USART1,ENABLE);
	
	
}

// 发送数据函数
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	// 要等待TXE置1,因此套一个while循环
	// 如果TXE标志位 == RESET,就一直循环,直到SET,结束等待
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);  // TXE:发送数据寄存器空标志位
	
}

/*
发送数组的函数
是一个uint8_t的指针类型,指向待发送数组的首地址
传送数组需要使用指针
由于数组无法判断是否结束,需要传递一个Length

*/
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]);
	}
}

/*
发送一个数字,需要将十位、百位、小数查分开,
转换成字符数字对应的数据,依次发送出去

*/
// 次方函数,x^y
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y --)
	{
		Result *= X;
	}
	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 + 0x30) ; // 以字符的形式显示,需要加一个偏移,0x30是0
	}
}

// fputc函数,是printf函数的底层,将其重定向到串口
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

// 封装sprintf
// char *format接收格式化字符串
// ...用来接收后面的可变参数列表
void Serial_Printf(char *format, ...)
{
	// 定义输出的字符串
	char String[100];
	// 定义一个参数列表变量
	va_list arg;
	// 从format位置开始接收参数表,放在arg里面
	va_start(arg, format);
	// 打印位置是String,格式化字符串是format,参数表是arg
	vsprintf(String, format, arg);
	// 释放参数表
	va_end(arg);
	// 把String发送出去
	Serial_SendString(String);
}

/*
HEX接收数据包:
接收中断函数,用状态机来执行接收逻辑,接收数据包,
然后把载荷数据存在RxPacket数组里,
*/
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;
	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_RxFlag = 1;  // 接收标志位
				Serial_RxPacket[pRxPacket] = '\0';
			}
		}
		
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);// 清除标志位
	}
}

main.c代码:

c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include <string.h>

int main(void)
{
	OLED_Init();
	LED_Init();
	Serial_Init();
	
	OLED_ShowString(1,1,"TxPacket");
	OLED_ShowString(3,1,"RxPacket");
	
	while(1)
	{
		if (Serial_RxFlag == 1) // 代表接收到数据包了
		{
			OLED_ShowString(4, 1, "                  ");
			OLED_ShowString(4,1, Serial_RxPacket);
			
			if (strcmp(Serial_RxPacket,"LED_ON") == 0)
			{
				LED1_ON();
				Serial_SendString("LED_ON_OK\r\n");
				OLED_ShowString(2, 1, "                  ");
			    OLED_ShowString(2,1, "LED_ON_OK");
			}
			else if (strcmp(Serial_RxPacket,"LED_OFF") == 0)
			{
				LED1_OFF();
				Serial_SendString("LED_OFF_OK\r\n");
				OLED_ShowString(2, 1, "                  ");
			    OLED_ShowString(2,1, "LED_OFF_OK");
			}
			else 
			{
				Serial_SendString("ERROR_COMMAND\r\n");
				OLED_ShowString(2, 1, "                  ");
			    OLED_ShowString(2,1, "ERROR_COMMAND");
			}
			
		}
		Serial_RxFlag =0;
	}
}

FlyMcu是串口下载,STLINK Utility是STLINK下载;

6.1 串口下载的原理:

在ROM区的0800位置,存储的是编译后的程序代码。

若想使用串口下载程序的话,只需要把程序数据通过串口发送给STM32,STM32接收数据,然后刷新到0800这一块位置就行了。

但接收并转存数据,这个过程本身也是程序,如何利用程序实现自我更新也是一个问题。

STM32通过串口进行程序的自我更新,就需要BootLoader。BootLoader是ST公司写好的一段程序代码,存储在ROM区的最后,1FFF F000,这段区域叫做系统存储器,存储的是BootLoader程序,或者叫自举程序,用途是程序自我更新,串口下载,在更新过程种,BootLoader接收USART1数据,刷新到程序存储器,这时主程序就处于瘫痪状态,更新好之后,再启动主程序,执行新程序,这就是串口下载的流程。

BOOT0为0时,就是主闪存,也就是0800的位置开始运行;BOOT0为1,BOOT1为0时,就是从系统存储器,也就是1FFF F000开始运行,BOOT0为1,BOOT1为1时,从SARM,也就是2000开始运行,

在系统复位后,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。所以每次切换BOOT引脚后,都要按一下复位。

6.2 每次下载都要切换跳线帽,怎么解决

BOOT0引脚和RST引脚必须得有高低电平变化

CH340模块中,RTS和DTR是输出引脚,可以用这两个引脚来控制BOOT0和RST,

但该外围还要设计一个控制电路,可以用两个三极管开关来进行控制,(STM32一键下载电路)

当串口具备一键下载电路之后,就不需要再频繁切换跳线帽和按复位键了,

一般配置是DTR的低电平复位,RTS高电平进BootLoader。

相关推荐
森旺电子2 小时前
51单片机仿真摇号抽奖机源程序 12864液晶显示
单片机·嵌入式硬件·51单片机
不过四级不改名6774 小时前
蓝桥杯嵌入式备赛教程(1、led,2、lcd,3、key)
stm32·嵌入式硬件·蓝桥杯
小A1594 小时前
STM32完全学习——SPI接口的FLASH(DMA模式)
stm32·嵌入式硬件·学习
Rorsion4 小时前
各种电机原理介绍
单片机·嵌入式硬件
善 .7 小时前
单片机的内存是指RAM还是ROM
单片机·嵌入式硬件
超级码农ProMax7 小时前
STM32——“SPI Flash”
stm32·单片机·嵌入式硬件
Asa3198 小时前
stm32点灯Hal库
stm32·单片机·嵌入式硬件
end_SJ9 小时前
初学stm32 --- 外部中断
stm32·单片机·嵌入式硬件
gantengsheng10 小时前
基于51单片机和OLED12864的小游戏《贪吃蛇》
单片机·嵌入式硬件·游戏·51单片机
嵌入式小强工作室11 小时前
stm32 查找进硬件错误方法
stm32·单片机·嵌入式硬件