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。

相关推荐
智商偏低6 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen7 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森9 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白9 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D10 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术13 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt13 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘14 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang14 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n16 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件