重庆渝网站建设,怎么做网上卖货,合肥论坛,教室在线设计网站9 USART串口 文章目录9 USART串口9.1 串口通信协议9.2 stm32的片上外设-USART9.3 USART收发相关实验9.3.1 实验1#xff1a;串口发送9.3.2 实验2#xff1a;移植printf函数9.3.3 实验3#xff1a;串口发送接收9.4 USART串口数据包9.5 USART数据包相关实验9.5.1 实验1#x…9 USART串口 文章目录9 USART串口9.1 串口通信协议9.2 stm32的片上外设-USART9.3 USART收发相关实验9.3.1 实验1串口发送9.3.2 实验2移植printf函数9.3.3 实验3串口发送接收9.4 USART串口数据包9.5 USART数据包相关实验9.5.1 实验1串口收发HEX数据包9.5.2 实验2串口收发文本数据包9.6 软件使用FlyMcu串口下载  STLINK Utility注笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。 注工程及代码文件放在了本人的Github仓库。 9.1 串口通信协议 
从本节开始将逐一学习STM32的通信接口。首先介绍以下stm32都集成了什么通信外设。 
为了控制或读取外挂模块stm32需要与外挂模块进行通信来扩展硬件系统。而这个“通信”的过程就需要遵守相应的“通信协议”也就是通信双方需要按照协议规则进行数据收发。不同外挂模块的会采用不同的通信协议如下表 
表9-1 stm32片上集成的通信模块外设 名称引脚双工时钟电平设备USARTTX/TXD、RX/RXD全双工异步单端点对点I2CSCL、SDA半双工同步单端多设备SPISCLK、MOSI、MISO、CS全双工同步单端多设备CANCAN_H、CAN_L半双工异步差分多设备USBDP/D、DM/D-半双工异步差分点对点 下面介绍一下引脚的全称 USARTTX(Transmit Exchange)数据发送脚、RX(Receive Exchange)数据接收脚。IICSCL(Serial Clock)时钟线、SDA(Serial Data)数据线。SPIMOSI(Master Output Slave Input)主机输出数据脚、MISO(Master Input Slave Output)主机输入数据脚、CS(Chip Select)片选USBDP(Data Postive)差分线正、DM(Data Minus)差分线负 注上述协议中单端电平都需要共地。 注使用差分信号可以抑制共模噪声可以极大的提高信号的抗干扰特性所以一般差分信号的传输速度和传输距离都非常高。 本节将介绍串口。串口是一种应用十分广泛的通讯接口串口成本低、容易使用、通信线路简单可实现两个设备的互相通信。单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信极大地扩展了单片机的应用范围增强了单片机系统的硬件实力。 
一般单片机中都会有串口的通信外设。在单片机领域中相比于IIC、SPI等协议串口是一种非常简单的通信接口只支持点对点的通信。单片机与电脑通信是串口的一大优势可以外接电脑屏幕非常适合调试程序、打印信息……IIC或SPI协议一般都是芯片之间的通信有片选引脚所以支持总线通信而不会直接外接在电脑上。 图9-1 串口模块实例 USB转串口模块使用CH340芯片可以将串口协议转换成USB协议。使用此模块可以实现单片机与电脑的通信。陀螺仪模块左侧是串口引脚右侧是IIC引脚。可以用于测量角速度、角速度等姿态参数。蓝牙串口模块上面的芯片可以和手机互联下面四个引脚是串口引脚。此芯片可以实现手机遥控单片机的功能。 图9-2 串口引脚接线图 下面介绍一些串口引脚的注意事项 TX与RX简单双向串口通信有两根通信线发送端TX和接收端RX要交叉连接。不过若仅单向的数据传输可以只接一根通信线。GND一定要共地。由于TX和RX的高低电平都是相对于GND来说的所以GND严格来说也算是通信线。VCC相同的电平才能通信如果两设备都有单独的供电VCC就可以不接在一起。但如果某个模块没有供电就需要连接VCC注意供电电压要按照模块要求来必要时需要添加电压转换电路。 电平标准是数据1和数据0的表达方式是传输线缆中人为规定的电压与数据的对应关系串口常用的电平标准有如下三种 TTL电平【单片机】3.3V或5V表示10V表示0。一般低压小型设备使用的都是TTL电平。传输范围几十米。RS232电平-3~-15V表示13~15V表示0。一般在大型机器上使用由于环境比较恶劣静电干扰比较大所以通信电压都很大并且允许波动的范围也很大。传输范围几十米。RS485电平两线压差2~6V表示1-2~-6V表示0差分信号。抗干扰能力极强通信距离可达上千米。 注不同电平标准之间的转换只需要加电压转换芯片即可并不需要修改相应的软件代码。 上面介绍了串口协议的硬件部分如何表示1/0下面来介绍串口协议的软件部分如何使用1/0组成字节数据。 图9-3 串口的单字节发送格式 下面介绍串口的参数 波特率串口通信的速率bit/s也就是通信双方所约定的通信速率异步通信。空闲状态固定为高电平。起始位固定为低电平标志一个数据帧的开始。数据位低位先行数据帧的有效载荷1为高电平0为低电平。校验位选填用于数据验证根据数据位计算得来。停止位固定为高电平用于表示数据帧的间隔同时也可以使得通信线回归到空闲状态。可以配置停止位是1位/2位。 图9-4 串口时序 上图给出了几个例子来展示串口通信的时序图 右侧最后两个图展示了不同长度停止位的现象。 注在stm32中根据发送数据自动转换发送波形、或根据波形自动读取数据都是由USART外设自动完成的无需软件控制每一位的发送或读取。 9.2 stm32的片上外设-USART 
USARTUniversal Synchronous/Asynchronous Receiver/Transmitter通用同步/异步收发器 是STM32内部集成的硬件外设可根据数据寄存器的一个字节数据 自动生成数据帧时序从TX引脚发送出去也可 自动接收RX引脚的数据帧时序拼接为一个字节数据存放在数据寄存器里。USART中的“S”表示同步只支持时钟输出不支持时钟输入是为了兼容别的协议或特殊用途而设计的并不支持两个USART之间进行同步通信所以这个功能几乎不会用到一般更常使用的是UART同步异步收发器。下面是一些参数 自带波特率发生器最高达4.5Mbits/s常用9600/115200。可配置数据位长度8/9、停止位长度0.5/1/1.5/2。可选校验位无校验常用/奇校验/偶校验。支持同步模式一般不用、硬件流控制指示从设备准备好接收的信号一般不用、DMA、智能卡、IrDA手机红外通信但并不是红外遥控目前很少见、LIN局域网的通信协议。STM32F103C8T6 USART资源USART1(APB2)、USART2(APB1)、USART3(APB1)。 表9-2 串口引脚定义和引脚复用关系 引脚USART1USART2USART3TXPA9/PB6PA2PB10RXPA10/PB7PA3PB11CKPA8PA4PB12CTSPA11PA0PB13RTSPA12PA1PB14注斜杠后面的引脚表示重定义 图9-5 USART框图 发送数据的过程某时刻给“TDR”写入数据0x55此时硬件检测到写入数据就会检查当前“发送移位寄存器”是否 有数据正在进行移位若正在移位就会等待移位完成若没有移位就会将TDR中的数据立刻移动到“发送移位寄存器”中准备发送。然后“发送移位寄存器”就会在下面的“发送器控制”的驱动下向右一位一位的移位低位先行将数据输出到TX发送引脚。移位完成后新的数据会再次自动的从TDR转移到“发送移位寄存器”中来。有了TDR和“发送移位寄存器”的双重缓存可以保证连续发送数据时数据帧之间不会有空闲。写入数据的时机当数据从TDR移动到“发送移位寄存器”时标志位TXE置位TX Empty发送寄存器空此时检测到TXE置位就可以继续写入下一个数据但注意此时上一个数据实际上还没发送出去。接收数据的过程数据从RX引脚通向“接收移位寄存器”在“接收器控制”的驱动下一位一位的读取RX电平并放在最高位整个过程不断右移低位先行最终就可以接收1个字节。当一个字节移位完成后这一个字节的数据就会整体地一下子转移到“接收数据寄存器RDR”中。然后就准备继续读取下一帧数据。同样RDR和“接收移位寄存器”也组成了双重缓存结构可以保证连续读取数据。读取数据的时机接收数据从“接收移位寄存器”转移到RDR的过程中标志位RXNE置位RX Not Empty接收数据寄存器非空。所以当检测到标志位RXNE置位时就可以将数据读走。 下面来看看其他的硬件电路功能 发送器控制控制“发送移位寄存器”工作。接收器控制控制“接收移位寄存器”工作。硬件数据流控也就是“硬件流控制”简称“流控”。如果主设备连续发送导致从设备内部无法及时处理接收数据就会出现数据丢弃或覆盖的现象注意不是UART没接收到而是从设备没有及时将数据从RDR取走此时“流控”就可以帮助从设备向主设备发信号指明自己还没有准备好接收数据主设备也就不会发送数据了。本教程不涉及。  nRTSRequest To Send输出脚请求发送。也就是告诉主设备当前是否已经准备好接收。前方“n”表示低电平有效。nCTSClear To Send输入脚清除发送。用于接收从设备的nRTS信号。前方“n”表示低电平有效。  SCLK用于产生“同步功能”的时钟信号配合“发送移位寄存器”输出用于给从设备提供时钟。注意没有同步时钟输入所以两个USART之间不能同步通信。唤醒单元实现串口挂载多设备。前面提到串口一般是点对点通信但是这个模块通过给串口分配地址“USART地址”就可以决定是否唤醒USART工作进而实现了总线通信。状态寄存器SR存储着串口通信的各种标志位其中比较重要的有发送寄存器空TXE、接收数据寄存器非空RXNE。USART中断控制中断输出控制配置中断是否可以通向NVIC。其中断申请位就是状态寄存器SR中的各种标志位。波特率发生器部分其实就是分频器APB时钟进行分频得到发送和接收移位的时钟。  fPCLKx(x1,2)时钟输入。由于UASRT1挂载在APB2上所以 时钟输入fPCLKx(x1,2) 就是PCLK2的时钟一般为72MHz。而UASRT2、USART3都挂载在APB1上所以 时钟输入fPCLKx(x1,2) 就是PCLK1的时钟一般为36MHz。/UASRTDIV时钟分频系数。内部结构也就是虚线框中的“传统的波特率发生器”。/16再进行16分频得到最终的“发送器时钟”、“接收器时钟”。  注发送时添加开始位、停止位接收时去除开始位、停止位这些工作由内部硬件电路自动完成。 注更多关于控制寄存器CR、状态寄存器SR的描述可以查阅参考手册“25.6 USART寄存器描述”。 图9-6 USART基本结构 上图给出USART最主要、最基本的结构 波特率发生器用于产生约定的通信速率。时钟来源是PCLK2/PCLK1经过波特率发生器分频后产生的时钟通向发送控制器、接收控制器。发送控制器、接收控制器用于控制发送移位、接收移位。GPIO发送端配置成复用推挽输出、接收端配置成上拉输入。标志位TXE置位时写入数据、RXNE置位时接收数据。开关控制用于开启整个USART外设。 下面来看几个细节的问题 细节1数据帧 图9-7 字长的配置 上图给出了8位字长无校验位、9位字长有校验位的波形 时钟上升沿可以看到每个数据中间都有一个时钟上升沿所以接收端采样时刻就是时钟上升沿。时钟的极性、相位等都可以通过配置寄存器配置。空闲帧、断开帧用于局域网协议。尽量选择9位字长有校验、8位字长无校验。 图9-8 停止位的配置 stm32串口外设的停止位长度可以配置成0.5/1/1.5/1位共四种选择区别就是停止位的时长不一样。 一般就选择停止位长度为1位。 细节2USART输入数据采样规则 串口的输出TX只需要保持相应时长的电平即可电路简单但是串口输入RX则需要判断电平持续时间所以电路会更加复杂所以下面来详细介绍stm32串口外设对于输入数据的采样。 图9-9 对于起始位的采样 注意到采样时钟是波特率的16倍即会对某一位采样16次。 检测下降沿若突然检测到下降沿则开始进行检测。检测3、5、7位在第3、5、7间隔采样采样判断原则为若三位全为0则正常进入后续若有2个0则还是会接着检测但噪声标志位NE(Noise Error)置位告诉用户我这儿接收的信号有噪声你悠着点用若低于2个0则认为之前检测到的下降沿为噪声忽略已经捕获的数据重新回到空闲状态开始捕捉下降沿。检测8、9、10位连续采样。采样判断原则与上述相同。 图9-10 对于数据位的采样 由于起始位采样已经对齐了数据时钟所以数据采样就直接在8、9、10位采样。 数据采样的判断原则三个数据中0/1哪个数据多就判断为接收到哪个。但是如果三个数据有不一致的情况噪声标志位NE(Noise Error)置位。 细节3计算分频系数DIV 图9-11 波特比率寄存器示意图 发送器和接收器的波特率由波特率寄存器BRR里的分频系数DIV确定 波特率fPCLK2/116∗DIV波特率  \frac{f_{PCLK2/1}}{16 * DIV} 波特率16∗DIVfPCLK2/1 
例如若输入时钟为fPCLK2/172MHzf_{PCLK2/1} 72MHzfPCLK2/172MHz希望配置波特率为9600则分频系数为DIV72M16∗9600468.75DIV\frac{72M}{16*9600}468.75DIV16∗960072M468.75转换成二进制为111010100.11于是USART_BRR的值为0001_1101_0100_1100(高位补零、低位补零)。 
比较方便的是上述过程都可以使用USART的外设库函数实现调用时只需输入波特率库函数会自动计算出DIV并按照格式配置好BRR寄存器。 注十进制转二进制工具为菜鸟工具的 “在线进制转换器”。 细节4USB转串口模块 图9-12 USB转串口模块-原理图 主要关注的是该模块的供电情况。 USB插座直接插在电脑USB端口上注意整个模块的供电来自于USB的VCC5V。CON6插针座  引脚2、引脚3用于连接到stm32上进行串口通信。引脚5【CH340_VCC】通过跳线帽可以选择 接入3.3Vstm32 或者5V。CH340芯片的供电引脚同时决定了TTL所以也就是串口通信的TTL电平。神奇的是即使不接跳线帽CH340也可以正常工作TTL为3.3V但是显然接上电路以后更加稳定。通信和供电的选择CON6插针座选择引脚4/6进行通信后剩下的引脚可以用于给从设备供电但是剩下的这个脚显然与TTL电平不匹配。此时需要注意 优先保证供电电平的正确通信TTL电平不一致问题不大。当然若从设备自己有电源那么就不存在这个问题了。  TXD指示灯、RXD指示灯若相应总线上有数据传输那么指示灯就会闪烁。 细节5数据模式 显然计算机在通信过程中只能传输二进制数据那么如何发送文本呢就需要制定一个规则来约定字符和接收数据的映射关系即字符编码表。不同的编码格式有不同的映射具体可以在网上随便搜搜“字符编码格式”如知乎文章“字符常见的编码方式”。 HEX模式/十六进制模式/二进制模式以原始数据的形式显示 文本模式/字符模式以原始数据编码后的形式显示。 图9-13 ASCII码表-UP自己做的高清版 9.3 USART收发相关实验 
9.3.1 实验1串口发送 
需求在软件代码中定义要发送的信息然后通过串口发送到电脑端使用“串口助手”小工具查看。要求依次发送单字节数据、数组、字符串、数据的每一位。 注串口助手可以切换“文本模式”/“HEX模式”。 注数字和字符的对应关系可以参考ASCII码表。 图9-14 串口发送-接线图 注接线图也可以不接OLED显示屏。 图9-15 串口发送-代码调用非库函数 下面是代码展示 - main.c 
#include stm32f10x.h                  // Device header
#include SerialPort.hint main(void){uint8_t send_byte  0x42;uint8_t send_array[6]  {0x30,0x31,0x32,0x33,0x34,0x35};//串口初始化SerialPort_Init();//发送单个字节SerialPort_SendByte(A);//可以直接发送字符SerialPort_SendByte(send_byte);SerialPort_SendByte(\r);SerialPort_SendByte(\n);//发送数组SerialPort_SendArray(send_array,6);SerialPort_SendByte(\r);SerialPort_SendByte(\n);//发送字符串SerialPort_SendString(Hello World!\r\n);//发送数字的每一位SerialPort_SendNum(65535, 5);SerialPort_SendString(\r\n);while(1){
//        //循环发送数字
//        SerialPort_SendByte(send_byte);
//        OLED_ShowHexNum(1,9,send_byte,2);
//        send_byte;
//        Delay_ms(1000);};
} 
- SerialPort.h 
#ifndef __SERIALPORT_H
#define __SERIALPORT_Hvoid SerialPort_Init(void);
void SerialPort_SendByte(uint8_t send_byte);
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array);
void SerialPort_SendString(char *send_string);
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len);#endif 
- SerialPort.c 
#include stm32f10x.h                  // Device header//串口初始化-USART1
void SerialPort_Init(void){//1.开启RCC外设时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//2.初始化GPIO-PA9GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode  GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin  GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed  GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure);//3.初始化USART结构体USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate  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;USART_InitStructure.USART_WordLength  USART_WordLength_8b;USART_Init(USART1, USART_InitStructure);//4.配置中断开启NVIC接收数据使用//5.开启外设USART_Cmd(USART1, ENABLE);
}//串口发送1字节数据
void SerialPort_SendByte(uint8_t send_byte){//向发送数据寄存器TDR中写入数据USART_SendData(USART1, send_byte);//确认数据被转移到发送移位寄存器等待标志位TXE置位while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)RESET);
}//发送一个数组
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array){uint8_t i0;for(i0;isize_array;i){SerialPort_SendByte(send_array[i]);}
}//发送一个字符串
void SerialPort_SendString(char *send_string){uint8_t i0;for(i0; send_string[i]!\0; i){SerialPort_SendByte(send_string[i]);}
}//非外部调用函数-幂次函数
uint32_t SerialPort_Pow(uint32_t X, uint32_t Y){uint32_t result  1;while(Y--){result * X;}return result;
}//发送数字的每一位-先发高位
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len){uint16_t i;for(i0;isend_len;i){SerialPort_SendByte((send_num/SerialPort_Pow(10,send_len-i-1))%100);}
} 
编程感想 关于接线。注意串口通信的两个设备是交叉连接的所以“USB转串口模块”的TX应该接在stm32串口的RXPA10、RX应该接在stm32串口的TXPA9关于sizeof。相信很多人也想到在封装发送数组的函数时为什么不直接在函数中使用sizeof函数来计算数组的大小呢这样能少传递一个参数更简洁。但实际上这里面的水很深这样操作是不能得到正确结果的具体原理可以参考CSDN博文“数组名不等于指针”。最终结论就是数组名在传参过程中会退化成一个指针此时所表示的大小不是数组的实际大小而是这个指针的大小所以要想在函数中使用到数组大小最好还是多传递一个参数。 9.3.2 实验2移植printf函数 
需求将C语言自带函数printf进行封装默认成将需要打印的数据发送到串口进而可以显示在电脑端串口助手上。 
接线图、函数调用非库函数 与上一小节实验“串口发送”相同。本节本人目前也不懂原理所以下面直接给出 代码展示 
方法一 
首先点击“魔术棒”在Target界面的“Code Generation”方框中勾选“USE MicroLIB”。MicroLIB是Keil为嵌入式平台优化的一个精简库要使用printf函数就会用到这个MicroLIB精简库。对printf函数重定向将printf函数打印的东西输出到串口。于是在 SerialPort.c 模块中添加下列代码 
#include stdio.h//对printf函数重定向-将fputc函数原型重定向到串口
//注ptintf函数本质上就是循环调用fputc将字符一个一个输出
int fputc(int ch, FILE *f){SerialPort_SendByte(ch);return ch;
}在 SerialPort.h 模块中添加下列代码 
#include stdio.h于是就可以在 main.c 中调用printf函数将数据输出到串口了。 
//使用重定向的printf函数printf(%d\r\n,666);但注意这个方法直接将printf函数重定向到了USART1别的USART外设USART2、USART3等想用就没办法了。所以有如下的改进方法。 
方法二 若多个串口都想使用printf函数那么就可以使用sprintf函数。sprintf函数可以将格式化字符输出到一个字符串里然后再调用相应的“串口发送字符串”函数发送这个字符串整个过程不涉及重定向于是就实现了所有USART外设都可以打印信息到串口了。所以下面可以直接在 main.c 中定义 
char String[100];//定义一个足够长的字符串数组
sprintf(String, Num%d\r\n, 666);//将格式化字符串存储在String中
SerialPort_SendString(String);//串口发送字符串方法三 方法二的sprintf函数很方便但是直接在主函数中写比较麻烦于是本方法就是来封装“方法二”。具体方法如下 
C语言可变参数。在串口模块SerialPort.c中添加下面代码 
#include stdarg.h//对sprintf函数进行封装
void SerialPort_Printf(char *format,...){char String[100];//定义输出的字符串va_list arg;//定义参数列表变量va_start(arg, format);//从format位置开始接收参数表放在arg里面vsprintf(String, format, arg);//sprintf只接收直接写的参数对于封装格式改用vsprintfva_end(arg);//释放参数表SerialPort_SendString(String);  
}
//注意上述函数要在头文件中声明但头文件就不需要再添加stdarg.h了。直接在主函数中调用 
SerialPort_Printf(Num%d\r\n, 888);特殊说明显示汉字 使用上面几种printf函数时有可能会出现乱码要解决这个问题关键是要保持Keil编译器“扳手”图标Editor界面的Encoding下拉菜单和“串口助手”的文本编码一致。可以选择以下两种情况 两者都选择UTF-8编码。但是直接在字符串写汉字编译器有可能报错解决方法是点击“魔术棒”–C/C±-最下面的Controls输入 --no-multibyte-chars即可。有些串口助手软件可能不兼容UTF-8所以两者都需要选择GB2312编码。 注更改编译器文本编码格式后需要将文件全部关闭重新打开否则编码方式不会改变。 9.3.3 实验3串口发送接收 
需求电脑端发送数据stm32接收到数据后将数据在OLED显示屏上显示出来、并且回传到电脑。 程序整体思路 查询。主函数不断查询RXNE标志位但是会占用很多的CPU资源所以不推荐。中断。推荐方法下面的演示也是基于此方法。 本实验的 接线图 与前两小节的实验均相同下面给出 代码调用 图9-16 串口发送接收-代码调用非库函数 下面是 代码展示仅给出在串口模块中增添的函数 - main.c 
#include stm32f10x.h                  // Device header
#include OLED.h
#include SerialPort.hint main(void){    uint8_t Rx_byte  0;//串口接收的单比特数据//设置中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//OLED初始化OLED_Init();OLED_ShowString(1,1,Rx_byte:);//串口初始化SerialPort_Init();while(1){if(SerialPort_GetRxFlag()1){Rx_byte  SerialPort_GetRxData();OLED_ShowHexNum(1,9,Rx_byte,2);SerialPort_SendByte(Rx_byte);}};
} 
- SerialPort.c 
uint8_t SerialPort_RxData  0;
uint8_t SerialPort_RxFlag  0;//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){if(SerialPort_RxFlag1){SerialPort_RxFlag  0;return 1;}else{return 0;}
}//获取接收的数据
uint8_t SerialPort_GetRxData(void){return SerialPort_RxData;
}//USART1_RXNE中断函数
void USART1_IRQHandler(void){if(USART_GetITStatus(USART1, USART_IT_RXNE)SET){SerialPort_RxFlag  1;SerialPort_RxData  USART_ReceiveData(USART1);//读操作可以自动清零标志位但加上也没事USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}
//不要忘了将前两个函数在头文件中声明9.4 USART串口数据包 
数据包的作用是将一个个单独的数据打包方便进行多字节的数据通信。因为实际应用中经常需要进行数据打包。比如陀螺仪传感器需要将数据发送到stm32其中包括X轴、Y轴、Z轴三个字节循环不断的发送若采用一个一个进行发送的方式接收方就有可能分不清对应的顺序进而出现数据错位现象。此时若能将同一批数据进行分割和打包就可以方便接收方识别。 
1. 数据包格式的定义 图9-17 HEX数据包示意图 若载荷数据与包头、包尾一样怎么办呢有三种解决思路 限制载荷数据的范围。使其不会与包头、包尾重复。尽量使用固定长度的数据包。只要数据长度固定那么就可以通过包头、包尾定位数据。增加包头包尾的数量使其尽量呈现出载荷数据不会出现的状态。 注包尾可以去除。但是这样会使得载荷数据和包头重复的问题更加严重。 若想发送16位整型数据、32位整型数据、float、double、结构体等只需使用 uint8_t型指针 指向这些数据就可以进行发送(将各种数据转换成字节流)。 图9-18 文本数据包示意图 文本数据包中每个数据都经过了一层编码和译码。 由于包头包尾非常容易唯一确定文本数据包基本不用担心载荷数据和包头包尾重复的问题。 
优缺点比较 HEX数据包  优点传输最直接解析数据非常简单比较适合一些模块发送最原始的数据。如使用串口通信的陀螺仪、温湿度传感器。缺点灵活性不足载荷容易和包头包尾重复。  文本数据包  优点数据直观易理解非常灵活比较适合一些输入指令进行人机交互的场合。如蓝牙模块常用的AT指令、CNC和3D打印机常用的G代码都是文本数据包的格式。缺点解析效率低。  2. 数据包格式的收发流程 数据包发送是非常简单的直接发就完事儿了。但是接收数据包的过程比较复杂这是就要考虑使用状态机。 图9-19 接收HEX数据包-状态机 图9-20 接收文本数据包-状态机 9.5 USART数据包相关实验 
9.5.1 实验1串口收发HEX数据包 
需求自定义数据包格式使用串口完成指定格式的数据包收发并将收发结果显示在OLED上。另外按键的功能是将发送的当前存储的发送数据全部加1再发送出去。 数据包头0xFF。载荷数据固定数据段长度为4个字节。数据包尾0xFE。 图9-21 串口收发HEX数据包-接线图 图9-22 串口收发HEX数据包-代码调用非库函数 下面是代码展示其中将串口的模块的数据接收部分全部删除重写。 - main.c 
#include stm32f10x.h                  // Device header
#include OLED.h
#include SerialPort.h
#include Key.hint main(void){//存储串口接收的HEX数据包uint8_t *Rx_Packet  SerialPort_GetRxPacket();//存储串口发送的HEX数据包uint8_t Tx_Packet[4]  {0x01,0x02,0x03,0x04};//设置中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//OLED初始化OLED_Init();OLED_ShowString(1,1,Rx_Packet:);OLED_ShowString(3,1,Tx_Packet:);//串口初始化SerialPort_Init();//按键初始化Key_Init();while(1){//显示接收到的数据if(SerialPort_GetRxFlag()1){OLED_ShowHexNum(2,1,*Rx_Packet,2);OLED_ShowHexNum(2,4,*(Rx_Packet1),2);OLED_ShowHexNum(2,7,*(Rx_Packet2),2);OLED_ShowHexNum(2,10,*(Rx_Packet3),2);}//检测按键发送数据包到电脑if(Key_GetNum()1){Tx_Packet[0];Tx_Packet[1];Tx_Packet[2];Tx_Packet[3];SerialPort_SendPacket(Tx_Packet);OLED_ShowHexNum(4,1,Tx_Packet[0],2);OLED_ShowHexNum(4,4,Tx_Packet[1],2);OLED_ShowHexNum(4,7,Tx_Packet[2],2);OLED_ShowHexNum(4,10,Tx_Packet[3],2);}};
} 
- SerialPort.c新增函数 
uint8_t SerialPort_RxPacket[4];
uint8_t SerialPort_RxPacketFlag  0;//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){if(SerialPort_RxPacketFlag1){SerialPort_RxPacketFlag  0;return 1;}else{return 0;}
}//获取接收的HEX数据包
uint8_t* SerialPort_GetRxPacket(void){return SerialPort_RxPacket;
}//发送HEX数据包
void SerialPort_SendPacket(uint8_t *send_array){SerialPort_SendByte(0xFF);SerialPort_SendArray(send_array, 4);SerialPort_SendByte(0xFE);
}//USART1_RXNE中断函数
void USART1_IRQHandler(void){uint8_t rec_byte;static uint8_t rx_state;static uint8_t rx_index;if(USART_GetITStatus(USART1, USART_IT_RXNE)SET){rec_byte  USART_ReceiveData(USART1);//利用状态机接收HEX数据包if(rx_state0){if(rec_byte0xFF){rx_index  0;rx_state  1;}}else if(rx_state1){SerialPort_RxPacket[rx_index]  rec_byte;rx_index;if(rx_index4){rx_state  2;}}else if(rx_state2){if(rec_byte0xFE){SerialPort_RxPacketFlag  1;rx_state  0;}}//读操作可以自动清零标志位但加上也没事USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}
//除了中断函数其余函数还要在头文件SerialPort.h中声明编程感想 数据混叠。若电脑端连续发送数据包而stm32处理不及时会导致数据错位。但是一般像传感器模块等的数据都具有连续性所以就算数据错位也没关系。发送数据不匹配。注意发送字节数据一定要写成0x11的形式而不是直接写一个11进行发送。收发数据没反应。注意一定要在最开始声明的地方赋初值否则有可能读不出数据。当然还有一种可能是串口连接不稳定可以重新拔插一下串口。 9.5.2 实验2串口收发文本数据包 
需求使用电脑端发送指定格式的文本数据包来控制单片机点亮或熄灭LED灯单片机完成指令后再将接收的状态回传到电脑。 数据包头。数据包有效指令为LED_ON\r\n、“LED_OFF\r\n”。不定字长数据包尾\r\n。 图9-23 串口收发文本数据包-接线图 图9-24 串口收发文本数据包-代码调用非库函数 下面是代码展示 - main.c 
#include stm32f10x.h                  // Device header
#include OLED.h
#include SerialPort.h
#include LED.h
#include string.hint main(void){//存储串口接收的HEX数据包char *Rx_Packet  SerialPort_GetRxPacket();//设置中断分组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//OLED初始化OLED_Init();OLED_ShowString(1,1,Rx_Packet:);OLED_ShowString(3,1,Tx_Packet:);//串口初始化SerialPort_Init();//LED初始化LED_Init();while(1){//对接收到的文本进行判断if(SerialPort_GetRxFlag()1){//OLED显示接收到的文本OLED_ShowString(2,1,                );OLED_ShowString(2,1,Rx_Packet);//根据接收的内容执行相应的动作if(strcmp(Rx_Packet, LED_ON)0){LED1_ON();OLED_ShowString(4,1,                );OLED_ShowString(4,1,LED_ON_OK);SerialPort_SendString(LED_ON_OK\r\n);}else if(strcmp(Rx_Packet, LED_OFF)0){LED1_OFF();OLED_ShowString(4,1,                );OLED_ShowString(4,1,LED_OFF_OK);SerialPort_SendString(LED_OFF_OK\r\n);}else{OLED_ShowString(4,1,                );OLED_ShowString(4,1,ERROR_COMMAND);SerialPort_SendString(ERROR_COMMAND\r\n);}}};
} 
- SerialPort.c新增函数将上一节HEX数据包部分全部删除 
char SerialPort_RxPacket[100];
uint8_t SerialPort_RxPacketFlag  0;//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){if(SerialPort_RxPacketFlag1){SerialPort_RxPacketFlag  0;return 1;}else{return 0;}
}//获取接收的HEX数据包
char* SerialPort_GetRxPacket(void){return SerialPort_RxPacket;
}//USART1_RXNE中断函数
void USART1_IRQHandler(void){uint8_t rec_byte;static uint8_t rx_state;static uint8_t rx_index;if(USART_GetITStatus(USART1, USART_IT_RXNE)SET){rec_byte  USART_ReceiveData(USART1);//利用状态机接收HEX数据包if(rx_state0){if(rec_byte ){rx_index  0;rx_state  1;}}else if(rx_state1){if(rec_byte ! \r){SerialPort_RxPacket[rx_index]  rec_byte;rx_index;}else{rx_state  2;}}else if(rx_state2){if(rec_byte  \n){SerialPort_RxPacket[rx_index]  \0;//字符串结束标志符SerialPort_RxPacketFlag  1;rx_state  0;}}//读操作可以自动清零标志位但加上也没事USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}
//除了中断函数其他函数还要在头文件SerialPort.h中声明- LED.c新增函数 
/*** brief  LED1亮*/
void LED1_ON(void){GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}/*** brief  LED1灭*/
void LED1_OFF(void){GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
//注意还要在头文件LED.h中声明编程感想 数据混叠。同样的问题如果电脑端发送指令太快就有可能导致stm32来不及处理导致错误。一个解决方法是指令发慢一点当然另一种方法是增加标志位当LED的状态没有改变之前不理会接收数据这样会破坏当前程序的独立性并且原理不复杂我就先不写了。 9.6 软件使用FlyMcu串口下载  STLINK Utility FlyMcu可以通过串口给stm32下载程序。绿色软件无需安装。STLINK Utility通过STLINK给stm32下载程序。需要安装。 以上两款软件类似于51单片机的程序烧录软件——STC-ISP可以通过串口给51单片机下载程序。 硬件方面接线图本章的第一个实验“串口发送”相同这是因为该芯片的串口下载只适配了USART1所以引脚都要连接在USART1引脚上。 图9-25 串口下载程序-硬件接线示意图 下面依次介绍FlyMcu、STLINK Utility 图9-26 FlyMcu界面功能介绍 FlyMcu只能下载HEX文件Keil生成HEX文件 的方法 点击“魔术棒”–Output选项卡–勾选“Create HEX File”。编译过后就可以在工程目录的“Object文件夹”下找到该工程的HEX文件——“工程名.hex”。 使用FlyMcu下载程序 配置下载串口点击菜单栏“搜索串口”稍等一会点击紧随其后的“Port:xx”选项选择对应的串口。接着点击紧随其后的“bps:xx”选择下载的波特率。选择程序文件。菜单栏下一行点击“…”按钮选择HEX文件。其他功能保持默认。调整启动位置为BootLoader程序。由于串口下载需要使得stm32的启动位置为BootLoader启动程序而BootLoader启动程序固定放在“系统存储器”所以BOOT引脚应调整为 [BOOT1,BOOT0][0,1]。注意调整BOOT后一定要按一下最小系统板上的复位键调整才会生效。 点击FlyMcu的“开始编程”。此时程序就会被下载到主闪存中了但由于BOOT引脚没有变动所以程序下载完复位后还是会停留在BootLoader程序中。调整启动位置为主闪存。BOOT引脚应调整为 [BOOT1,BOOT0][0,0]然后按下复位键就可以看到程序正常运行了。  注关于串口下载的原理我觉得前面将“存储器映像”时已经说得很清楚了就不记录了UP讲解的地方在“P30 [9-6] FlyMcu串口下载STLINK Utility 中的4:56~8:11”。  每次下载程序都要拔插两边跳线帽太麻烦了有什么更简单的方法吗 方法一设计外围的STM32一键下载电路。利用“USB转串口模块”上CH340的 RTS#、DTR# 两个流控输出引脚分别控制stm32的BOOT0引脚、复位引脚但是不使用流控功能而仅仅只是当作一个普通的GPIO口加上FlyMcu的选项便可以利用电路自动切换BOOT引脚的高低电平。方法二软件设置自动跳转一次性功能。勾选FlyMcu的“编程后执行”去除勾选“编程到FLASH时写选项字节”。然后 [BOOT1,BOOT0][0,1]并按下复位键。然后点击“开始编程”就可以看到程序正常执行。  方法二评价该方法原理是下载程序后软件设置从主闪存0x08000000开始执行程序但是按下复位键后程序又会回到“系统存储器”执行BootLoader程序。所以可以不断的使用串口调试程序最后调试成功后再将BOOT引脚切换回来即可。也就是只需要最开始和最后切换跳线帽而已。  读FLASH 读取主闪存中的程序数据以BIN文件格式存储BIN文件不包含地址信息HEX文件包含地址信息。后续可以使用STLINK Utility下载实现盗取他人软件劳动成果。 清除芯片 将主程序区域全部擦除全部变成高电平。 读器件信息 读取芯片的信息。但是FLASH容量、SRAM容量等信息可能会出错。 设定选项字节等 可以设置选项字节的各项参数。点击“STM32F1选项设置”弹出以下界面   读保护字节是否允许读出主闪存数据。注意如果“阻止读出”那么就无法使用Keil下载程序了。另外在清除读保护的同时同时也会清空主闪存的数据。硬件选项字节有需求可以使用。用户数据字节有需求可以使用。那使用选项字节存储数据有什么好处呢  选项字节的数据相当于是“世外桃源”无论如何下载程序选项字节中的数据都可以不变可以存储一些不随程序变化的参数用上位机如FlyMcu、STLINK Utility可以很方便的修改。  写保护字节可以对Flash的某几页单独写保护。比如在主程序的最后几页写了一些自定的数据不希望在下载时被擦除就可以将最后几页设置写保护锁起来。注意开启“写保护”后若需要对保护区写入程序就会出错并且该软件不支持单独写入选项字节只能在Flash下载时顺便写入选项字节那也就是说如果将Flash前几页写保护了就再也无法使用FlyMcu下载程序了使用STLINK Utility可以补救回来 注意配置好选项字节数据后点击“采用这个设置”并在FlyMcu主界面勾选“编程到FLASH时写选项字节”。  图9-27 STLINK Utility界面功能介绍 STLINK Utility需要安装安装完成后在桌面上存在快捷方式。STLINK Utility可以下载多种文件包括HEX文件、BIN文件等。下面演示 下载程序的流程 硬件接线。无需连接“USB转串口模块”只需连接STLINK即可。BOOT引脚设置为 [BOOT1,BOOT0][0,0]然后按下复位键。点击左起第三个按钮进行连接。左起第一个按钮打开程序文件支持HEX格式、BIN格式。也可以直接跳到下一步左起第六个按钮下载程序。跳过上一步的此时选择程序文件。然后点击“Start”下载程序。下载完成后可以发现程序正常运行。 选项字节的配置菜单栏“Target”–“Option Byte…”下面依次介绍   读保护。硬件参数。灰色的选项就是本芯片不支持的选项。用户参数。写保护。 注配置好之后点击“Apply”就可以直接单独更改选项字节的参数。  左起第二个按钮读芯片数据格式可以选为HEX格式、BIN格式。 左起第四个按钮断开连接。 左起第五个按钮擦除芯片。 STLINK固件更新菜单栏“ST-LINK”–Firmware update–重新拔插STLINK–点击“Device Connect”–“Yes”。