6.STM32之通信接口《精讲》之USART通信(PC串口与OLED交互)---多字节数据收发(数据包的模式:HEX数据包和文本数据包)
本节将串口收发HEX数据包(发送数据包很简单,主要还是关注一下如何接收数据包)
在这里给大家介绍一下状态机的思想
状态机模式(State Machine Patern)是一种用于描述对象的行为软件设计模式,属于行为型设计模式。在状态机模式中,对象的行为取决于其内部状态,并且在不同的状态下,对象可能会有不同的行为。状态机模式通常涉及定义一组状态以及状态之间的转换规则。
状态机模式结构与实现
状态机模式是一种行为设计模式,用于表示对象的不同状态及其行为变化。它通过将状态、行为和状态之间的转换独立封装,从而使代码更清晰、易扩展。以下是状态机模式的主要组成要素以及实现步骤:
结构组成
-
状态(State)
表示对象所处的特定状态,每个状态定义了在该状态下的行为。 -
上下文(Context)
包含状态机的核心对象,维护当前状态并在状态转换时更新状态。 -
转换(Transition)
描述从一个状态到另一个状态的过程,由外部事件或条件触发。 -
动作(Action)
在状态转换时可能需要执行的操作或行为。
实现步骤
1. 确定状态
识别系统中可能的状态,确保每个状态定义明确。例如,电梯可能有静止
、上升
、下降
三种状态。
2. 定义状态接口
为所有状态创建一个统一接口或抽象类,定义公共行为。
3. 实现具体状态类
为每个状态实现具体行为,并包含可能的状态转换逻辑。
4. 设计状态转换规则
明确各状态之间的转换条件和触发事件,确保系统运行逻辑一致。
5. 实现上下文类
创建一个上下文类,负责管理当前状态并触发状态的切换。
6. 测试与优化
验证状态转换逻辑,确保所有可能的转换路径正常工作。
思路明确了,我们就行实验了!
根据上图,我们发送数据包,是比较容易的,仅需将数据发送出去即可,完全自主可控,但是接受一个数据包就没那抹容易了,这里我们引用到了状态机的思想!如第二张图所示!
接收模式下,我们依然采用的是中断查询模式,也就是说,每来一个字节的数据,我么就会进入中断处理函数对数据进行处理,也就是串口中断函数是逐字节处理接收到的数据的
需要注意的一点,一定要分清硬件完成了那些动作,比如,在中断函数中,由硬件完成了接收数据寄存器的自动置位!
#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;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_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);
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;
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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
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);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
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]);
}
}
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 + '0');
}
}
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket,4);
Serial_SendByte(0xFE);
}
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxData = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
extern uint16_t RxData;
RxData = USART_ReceiveData(USART1);
if(RxState == 0)
{
if(RxData == 0xFF)
{
RxState = 1;
pRxData = 0;
}
}
else if (RxState == 1)
{
Serial_RxPacket[pRxData++] = RxData;
if(pRxData >= 4)
{
RxState = 2;
}
}
else if(RxState == 2)
{
if(RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
// USART_SendData(USART1,RxData);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
#include <stdio.h>
uint8_t RxData;
uint16_t KeyNum;
int main(void)
{
OLED_Init();
Serial_Init();
Key_Init();
OLED_ShowString(1,1,"TxData:");
OLED_ShowString(3,1,"RxData:");
Serial_TxPacket[0] = 0x01;
Serial_TxPacket[1] = 0x02;
Serial_TxPacket[2] = 0x03;
Serial_TxPacket[3] = 0x04;
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);
// OLED_ShowHexNum(1,1,
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);
}
}
}
ChatGPT 说:
ChatGPT
USART 接收数据时以一个字节为单位触发中断,而不是多个字节,主要是由 USART 硬件设计的工作机制 决定的。以下是详细解释:
1. USART 的工作原理
(1) 数据接收与寄存器设计
- USART 的核心包括一个接收数据寄存器(
DR
)和接收缓冲机制:- 当接收到一个完整的数据字节(例如 8 位或 9 位)后,数据会存储在寄存器
DR
中。 - 同时,USART 硬件会自动设置状态寄存器中的
RXNE
标志位(Receive Data Register Not Empty),表示接收寄存器非空,有数据可以读取。
- 当接收到一个完整的数据字节(例如 8 位或 9 位)后,数据会存储在寄存器
(2) 中断触发机制
- 当
RXNE
标志位被设置时,USART 会触发接收中断(USART_IT_RXNE
)。 - 每次中断只处理一个字节的数据,因为
RXNE
标志仅表示接收寄存器中有一个新数据字节。
2. 为什么不是多个字节触发中断?
(1) 硬件设计限制
- USART 的接收缓冲区通常只存储一个字节(或有限的 FIFO 深度),每次接收一个字节后会立即触发中断,等待软件处理。
- 如果接收多个字节才触发一次中断,会带来如下问题:
- 硬件需要额外的缓冲空间来存储多个字节。
- 无法确定软件读取数据的时序,容易导致缓冲区溢出或数据丢失。
(2) 实时性要求
- 通信协议中,很多数据是逐字节解析的,逐字节触发中断能确保数据被快速处理,减少延迟。
- 多字节触发中断可能会增加处理的复杂性,特别是在数据速率较高的情况下。
(3) 单字节设计更通用
- 单字节的中断设计可以兼容不同的数据帧格式(如 8 位或 9 位数据)。
- 对于固定帧长或可变帧长的数据包,软件可以通过状态机或 DMA 实现多字节处理,而无需硬件支持。
3. 如果需要处理多个字节怎么办?
(1) 循环读取
- 在中断服务程序(ISR)中,每次接收一个字节后,软件可以自行处理数据,或者将数据存入缓冲区,等待完整包到达后再解析。
(2) DMA 模式
- 如果数据量较大且需要高效传输,可以启用 DMA(Direct Memory Access) 模式。
- DMA 可以自动将多个字节从
DR
寄存器转移到内存,而不依赖 CPU 的逐字节读取。 - 中断仅在传输完成或错误时触发,减少中断次数。
4. 优点总结:逐字节触发中断
- 简单可靠:硬件设计简单,减少实现复杂性。
- 实时性强:每接收到一个字节,立刻通知 CPU 处理,适合实时通信。
- 灵活性高:软件可以根据实际需求,自由选择逐字节处理或多字节处理。
- 适应协议多样性:逐字节接收更容易适配各种通信协议。
示例对比
逐字节中断处理(当前实现方式)
c
复制代码
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { uint16_t RxData = USART_ReceiveData(USART1); // 读取一个字节 ProcessData(RxData); // 逐字节处理数据 USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志 } }
- 每次接收到一个字节,触发中断,实时处理。
DMA 多字节接收
c
复制代码
void USART1_DMA_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TCIF5)) // 检查传输完成中断 { uint8_t buffer[64]; // 假设缓冲区大小为 64 字节 DMA_ReadData(buffer); // 一次性读取整个数据包 ProcessPacket(buffer); // 处理完整数据包 DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TCIF5); // 清除中断标志 } }
- 一次性处理多个字节,减少中断频率,但增加初始配置复杂性。
总结
USART 逐字节触发中断(RXNE
)的设计:
- 基于硬件缓冲能力和实时性需求,确保每个字节都能被及时处理。
- 提供通用性和灵活性,适用于多种通信协议。
- 如果需要高效传输大量数据,可以使用 DMA 模式或专用硬件 FIFO,但这些是更高级的扩展功能,单字节触发中断仍是最基础、可靠的实现方式。
4o
您说:
接收模式下,我们依然采用的是中断查询模式,也就是说,每来一个字节的数据,我么就会进入中断处理函数对数据进行处理,也就是串口中断函数是逐字节处理接收到的数据的 润色一下
ChatGPT 说:
ChatGPT
在接收模式下,我们采用的是中断查询模式。也就是说,每当接收到一个字节的数据时,就会触发中断,进入中断处理函数对该字节的数据进行处理。因此,串口中断函数会以逐字节的方式对接收到的数据进行处理。
4o
您说:
根据上文我说的对吗接收模式下,我们依然采用的是中断查询模式,也就是说,每来一个字节的数据,我么就会进入中断处理函数对数据进行处理,也就是串口中断函数是逐字节处理接收到的数据的
ChatGPT 说:
ChatGPT
是的,你的理解是正确的。
在接收模式下,采用中断查询模式时,每当串口接收到一个字节的数据,USART 硬件会设置接收标志位(RXNE
),并触发中断。在中断处理函数中,对该字节的数据进行读取和处理。因此,串口中断函数确实是逐字节地处理接收到的数据的。这种机制保证了每个接收到的数据字节都能被及时处理,避免丢失数据。
4o
您说:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) { /* Check the parameters */ assert_param(IS_USART_ALL_PERIPH(USARTx)); assert_param(IS_USART_DATA(Data)); /* Transmit Data */ USARTx->DR = (Data & (uint16_t)0x01FF); }
ChatGPT 说:
ChatGPT
这段代码是一个函数,用于通过 USART 外设发送数据。它的作用是将一个 8 位或 9 位的数据单元写入 USART 数据寄存器(DR
),从而通过串口将数据发送出去。
代码详细解析
1. 函数功能
c
复制代码
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
USARTx
: 指定 USART 外设实例(如USART1
、USART2
等),决定数据将通过哪个串口发送。Data
: 要发送的数据,类型为uint16_t
,可以是 8 位(标准数据模式)或 9 位(扩展数据模式)。
2. 参数检查
c
复制代码
assert_param(IS_USART_ALL_PERIPH(USARTx)); assert_param(IS_USART_DATA(Data));
IS_USART_ALL_PERIPH(USARTx)
:- 检查传入的
USARTx
参数是否是有效的 USART 外设实例。
- 检查传入的
IS_USART_DATA(Data)
:- 检查传入的
Data
是否在有效的范围内。 - 如果是 8 位数据模式,
Data
应在0x00
到0xFF
范围内。 - 如果是 9 位数据模式,
Data
应在0x00
到0x1FF
范围内。
- 检查传入的
3. 发送数据
c
复制代码
USARTx->DR = (Data & (uint16_t)0x01FF);
Data & 0x01FF
:- 使用掩码
0x01FF
将数据限制在低 9 位范围内,确保高位无效数据不会影响发送。 - 即使只使用 8 位数据模式,这种操作也不会有副作用,保证兼容性。
- 使用掩码
USARTx->DR
:- 将数据写入 USART 的数据寄存器(
DR
)。 - 硬件会自动将该数据发送到 USART 的发送缓冲区,再通过 TX 引脚发送出去。
- 将数据写入 USART 的数据寄存器(
函数特点和作用
-
逐个数据单元发送:
- 每次调用函数只发送一个 8 位或 9 位的数据单元。
-
适配 8 位或 9 位数据格式:
- 通过掩码处理,支持 8 位和 9 位两种数据格式,增强了通用性。
-
硬件自动发送:
- 写入
DR
后,USART 硬件会自动完成数据传输,程序无需额外操作。
- 写入
-
简化上层调用:
- 调用者只需提供一个数据,函数内部完成发送寄存器的写入和有效性检查,简化了使用过程。
示例使用
发送 8 位数据
c
复制代码
USART_SendData(USART1, 0x41); // 发送 ASCII 字符 'A'(0x41)
发送 9 位数据
c
复制代码
USART_SendData(USART1, 0x1C1); // 发送 9 位数据,二进制 111000001
结合中断或查询模式
该函数通常结合以下两种方式使用:
- 中断模式:
- 当发送缓冲区空闲时(
TXE
标志位被设置),调用该函数发送下一个数据。
- 当发送缓冲区空闲时(
- 查询模式:
- 主动轮询
TXE
标志位,确保发送缓冲区空闲后调用该函数发送数据。
- 主动轮询
整体流程
- 上层程序调用
USART_SendData
。 - 函数完成参数检查,确保外设和数据范围有效。
- 将数据写入
USARTx->DR
。 - 硬件 USART 自动完成数据发送流程。
小结
- 该函数是 USART 数据发送的核心接口,通过简单的操作封装,屏蔽了底层寄存器操作细节。
- 使用掩码
0x01FF
保证兼容 8 位和 9 位模式。 - 它只负责将数据送入发送寄存器,而数据是否完全发送完毕由硬件和状态寄存器负责通知。
4o
您说:
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
ChatGPT 说:
ChatGPT
这行代码的作用是检查 USART1 的接收中断状态,具体来看:
1. 代码解析
函数:USART_GetITStatus(USART1, USART_IT_RXNE)
USART1
:- 指定需要检查状态的 USART 外设(这里是
USART1
)。
- 指定需要检查状态的 USART 外设(这里是
USART_IT_RXNE
:- 表示接收数据寄存器非空中断(
Receive Data Register Not Empty
)。这个中断标志位表明接收寄存器中有新数据需要读取。
- 表示接收数据寄存器非空中断(
- 返回值:
- 如果对应的中断标志位被设置,函数返回
SET
。 - 如果未设置,返回
RESET
。
- 如果对应的中断标志位被设置,函数返回
比较语句:== SET
- 检查是否接收到数据:
- 如果标志位被设置(
SET
),说明 USART1 接收了一个完整的数据字节,可以读取数据。 - 如果标志位未设置(
RESET
),说明没有新数据到来。
- 如果标志位被设置(
2. 具体功能
c
复制代码
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
- 这行代码用来判断是否接收到一个新的数据字节。
- 当接收到新数据时,
RXNE
标志位会被硬件设置,触发接收中断,程序进入接收数据处理逻辑。
3. USART_IT_RXNE 的意义
USART_IT_RXNE
对应接收缓冲区非空(RXNE
)标志位。- 当一个字节被接收到并存储在
DR
(数据寄存器)中时,RXNE
标志位会被硬件置位。 - 程序需要及时读取
DR
中的数据,否则可能导致缓冲区溢出,丢失后续数据。
- 当一个字节被接收到并存储在
4. 适用场景
- 中断模式:
- 通过检查
USART_IT_RXNE
,确保进入中断时确实有新数据需要处理。
- 通过检查
- 轮询模式:
- 在主程序中循环检查
USART_IT_RXNE
,只有在接收到数据时才执行读取操作。
- 在主程序中循环检查
5. 使用流程
中断处理函数示例
c
复制代码
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) // 判断是否接收到数据 { uint16_t data = USART_ReceiveData(USART1); // 读取接收到的数据 ProcessData(data); // 对数据进行处理 USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志位 } }
6. 为何要这样检查?
-
确保中断触发的原因明确:
- 中断可能由多种标志位触发,例如
TXE
(发送缓冲区空)或RXNE
(接收数据寄存器非空)。 - 通过检查特定标志位,确保处理逻辑与触发原因一致。
- 中断可能由多种标志位触发,例如
-
避免误操作:
- 如果不检查标志位直接处理,可能导致错误读取无效数据或执行不必要的操作。
7. 总结
这段代码的核心作用是通过检查 RXNE
标志位,判断 USART 是否接收到了新数据,从而决定是否执行接收数据的处理逻辑。这是 USART 通信中断处理中必不可少的一步,用于保障数据接收的准确性和实时性。
4o
您说:
Cannot set breakpoint(s). -HW breakpoint: all breakpoints in use. -SW breakpoint: cannot write to memory.
ChatGPT 说:
ChatGPT
出现这种问题的原因是调试器无法正确设置断点,可能涉及硬件断点限制、软件断点无法写入内存等多种情况。以下是详细分析及解决方案:
可能的原因
-
硬件断点数量限制
- 一些微控制器的调试器只支持有限数量的硬件断点(通常为 4 或 6 个)。
- 如果当前已经设置的硬件断点超过了硬件支持的数量,就会导致无法再添加新的断点。
-
尝试使用软件断点失败
- 软件断点依赖于将特定指令替换为中断指令来实现,因此需要调试器对目标内存的写权限。
- 如果断点所在的内存区域是只读的(如 Flash 区域或只读寄存器),软件断点无法写入,导致错误。
-
调试器设置问题
- 调试器配置可能未正确启用断点功能,或相关调试接口(如 SWD 或 JTAG)未正确连接。
-
内存保护机制
- 某些微控制器可能启用了内存保护机制(如 MPU 或 Flash 写保护),阻止调试器修改代码存储区域,从而导致无法设置断点。
-
调试器或 IDE 软件问题
- 调试器驱动或 IDE 的某些版本可能存在兼容性问题,导致断点功能异常。
解决方法
1. 检查硬件断点数量
- 查看芯片或调试器手册,确认支持的最大硬件断点数。
- 如果硬件断点已用尽:
- 删除一些不必要的断点。
- 尝试使用软件断点(适用于可写内存区域)。
2. 调整断点位置
- 确保断点设置在 可写的内存区域,如 RAM 中的函数或数据段。
- 避免在只读区域(如 Flash、ROM 或寄存器区域)设置软件断点。
3. 优化调试器设置
- 检查 IDE 的调试设置,确保调试模式与芯片实际配置匹配(如 SWD 或 JTAG 模式)。
- 在 Keil、IAR、STM32CubeIDE 等 IDE 中,可以尝试以下操作:
- 清除所有断点,重新设置关键位置的断点。
- 重置调试会话(终止调试后重新启动)。
- 在调试器设置中,明确选择硬件或软件断点。
4. 禁用内存保护
- 如果已启用 Flash 写保护或 MPU 配置,临时禁用相关保护设置以允许软件断点写入。
5. 升级调试工具
- 确保使用的是调试器和 IDE 的最新版本,以避免已知的兼容性问题。
6. 使用日志调试替代
- 如果无法添加断点,可以尝试通过串口打印日志或 LED 闪烁等方式记录代码执行情况,辅助调试问题。
示例:STM32CubeIDE 硬件断点调整
-
打开调试设置:
- 右键项目 → Debug Configurations → Debugger → Breakpoints。
-
检查断点数量:
- 查看硬件断点和软件断点的使用情况。
-
清除所有断点:
- 点击清除按钮,然后仅添加关键位置的断点。
-
切换到 SWD 模式:
- 确保调试接口为 SWD,并正确连接。
小结
这种问题通常是硬件断点数量超出限制或断点位置不可写导致的。通过合理分配硬件断点、优化断点位置、调整调试设置可以有效解决。如果问题仍然存在,可能需要检查调试工具的兼容性或通过其他方式辅助调试程序。
4o
您说:
中断函数 void USART1_IRQHandler(void) { static uint8_t RxState = 0; static uint8_t pRxData = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { extern uint16_t RxData; RxData = USART_ReceiveData(USART1); if(RxState == 0) { if(RxData == 0xFF) { RxState = 1; pRxData = 0; } } else if (RxState == 1) { Serial_RxPacket[pRxData++] = RxData; if(pRxData >= 4) { RxState = 2; } } else if(RxState == 2) { if(RxData == 0xFE) { RxState = 0; Serial_RxFlag = 1; } } // USART_SendData(USART1,RxData); USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }
ChatGPT 说:
ChatGPT
这段代码是一个 USART 接收中断处理函数 USART1_IRQHandler
,其目的是在接收到数据时,按特定的协议解析数据包。让我们逐步解析这段代码的功能。
代码解析
函数头
c
复制代码
void USART1_IRQHandler(void)
- 这是 USART1 的中断服务函数(IRQ Handler),每当 USART1 接收到数据或发生指定中断时,该函数会被自动调用。
静态变量
c
复制代码
static uint8_t RxState = 0; static uint8_t pRxData = 0;
RxState
:跟踪接收的状态。RxState
的不同值表示数据接收过程的不同阶段。0
:初始状态,等待数据包头。1
:正在接收数据包内容。2
:等待数据包的尾部。
- **
pRxData
4o mini
您说:
中断函数 void USART1_IRQHandler(void) { static uint8_t RxState = 0; static uint8_t pRxData = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { extern uint16_t RxData; RxData = USART_ReceiveData(USART1); if(RxState == 0) { if(RxData == 0xFF) { RxState = 1; pRxData = 0; } } else if (RxState == 1) { Serial_RxPacket[pRxData++] = RxData; if(pRxData >= 4) { RxState = 2; } } else if(RxState == 2) { if(RxData == 0xFE) { RxState = 0; Serial_RxFlag = 1; } } // USART_SendData(USART1,RxData); USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } 主函数 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); } 这个函数是如何进行的,数据是如何变化的,是如何触发中断的,中断里的处理逻辑是如何进行的,最后如何显示的,
ChatGPT 说:
ChatGPT
这段代码展示了一个基于 USART1 接收数据的中断处理机制,并通过 OLED 显示接收到的数据。我们将分步骤分析数据的流动、触发中断的过程、接收逻辑以及数据如何显示。
主函数部分
c
复制代码
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); }
Serial_GetRxFlag()
:这是一个函数(假设它返回Serial_RxFlag
的值),检查接收标志是否为 1,表示数据包接收完毕并可以进行处理。OLED_ShowHexNum()
:在 OLED 显示屏上显示数据包中的四个字节。每次调用会显示一个字节的数据,参数4, 1
,4, 4
等是显示位置,2
是显示的字符宽度(每个字节以 2 位十六进制格式显示)。
中断处理函数
c
复制代码
void USART1_IRQHandler(void) { static uint8_t RxState = 0; static uint8_t pRxData = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { extern uint16_t RxData; RxData = USART_ReceiveData(USART1); if (RxState == 0) { if (RxData == 0xFF) { RxState = 1; pRxData = 0; } } else if (RxState == 1) { Serial_RxPacket[pRxData++] = RxData; if (pRxData >= 4) { RxState = 2; } } else if (RxState == 2) { if (RxData == 0xFE) { RxState = 0; Serial_RxFlag = 1; } } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }
触发中断的过程
-
USART 接收到数据:
- 当 USART1 接收到数据时,接收数据寄存器(DR)会被填充,并触发
USART_IT_RXNE
中断。 - 进入中断服务函数
USART1_IRQHandler
。
- 当 USART1 接收到数据时,接收数据寄存器(DR)会被填充,并触发
-
检查接收中断标志:
- 使用
USART_GetITStatus(USART1, USART_IT_RXNE)
检查接收数据寄存器是否非空(RXNE)。如果设置了这个标志,说明有新的数据字节可读取。
- 使用
-
读取接收到的数据:
RxData = USART_ReceiveData(USART1)
从数据寄存器读取接收到的数据字节并存储到RxData
变量中。
中断函数的逻辑
状态机解析数据
-
状态
RxState == 0
:等待数据包的起始标志。- 如果接收到的数据是
0xFF
,表示数据包的开始。 - 更新状态
RxState = 1
,准备接收数据内容。pRxData = 0
重新初始化数据包字节索引。
- 如果接收到的数据是
-
状态
RxState == 1
:接收数据内容。- 将接收到的数据字节
RxData
存储到Serial_RxPacket[pRxData++]
中。 - 如果接收到 4 个字节(
pRxData >= 4
),表示数据包接收完成,更新状态RxState = 2
,等待数据包的结束标志。
- 将接收到的数据字节
-
状态
RxState == 2
:等待数据包结束标志。- 如果接收到的数据是
0xFE
,表示数据包的结束标志。 - 更新状态
RxState = 0
,恢复为初始状态。 - 设置
Serial_RxFlag = 1
,表示数据包已经接收完整,主程序可以处理这个数据包。
- 如果接收到的数据是
清除中断标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE)
:清除接收中断标志位,准备接收下一个字节。
数据流动和显示
-
数据流动过程:
- USART 接收到的数据会依次进入中断函数进行处理。
- 当接收到
0xFF
,状态机进入接收数据模式,将接收到的数据存储到Serial_RxPacket
数组中。 - 一旦接收到 4 个数据字节,状态机切换到等待结束标志状态。
- 当接收到
0xFE
,标志着数据包接收完成,设置Serial_RxFlag = 1
。
-
主函数如何显示数据:
- 主程序通过检查
Serial_RxFlag
来判断数据是否接收完整。 - 一旦接收到完整的数据包,
Serial_RxFlag
被置为 1,主程序通过OLED_ShowHexNum()
函数将接收到的数据逐个显示在 OLED 屏幕上。
- 主程序通过检查
总结
-
数据接收流程:
- 数据包的接收是通过 USART 中断触发的,每接收到一个字节数据,中断函数就会被调用进行处理。
- 采用状态机方式解析数据包,依次检测数据包的起始标志
0xFF
、数据内容(最多 4 字节)和结束标志0xFE
。
-
主程序的工作:
- 主程序通过检查
Serial_RxFlag
判断数据包是否接收完毕。 - 如果数据包接收完成,则将
Serial_RxPacket
数组中的数据通过 OLED 屏幕显示。
- 主程序通过检查
这种方式使用中断和状态机来逐字节接收数据并解析,确保数据传输的可靠性和及时性,同时通过 OLED 显示实时反馈接收到的内容。
4o mini
您说:
USART 接收到数据: 当 USART1 接收到数据时,接收数据寄存器(DR)会被填充,并触发 USART_IT_RXNE 中断。 进入中断服务函数 USART1_IRQHandler。 详细解释一下这个中断为啥会被触发,触发条件是什么,一共触发了多少次,这个中断函数执行了多少次,每次中进行哪些操作,中断函数每次处理的数据是1个字节还是多个字节或者是多少位,
ChatGPT 说:
ChatGPT
详细解释:USART 接收到数据中断触发的过程
触发条件:
当 USART 接收到一个字节的数据时,会触发 USART_IT_RXNE
中断。具体的触发条件和过程如下:
-
接收数据寄存器 (DR) 填充:
- 在 USART 工作时,它会接收串行数据流。当接收到一个字节的数据时,硬件会将该字节存入 USART 的接收数据寄存器(DR)。
-
中断条件:
- 一旦接收数据寄存器(DR)中的数据被填充完一个字节,且接收缓冲区不为空,USART 会自动设置
RXNE
(Receive Data Register Not Empty) 中断标志。这表示接收到的新字节数据已经准备好供读取。 - 如果启用了接收中断
USART_IT_RXNE
,则会触发一个中断,并调用相应的中断处理函数USART1_IRQHandler
。
- 一旦接收数据寄存器(DR)中的数据被填充完一个字节,且接收缓冲区不为空,USART 会自动设置
-
中断的触发:
- 每次接收到一个字节数据时,都会触发一次
USART_IT_RXNE
中断,进入USART1_IRQHandler
函数。 - 该中断会被触发 一次,每次触发都是接收到一个字节的数据。
- 每次接收到一个字节数据时,都会触发一次
中断函数执行时的步骤
c
复制代码
void USART1_IRQHandler(void) { static uint8_t RxState = 0; static uint8_t pRxData = 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { extern uint16_t RxData; RxData = USART_ReceiveData(USART1); // 读取接收到的数据 if (RxState == 0) { if (RxData == 0xFF) { RxState = 1; // 开始接收数据包 pRxData = 0; // 重置数据包索引 } } else if (RxState == 1) { Serial_RxPacket[pRxData++] = RxData; // 存储接收到的数据字节 if (pRxData >= 4) // 接收到 4 个字节 { RxState = 2; // 数据包接收完毕,进入尾部判断 } } else if (RxState == 2) { if (RxData == 0xFE) // 检查数据包的结束标志 { RxState = 0; // 恢复初始状态 Serial_RxFlag = 1; // 设置接收标志,通知主程序 } } USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志 } }
中断函数的执行过程
每次 USART 接收到一个字节的数据,USART1_IRQHandler
会执行以下步骤:
-
检查中断标志:
USART_GetITStatus(USART1, USART_IT_RXNE) == SET
:检查RXNE
中断标志,确保接收数据寄存器中有新数据。
-
读取接收到的数据:
RxData = USART_ReceiveData(USART1)
:读取接收到的数据字节并存储到RxData
中。这里是 16 位的RxData
变量,它从USARTx->DR
中提取接收到的字节。
-
状态机处理数据:
RxState == 0
:等待数据包的开始标志。- 如果接收到
0xFF
,表示数据包开始,进入状态 1。
- 如果接收到
RxState == 1
:接收数据包内容。- 接收到的数据字节会存储到
Serial_RxPacket[pRxData++]
中,直到接收 4 个字节为止。
- 接收到的数据字节会存储到
RxState == 2
:等待数据包的结束标志。- 如果接收到
0xFE
,表示数据包的结束标志,完成数据包接收,并将Serial_RxFlag
置为 1,通知主程序数据接收完成。
- 如果接收到
-
清除中断标志:
USART_ClearITPendingBit(USART1, USART_IT_RXNE)
:清除接收中断标志,准备接收下一个字节。
每次中断处理的数据
- 每次中断服务函数的执行会处理 一个字节 的数据。
- 为什么是一个字节:
USART_ReceiveData(USART1)
函数返回的是 16 位(uint16_t
)的值,但该函数从接收数据寄存器(DR
)中读取的数据实际上只包含一个字节(即 8 位)。所以,RxData
实际上存储的是一个字节数据。- 数据传输是逐字节的,每次中断的触发仅会处理接收到的 1 字节数据。
中断触发次数
- 每次接收到 1 字节数据时都会触发一次中断。因此,如果我们收到 4 字节数据,则中断会被触发 4 次。
- 每次触发时,中断函数都会被执行一次,接收并处理 1 字节数据。
总结:
- 触发条件:每次接收到 1 字节数据时,
RXNE
中断标志被设置,触发中断。 - 中断处理:每次中断都会读取 1 字节数据并进行状态机处理。状态机根据数据的起始标志、数据内容和结束标志来管理数据接收。
- 数据处理:每次中断处理 1 字节数据,直到数据包接收完整(4 个字节),然后通过设置
Serial_RxFlag
通知主程序数据包已完成。
中断触发与执行次数示例:
假设接收数据为:0xFF, 0x01, 0x02, 0x03, 0xFE
- 中断 1:接收到
0xFF
,状态切换到 1,准备接收数据。 - 中断 2:接收到
0x01
,存储到Serial_RxPacket[0]
。 - 中断 3:接收到
0x02
,存储到Serial_RxPacket[1]
。 - 中断 4:接收到
0x03
,存储到Serial_RxPacket[2]
,达到 4 个字节,状态切换到 2。 - 中断 5:接收到
0xFE
,状态恢复为 0,Serial_RxFlag
设置为 1,通知主程序。
每次中断都处理一个字节数据,并根据状态机的逻辑判断是否接收到完整的数据包。
4o mini
您说:
USART_GetITStatus(USART1, USART_IT_RXNE) == SET这个函数不是在中断函数里面吗?如果没进入中断,这个函数还会执行吗,还怎么触发中断,中断服务函数不是在触发之后才显示的吗?
ChatGPT 说:
ChatGPT
你提的问题非常好,涉及到了中断触发的本质以及中断处理函数的执行逻辑。我们来一步步详细解答你的疑问。
1. 为什么 USART_GetITStatus(USART1, USART_IT_RXNE)
会在中断服务函数里?
USART_GetITStatus
是一个 查询函数,用于检查 USART 的指定中断标志是否被置位。- 在中断服务函数(ISR)中,通常需要检查这个标志,确认是哪种中断事件触发了当前的中断(例如
RXNE
、TXE
等),以便处理相应的事件。
2. 如果没有进入中断,这个函数还会执行吗?
不会。
- 中断服务函数(
USART1_IRQHandler
)是由硬件在中断触发时调用的代码。如果没有触发中断,CPU 不会进入中断服务函数,自然也就不会执行USART_GetITStatus
。
中断触发流程:
- 硬件(USART 外设)检测到接收事件,设置
RXNE
标志位。 - 如果 USART 的接收中断(
USART_IT_RXNE
)被启用,且 NVIC 的中断优先级允许,CPU 会跳转执行USART1_IRQHandler
。 - CPU 进入中断服务函数后,才会执行
USART_GetITStatus
,检查RXNE
标志是否为SET
。
总结:
如果没有触发中断,CPU 根本不会跳转到中断服务函数(USART1_IRQHandler
),所以 USART_GetITStatus
不会被调用。
3. 中断是如何触发的?
中断的触发是由硬件条件决定的,具体到 USART 的 RXNE
中断,以下条件必须满足:
- 接收数据寄存器(DR)非空:当 USART 接收到 1 字节数据并存储到
DR
时,会自动设置RXNE
标志位。 RXNE
中断被使能:必须通过调用函数(或直接操作寄存器)显式启用USART_IT_RXNE
中断。一般通过USART_ITConfig
函数实现,例如:c
复制代码
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
- 全局中断被使能:CPU 必须允许中断(一般通过
__enable_irq()
或类似指令启用全局中断)。 - 中断优先级允许:NVIC 必须设置适当的中断优先级,确保当前中断能够被响应。
只有满足上述条件,硬件才会触发中断并执行中断服务函数。
4. 中断服务函数是怎么被执行的?
- 当 USART 硬件触发中断时,CPU 暂停当前的主程序,保存上下文并跳转到中断向量表中指定的中断服务函数(例如
USART1_IRQHandler
)。 - 中断服务函数会根据硬件的状态标志(如
RXNE
标志位)判断事件类型,并执行相应处理。
因此,中断服务函数只会在中断触发后才被执行。
5. 为什么需要在中断函数中检查 USART_GetITStatus
?
虽然中断是由 USART_IT_RXNE
触发的,但实际应用中,一个外设可能有多个中断源(例如发送完成中断、接收完成中断等)。
通过检查中断标志位(如 USART_GetITStatus
),可以明确是哪一种事件导致了中断触发,确保对症处理。
例如:
- 如果是接收事件触发中断,处理接收到的数据;
- 如果是发送完成事件触发中断,准备发送下一个字节数据。
6. 中断处理函数执行了多少次?
每次接收到一个字节,都会触发一次 RXNE
中断。因此,中断处理函数的执行次数与接收字节数相等。
假设接收 4 字节数据(例如 0xFF, 0x01, 0x02, 0xFE
),中断服务函数会执行 4 次,每次处理一个字节。
7. 中断函数每次处理的数据是 1 字节还是多个字节?
中断函数每次处理 1 字节 数据。
这是因为:
- USART 的接收缓冲区是逐字节接收的,每次接收一个字节后,
RXNE
标志被置位。 - 中断触发机制是基于
RXNE
,所以每次中断只会对应 1 个字节的数据。
8. 总结:
USART_GetITStatus
只会在中断函数内执行,因为中断函数是中断触发后由硬件调用的。- 中断函数执行的前提是硬件事件满足中断触发条件,例如接收到 1 字节数据并设置
RXNE
标志位。 - 每次中断函数执行仅处理 1 字节数据,函数执行次数与接收字节数相等。
4o
您说:
根据上文,假如我通过串口助手发送一个FF 11 22 33 44 FE的数据到单片机的RX上,根据上述代码,我的执行过程,
ChatGPT 说:
ChatGPT
根据你的代码逻辑和发送的数据 FF 11 22 33 44 FE
,以下是完整的执行过程:
前提条件
- 串口助手通过串口发送数据到单片机的 RX 引脚。
- USART 硬件模块接收数据,满足触发中断的条件:
- 每接收到 1 字节数据时,
RXNE
标志位被置位。 - 中断配置已经完成(
USART_IT_RXNE
已经启用,全局中断已开启,NVIC 优先级设置正确)。
- 每接收到 1 字节数据时,
- 串口中断服务函数(
USART1_IRQHandler
)每次触发时处理 1 字节数据。
发送数据 FF 11 22 33 44 FE
的执行过程
第一字节: 0xFF
- 串口助手发送
0xFF
,USART 接收完成,将其存入接收寄存器DR
。 RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0xFF
存入RxData
。- 当前
RxState
为0
,代码检查数据是否为0xFF
。 - 条件成立,进入下一个状态:
RxState = 1
。 - 重置
pRxData
为0
,准备存储接收数据。
第二字节: 0x11
- 串口助手发送
0x11
,USART 接收完成,将其存入接收寄存器DR
。 RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0x11
存入RxData
。- 当前
RxState
为1
,将0x11
存入Serial_RxPacket[0]
。 - 自增
pRxData
为1
,等待下一字节数据。
第三字节: 0x22
- 串口助手发送
0x22
,USART 接收完成,将其存入接收寄存器DR
。 RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0x22
存入RxData
。- 当前
RxState
为1
,将0x22
存入Serial_RxPacket[1]
。 - 自增
pRxData
为2
,等待下一字节数据。
第四字节: 0x33
- 串口助手发送
0x33
,USART 接收完成,将其存入接收寄存器DR
. RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0x33
存入RxData
。- 当前
RxState
为1
,将0x33
存入Serial_RxPacket[2]
。 - 自增
pRxData
为3
,等待下一字节数据。
第五字节: 0x44
- 串口助手发送
0x44
,USART 接收完成,将其存入接收寄存器DR
。 RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0x44
存入RxData
。- 当前
RxState
为1
,将0x44
存入Serial_RxPacket[3]
。 - 自增
pRxData
为4
,满足数据包长度条件,设置RxState = 2
。
第六字节: 0xFE
- 串口助手发送
0xFE
,USART 接收完成,将其存入接收寄存器DR
。 RXNE
标志位被置位,触发中断。- CPU 跳转到
USART1_IRQHandler
:USART_ReceiveData(USART1)
读取接收数据寄存器,将0xFE
存入RxData
。- 当前
RxState
为2
,代码检查数据是否为0xFE
。 - 条件成立,重置
RxState = 0
,表示完整数据包接收完成。 - 设置标志
Serial_RxFlag = 1
,通知主函数数据已就绪。
主函数处理
- 主函数中检测到
Serial_RxFlag == 1
,进入数据处理流程:c
复制代码
if (Serial_GetRxFlag() == 1) { OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2); // 显示 0x11 OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2); // 显示 0x22 OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2); // 显示 0x33 OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2); // 显示 0x44 }
- OLED 显示的数据:
复制代码
第1列:11 第2列:22 第3列:33 第4列:44
总结:中断函数执行过程
- 触发条件:每接收 1 字节,触发一次中断。
- 执行次数:共触发 6 次中断,对应接收的 6 个字节。
- 每次处理的数据:1 字节。
- 逻辑变化:
- 第一字节
0xFF
检测帧头,进入接收数据状态。 - 第二到第五字节存入接收缓冲区。
- 第六字节
0xFE
检测帧尾,标志数据包接收完成。
- 第一字节
- 主函数响应:处理接收的数据包,更新 OLED 显示。
这套流程体现了逐字节中断接收的完整逻辑,可靠地完成了数据包的接收与处理。
4o
您说:
串口助手发送 0xFF,USART 接收完成,将其存入接收寄存器 DR。 RXNE 标志位被置位,触发中断。这部分是不是硬件完成的
ChatGPT 说:
ChatGPT
是的,这部分确实是由硬件完成的。以下是硬件工作流程的详细说明:
硬件完成的部分
-
数据接收:
当串口助手发送0xFF
,数据通过串口线传输到单片机的 RX 引脚。
USART 硬件模块在波特率匹配的情况下,将收到的信号解析为数字数据(0xFF
)。 -
存入数据寄存器:
解析后的字节数据会自动存入 USART 的 接收数据寄存器(USARTx->DR
)。这是 USART 硬件的内置功能,软件不需要干预。 -
标志位置位:
数据寄存器DR
中有新数据时,硬件会自动将 RXNE(Receive Not Empty)标志位 置位,表示接收数据寄存器不为空。这是由硬件自动处理的,不需要软件操作。 -
中断触发:
如果对应的中断使能位(USART_IT_RXNE
)已经被设置,RXNE
标志置位时,硬件会自动触发中断请求。- CPU 检测到中断请求后,立即跳转到对应的中断服务函数
USART1_IRQHandler
。
- CPU 检测到中断请求后,立即跳转到对应的中断服务函数
软件需要处理的部分
从中断触发后,才是软件逻辑的介入:
- 进入中断服务函数
USART1_IRQHandler
。 - 检查
RXNE
标志位是否置位:c
复制代码
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
- 如果标志位已置位,表明确实有数据到来。
- 读取数据寄存器:
c
复制代码
RxData = USART_ReceiveData(USART1);
- 通过
USART_ReceiveData
读取接收数据寄存器DR
的内容。此时硬件会自动清除RXNE
标志位。
- 通过
- 根据状态机逻辑处理数据。
总结
- 硬件完成部分:
- 接收数据并存入
DR
。 - 自动置位
RXNE
标志位。 - 根据中断使能,自动触发中断。
- 接收数据并存入
- 软件处理部分:
- 中断触发后,由中断服务函数完成数据读取。
- 处理逻辑基于读取的数据执行相应操作。
因此,硬件的自动化机制保证了数据接收的实时性,而软件负责实现更复杂的逻辑。