STM32-USART

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

文章目录

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

1. 串口通信协议

1.1 通信接口

  • 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
  • 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
名称引脚双工时钟电平设备
USARTTX(发送)、RX(接收)全双工异步单端点对点
I2CSCL(时钟)、SDA(数据)半双工同步单端多设备
SPISCLK(时钟)、MOSI(主机输出数据脚)、MISO(主机输入数据脚)、CS(片选,用于指定通信的对象)全双工同步单端多设备
CANCAN_H、CAN_L(差分数据脚,用两个引脚表示一个差分数据)半双工异步差分多设备
USBDP、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 串口通信

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机地应用范围,增强了单片机系统的硬件实力。

image.png

  • 第一个是:USB转串口模块,内部有个芯片CH340,可以把串口协议转换为USB协议。一边是USB串口,接在电脑上,另一边是串口的引脚,可以和支持串口的芯片接在一起。
  • 中间是陀螺仪传感器的模块,可以测量角速度、加速度等姿态参数,一边是串口的引脚,一边是I2C的引脚。
  • 右边是蓝牙串口模块,下面4个引脚是串口通信的引脚。

1.3 硬件电路

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

image.png
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 串口参数及时序

image.png
image.png
串口中每一个字节都装载在一个数据帧里面,每个数据帧都由起始位、数据位和停止位组成,第一个图数据位有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 串口时序

image.png

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–>移位寄存器–>逐位发送
image.png

  • 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基本结构

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

2.4 数据帧

image.png

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

2.5 数据帧-配置停止位

发送器:
image.png
可以配置停止位长度为0.5、1、1.5、2四种,

2.6 起始位侦测

接收器:
image.png

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

2.7 数据采样

image.png
从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,
image.png

2.9 数据模式

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

image.png
image.png

3. USART库函数和代码

3.1 USART库函数

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 运行结果

//	传送一个字节
	Serial_SendByte(0x41);
	
//	传送数组
	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
	Serial_SendArray(MyArray, 4);

image.png

// 传送字符串,\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);

image.png

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. 串口代码:
#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:测试在串口代码中编写的函数
#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 运行结果

image.png
IMG_20240405_115049.jpg

image.png

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. 串口代码
#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
#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数据包

  • 固定包长,含包头包尾

image.png

  • 可变包长,含包头包尾

image.png

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

4.2 文本数据包

  • 固定包长,含包头包尾

image.png

  • 可变包长,含包头包尾

image.png

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

4.3 HEX数据包接收

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

4.4 文本数据包接收

image.png

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 运行结果

发送:

image.png
IMG_20240405_142058.jpg

接收:
image.png
IMG_20240405_142353.jpg

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 代码

串口代码:

#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代码:
#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,错误指令。

灯亮:
image.png
IMG_20240405_151525.jpg
灯灭:
image.png
IMG_20240405_151725.jpg

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 代码

串口代码:

#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代码:

#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;
	}
}

6. FlyMcu和STLINK Utility

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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/781275.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大连外贸建站公司wordpress主题模板

Robonaut萝卜纳特WP外贸站模板 适合用于工业机器人公司出口做外贸搭建公司官方网站使用的WordPress模板。 https://www.jianzhanpress.com/?p7091 优衣裳WordPress外贸建站模板 简洁的wordpress外贸独立站模板&#xff0c;适合服装、衣服、制衣外贸公司搭建公司官方网站使用…

ByteTrack论文阅读笔记

目录 ByteTrack: Multi-Object Tracking by Associating Every Detection Box摘要INTRODUCTION — 简介BYTE算法BYTE算法用Python代码实现实验评测指标轻量模型的跟踪性能 总结SORT算法简介ByteTrack算法和SORT算法的区别 ByteTrack: Multi-Object Tracking by Associating Eve…

location匹配和rewrite重定向

目录 location 匹配 location匹配的分类和优先级 优先级细分 实际网站中的使用规则 1.用精确匹配来实现网站的首页 访问网站的首页 &#xff08; /&#xff09; 2.用正则匹配来实现静态请求的页面和图片 匹配静态页面 访问图片或者指定的后缀名 3.用一般匹配转发.php…

【qt】TCP的监听 (设置服务器IP地址和端口号)

TCP监听是在自己的IP地址上进行的。 当一个TCP服务器程序启动时&#xff0c;它会绑定到一个特定的IP地址和一个端口号上&#xff0c;以便可以接收来自该IP地址和端口号的传入连接请求. 所以我们要先来获取主机的IP地址和设置端口号. 注意: 服务器程序无法任意设置IP地址&…

数据结构学生信息顺序表

主程序 #include "fun.h" int main(int argc, const char *argv[]) { seq_p Screate_seq(); stu data; printf("请问要输入几个学生的数据&#xff1a;"); int n; scanf("%d",&n); while(n--) { prin…

cloudflare tunnels tcp

这里是官网的说明Cloudflare Tunnel Cloudflare Zero Trust docs 根据实际情况安装环境 tunnels除了http,https协议是直接暴露公网&#xff0c;tcp是类似ssh端口转发。 在需要内网穿透的局域网找一条机子部署代理 我这边是window cloudflared tunnel login #生成一个身份校…

防火墙概述

1、防火墙 防火墙顾名思义就是防止火灾发生时&#xff0c;火势烧到其它区域&#xff0c;使用由防火材料砌的墙。在网络安全中&#xff0c;防火墙的作用就是保护本地网络不受到外部网络或恶意程序的伤害。 防火墙的核心任务是控制和防护&#xff0c;即通过安全策略识别流量并做…

【周末闲谈】AI“抢饭碗”?绝对不是危言耸听

AI是在帮助开发者还是取代他们? 在软件开发领域,生成式人工智能(AIGC)正在改变开发者的工作方式。无论是代码生成、错误检测还是自动化测试,AI工具正在成为开发者的得力助手。然而,这也引发了对开发者职业前景和技能需求变化的讨论。AI究竟是在帮助开发者还是取代他们?…

【论文阅读】-- Visual Analytics for Model Selection in Time Series Analysis

时间序列分析中模型选择的可视化分析 摘要1 引言2 相关工作3 问题表征3.1 Box-Jenkins 方法论3.2 ARIMA 和季节性 ARIMA 模型3.3 模型规范3.4 模型拟合3.5 模型诊断 4 需求分析5 VA 用于时间序列分析中的模型选择5.1 VA选型流程说明5.2 TiMoVA 原型5.2.1 实施选择5.2.2 图形用户…

【JavaSE复习】数据结构、集合

JavaSE 复习 1.数据结构1.1 查找1.1.1 基本查找1.1.2 二分查找1.1.3 插值查找1.1.4 斐波那契查找1.1.5 分块查找1.1.6 分块查找的扩展&#xff08;无规律数据&#xff09; 1.2 排序1.2.1 冒泡排序1.2.2 选择排序1.2.3 插入排序1.2.4 快速排序 2. 集合2.1 基础集合2.1.1 集合和数…

MyBatis中二级缓存的配置与实现原理

大家好&#xff0c;我是王有志&#xff0c;一个分享硬核 Java 技术的金融摸鱼侠&#xff0c;欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。 上一篇文章《MyBatis中一级缓存的配置与实现原理》中&#xff0c;我们已经掌握了 MyBatis 一级缓存的配置&#xff08;虽然…

使用AOP思想实现开闭原则下的流水日志输出

主要实现思想&#xff1a; 通过实现Convert接口来抽取公共组件&#xff0c;获取想要的标准模型。 现在有两个订单场景&#xff0c;一个保存订单&#xff0c;一个为更新订单。构造如下的服务类&#xff1a; import org.springframework.stereotype.Service;Service public clas…

pwm 呼吸灯(如果灯一直亮或者一直灭)

&#xff08;这个文章收藏在我的csdn keil文件夹下面&#xff09; 如果这样设置预分频和计数周期&#xff0c;那么算出来的pwm频率如下 人眼看起来就只能是一直亮或者灭&#xff0c;因为pwm的频率太高了&#xff0c;但是必须是频率够高&#xff0c;才能实现呼吸灯的缓慢亮缓慢…

Django之项目开发(一)

一、项目的生命周期介绍 传统Web 项目的生命周期指的是从开始构建一个网站到该网站完成并维护的整个过程。通常情况下,Web 项目的生命周期包括以下几个阶段 需求分析阶段:在这个阶段,项目组会与客户进行沟通,确定网站的功能、内容和设计。 主要由产品经理参与产出思路与方案…

ChatGPT-4o大语言模型优化、本地私有化部署、从0-1搭建、智能体构建等高级进阶

目录 第一章 ChatGPT-4o使用进阶 第二章 大语言模型原理详解 第三章 大语言模型优化 第四章 开源大语言模型及本地部署 第五章 从0到1搭建第一个大语言模型 第六章 智能体&#xff08;Agent&#xff09;构建 第七章 大语言模型发展趋势 第八章 总结与答疑讨论 更多应用…

Nginx auth 的权限验证

基本流程 整个流程为&#xff1b;以用户视角访问API开始&#xff0c;进入 Nginx 的 auth 认证模块&#xff0c;调用 SpringBoot 提供的认证服务。根据认证结果调用重定向到对应的 API 接口或者 404 页面。 查看版本保证有 Nginx auth 模块 由于 OpenAI 或者本身自己训练的一套…

数据结构(其一)--基础知识篇

1. 数据结构三要素 1.1 数据结构的运算 即&#xff0c;增删改查 1.2 数据结构的存储结构 2. 数据类型&#xff0c;抽象数据类型 数据类型&#xff1a; &#xff08;1&#xff09;. 原子类型&#xff1a;bool、int... &#xff08;2&#xff09;. 结构类型&#xff1a;类、…

Linux多线程(中)

Linux多线程&#xff08;中&#xff09; 1.Linux线程互斥1.1互斥量的接口1.1.1初始化互斥量1.1.2销毁互斥量1.1.3互斥量加锁和解锁 1.2修改代码1.3互斥量实现原理 2.可重入VS线程安全3.死锁4.Linux线程同步5.生产者消费者模型 &#x1f31f;&#x1f31f;hello&#xff0c;各位…

Java 自定义集合常量

文章目录 Java 自定义集合常量一、普通方法自定义集合常量信息1、定义 Map 集合信息&#xff08;1&#xff09;方法一&#xff1a;使用静态代码块&#xff08;2&#xff09;方法二&#xff1a;简单定义 Map 常量 2、定义 List 集合信息3、定义 Set 集合信息 二、通过 Collectio…

用win的控制台去远程连接虚拟机linux的终端

以Ubuntu为例&#xff0c;首先确保Ubuntu已经安装了ssh服务 sudo apt-get install openssh-server输入密码 安装完毕后查看ssh状态是否开启 sudo systemctl status ssh 显示绿色激活状态&#xff0c;可以关闭或开启 对应start和stop winr打开win端控制台 输入 ssh -p 22 …