本内容基于江协科技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 通信接口
- 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
名称 | 引脚 | 双工 | 时钟 | 电平 | 设备 |
---|---|---|---|---|---|
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库函数
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);
// 传送字符串,\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 代码实现流程
- 串口代码:
- 配置USART:
- 开启时钟,把需要用的USART和GPIO的时钟打开
- GPIO初始化,把TX配置成复用输出,RX配置成输入
- 配置USART ,使用结构体
- 若只需要发送的功能,直接开启USART (USART_Cmd),初始化就结束了
- 编写函数:发送数据,发送一个字节
- 编写函数:发送数组
- 编写函数:发送字符串
- 编写函数:发送数字
- 编写函数:封装sprintf
- 配置USART:
- main.c:测试在串口代码中编写的函数
3.2.4 代码
- 串口代码:
#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发送出去
}
- 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 运行结果
3.3.3 代码实现流程
- 串口代码:
- 配置USART:
- 开启时钟,把需要用的USART和GPIO的时钟打开
- GPIO初始化,把TX配置成复用输出,RX配置成输入
- 配置USART ,使用结构体
- 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
- 编写函数:发送数据,发送一个字节
- 编写函数:发送数组
- 编写函数:发送字符串
- 编写函数:发送数字
- 编写函数:封装sprintf
- 编写USART1中断函数
- 配置USART:
- main.c:
- 串口初始化;
- 判断接收标志位Serial_RxFlag==1,说明接收到数据了,
- 就可以再次发送数据,并在OLED上显示
3.3.4 代码
- 串口代码
#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);// 清除标志位
}
}
- 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数据包
- 固定包长,含包头包尾
- 可变包长,含包头包尾
- 优点:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据
- 缺点:灵活性不足、载荷容易和包头包尾重复
- 解决方法:第一个就是限制载荷的范围,如果可以的话,在发送的时候,对数据进行限幅。第二种,如果无法避免载荷数据和包头包尾重复,就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要通过包头包尾对齐了数据,就可以知道哪个数据应该是包头包尾,哪个数据应该是载荷数据。在接收载荷数据的时候,并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐。第三种,就是增加包头包尾的数量,并且让它尽量呈现载荷数据出现不了的状态。
包头包尾并不是都需要的,可以只要包头,这样数据包的格式就是一个包头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 代码流程
- 串口代码:
- 配置USART:
- 开启时钟,把需要用的USART和GPIO的时钟打开
- GPIO初始化,把TX配置成复用输出,RX配置成输入
- 配置USART ,使用结构体
- 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
- 编写函数:发送数据,发送一个字节
- 编写函数:发送数组
- 编写函数:发送字符串
- 编写函数:发送数字
- 编写函数:封装sprintf
- 编写函数:TxPacket数组的4个数据,自动加上包头包尾发送出去
- 编写USART1接收中断函数:HEX接收数据包,用状态机来执行接收逻辑,接收数据包,把载荷数据存在RxPacket数组里。
- 配置USART:
- main.c:
- 按下按键执行发送,在OLED的前两行显示发送的数据,并在串口助手的接收区显示接收到的数据包;
- 在串口助手的发送区,发送数据包(如: 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);// 清除标志位
}
}
- 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,错误指令。
灯亮:
灯灭:
5.2.3 代码实现流程
- 串口代码:
- 配置USART:
- 开启时钟,把需要用的USART和GPIO的时钟打开
- GPIO初始化,把TX配置成复用输出,RX配置成输入
- 配置USART ,使用结构体
- 需要接收的功能,配置中断:在开启USART之前,再加上ITConfig和NVIC的代码就行
- 编写函数:发送数据,发送一个字节
- 编写函数:发送数组
- 编写函数:发送字符串
- 编写函数:发送数字
- 编写函数:封装sprintf
- 编写USART1接收中断函数:HEX接收数据包,用状态机来执行接收逻辑,接收数据包,把载荷数据存在RxPacket数组里。
- 配置USART:
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。