笔者使用 FreeRTOS 创建了两个任务,使两颗 LED 以不同频率闪烁,但是在加入串口 USART 部分代码后, LED 不能正常工作了。
这个部分记录了笔者解决问题的思路,或许对你有一些启发,如果觉得太长可以直接按右边的目录跳到问题的解决方法:
首先尝试屏蔽了重定向的 printf,但是未注释掉 USART 的初始化函数,问题没有解决;
尝试屏蔽 USART 的初始化函数,问题解决,此时可以确定问题出在 USART 的初始化函数中;
查看 USART 的初始化函数代码,如下:
/**
* @brief USART GPIO 配置,工作参数配置
* @param 无
* @retval 无
*/
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
可以将函数复制到 ChatGPT 中,让 AI 帮助分析函数的结构,这里分享一个我自制的 Prompt:
你是一个优秀的编程工程师,你的工作是分析我发送给你的函数,把函数执行的功能用较简洁的语言归纳为几步。需要进行分析的函数我将放置于中括号中,你只需要输出归纳出的函数功能,不需要输出其他提示语。为了防止我们的对话过长你忘记了你的职责,当你忘记时我将在发送的消息前加上 “/keep” 来提醒你。
这个函数的结构大致是:
由于我们不确定问题出在这个函数的哪个地方,所以笔者采用了逐层解屏蔽的方式进行排查,也就是先将这个函数中所有代码都注释掉,然后从上往下逐层去掉屏蔽,直到问题出现,就可以确定有问题的代码。
如果函数较长,可以采用类似二分法的方式进行排查,也就是每次都解除一半的注释,如果问题出现,那么有问题的代码就在刚刚接触注释的那一半代码中;如果问题没有出现,有问题的代码就在另一半代码中。
不过在我们对这个函数进行逐层解屏蔽的排查操作时,我们要注意从函数的结构可以看出,串口的生效操作在最后一行,如果这行被注释了意味着前面所有的操作实际上都没有生效,所以这行从始至终都不能被注释。
经过笔者的排查,发现问题就出在 “使能串口接收中断” 这部分代码中,只要这部分代码被屏蔽了,问题就解决了。
虽然注释掉串口接收中断就可以解决问题,但是这意味着我们只能接收的 STM32 发送给电脑的信息,而不能给 STM32 发送信息。笔者自然是对这个解决方式不满意的,接着往下看。
值得一提的是,我发现了野火的代码中另一个 bug:野火在板载硬件初始化和串口初始化函数中都对中断优先级分组进行了配置,而且配置的方式是冲突的:
/**
* @brief 配置嵌套向量中断控制器NVIC
* @param 无
* @retval 无
*/
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// /* 嵌套向量中断控制器组选择 */
// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级*/
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 15;
/* 子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置NVIC */
NVIC_Init(&NVIC_InitStructure);
}
于是笔者测量了串口模块的 TX 电平,发现有 3.5V。推测出是不是当 STM32 的 RX 引脚需要一个类似于 3.5V 的高电平来确保其不会无故触发接收中断导致任务无法执行。所以笔者直接将 RX 引脚接入 STM32 板子自带的 3.3V 引脚,发现工作正常,猜想正确!
笔者又想到,可能不一定需要一个确定的高电平,确定的电平即可。于是将 STM32 的 RX 引脚接地,工作也正常!
// 将USART Rx的GPIO配置为上拉输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为上拉输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
定义了串口中断函数后,使用 ST-Link 进入调试模式,发现的确卡在了串口中断函数中导致任务不能得到执行
推测可能是串口的电平不稳定导致的
在一开始我们提到,一定程度上是因为移植野火的 FreeRTOS 例程才出现了冲突的问题。怎么理解呢?
实际上,为了降低学习成本,笔者使用的是 STM32F103C8 的最小系统板,所以才需要外接 USB 转 TTL 电平模块与计算机通信。
而野火的官方例程是适配其开发板的,肯定是经过了测试正常才发布的。那么为什么在野火的开发板上就不会出现类似的 bug 呢?因为野火的开发板已经集成了 USB 转 TTL 电平模块,一上电就能为 STM32 USART 的 RX 引脚提供一个确定的电平,规避了产生这个 bug 的条件。
笔者刚发现这个 bug 的时候,并不清楚是引脚的电平配置导致的问题,在网络上寻求解决方式的时候多半都是往 FreeRTOS 与 USART 冲突这个方向去搜索,导致并未找到正确的解决方法。
而在笔者写这篇文章的时候,就搜到了几篇类似的文章:
stm32 串口接收引脚配置为浮空输入问题
STM32F1频繁进入串口中断-串口直接连接到了接插件引脚上
USART3------------RXD----------PB11 悬空会导致程序频繁进入串口接收中断!!!
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!