当前位置:   article > 正文

通过串口中断的方式进行ASR-01S模块与STM32通信(问题与解决)

通过串口中断的方式进行ASR-01S模块与STM32通信(问题与解决)

前言:

最近在做一个智能家居的项目,需要实现语音控制的功能,于是我选用了ASR-01S模块与STM32通信,这个模块最大的好处在于有配套的编程软件和语音库,不用自己训练且编程简单(少儿编程的程度)。ASR-01S的代码架构在这不多说,总之在收到语音后它会通过串口发送一串命令给STM32,STM32收到后通过串口中断的方式进行一系列操作。但没想到在这块看起来很简单的地方翻车了(太丢人了。。。),经过求助之后终于解决了,在这里浅浅记录一下自己的翻车过程及解决方案。

问题引入

代码这里偷了一下懒,直接问了GPT,可以直接看我这篇文章:GPT对话代码库——基于STM32F103 1,标志位切换模式 & 2,串口的接受和发送

基础的就不多说了,主要讲一下核心问题,先放一下代码

  1. #define BUFFER_SIZE 100 // 定义缓冲区大小为100
  2. char buffer[BUFFER_SIZE]; // 定义一个缓冲区数组用于存储接收到的数据
  3. volatile unsigned int buffer_index = 0; // 声明一个用于记录缓冲区当前索引的变量,使用 volatile 关键字修饰以确保在中断中的可见性
  4. void USART3_IRQHandler(void) {
  5. // 检查是否接收到数据
  6. if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) {
  7. char data = (char)USART_ReceiveData(USART3); // 读取接收到的数据
  8. // 简单的字符串终止判断(例如以换行结束)
  9. if (data != '\n' && buffer_index < BUFFER_SIZE - 1) { // 如果接收到的数据不是换行符且缓冲区未满
  10. buffer[buffer_index++] = data; // 将接收到的数据存储到缓冲区中
  11. } else { // 如果接收到换行符或者缓冲区已满
  12. buffer[buffer_index] = '\0'; // 确保字符串结束,即在缓冲区末尾添加字符串结束符'\0'
  13. // 检查接收到的命令
  14. if (strcmp(buffer, "led on") == 0) { // 如果接收到的命令是"led on"
  15. GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 点亮LED
  16. char *msg = "已打开\n"; // 定义提示消息
  17. while (*msg) { // 循环发送消息中的每个字符
  18. USART3_SendChar(*msg++); // 通过串口发送字符
  19. }
  20. }
  21. // 重置索引,准备下一次接收
  22. buffer_index = 0; // 重置缓冲区索引,准备接收下一条指令
  23. }
  24. USART_ClearITPendingBit(USART3, USART_IT_RXNE); // 清除接收中断标志位
  25. }
  26. }

按照我原本的想法是先通过 USART3 接收中断判断是否接收到数据,然后读取接收到的数据并存储到缓冲区中。当接收到换行符或者缓冲区已满时,将缓冲区末尾添加字符串结束符'\0',然后检查接收到的命令,如果是"led on"则点亮LED,并通过串口发送提示消息"已打开\n",最后重置缓冲区索引,准备接收下一条指令。乍一看没啥毛病,但发现虽然buffer[]这个数组接收到了来自data的信息,最后经过仿真调试发现是在代码实现的时候给自己埋了雷。

strcmp这个函数用于比较两个字符串是否相等,函数原型:

int strcmp(const char *str1, const char *str2);

strcmp函数接受两个参数,分别是要比较的两个字符串 str1str2。它会按照字典顺序逐个比较两个字符串中的字符,直到遇到不相等的字符或者到达字符串结尾(即遇到 '\0' 终止符)。

如果两个字符串相等,则返回值为0;如果第一个字符串小于第二个字符串,则返回值为负数;如果第一个字符串大于第二个字符串,则返回值为正数。

GPT在这里是这样写的:

if (strcmp(buffer, "led on") == 0)  // 如果接收到的命令是"led on"

也就是要求我发送的字符串完全等于 "led on"才能进入,但在串口助手中却选择了“发送新行”这个选项,就相当于每次发送的都是"led on\n",由于strcmp函数的作用自然不会让我们进入逻辑,而且这段代码中并没有清空缓冲区数据的操作,也就是说就算没有发送新行,在第一次发送结束之后buffer[]中的数据就是“led onled onled on...”这种,所以需要使用memset函数在每次接收后清空缓冲区数据,并且最好将strcmp函数换成strstr函数,strstr作用和strcmp很相似,但更适合这个场景,简单来说使用strstr就是只要buffer[]这个数组中出现“led on”就能进入逻辑,只要在每次接收完数据后清空缓冲区数据就行。下面看我修正优化过的正确代码。

注:

1,strstr 函数是 C 标准库中的一个字符串查找函数,用于在一个字符串中查找另一个子字符串的第一次出现位置。其函数原型为:

char *strstr(const char *haystack, const char *needle);

strstr 函数接受两个参数,分别是要搜索的主字符串 haystack 和要查找的子字符串 needle。它会在主字符串中从头开始逐个字符地搜索子字符串,直到找到子字符串的第一次出现或者到达主字符串的结尾。如果找到子字符串,则返回指向该子字符串在主字符串中第一次出现位置的指针;如果没有找到子字符串,则返回 NULL

2,memset 函数是 C 标准库中的一个函数,用于将一段内存块的内容设置为指定的值。其函数原型为:

void *memset(void *ptr, int value, size_t num);

memset 函数接受三个参数,分别是指向要设置的内存块的指针 ptr、要设置的值 value、以及要设置的字节数 num

具体来说,ptr 是要设置的内存块的起始地址,value 是要设置的值(通常是一个字节的值,即 0 到 255 之间的整数),num 是要设置的字节数。函数会将 ptr 指向的内存块中的前 num 个字节的内容都设置为 value

代码讲解

串口的头文件

  1. #ifndef __USART_H
  2. #define __USART_H
  3. #include "stm32f10x.h"
  4. #include <stdio.h>
  5. // 串口3-USART3
  6. #define DEBUG_USARTx USART3
  7. #define BUFFER_SIZE 100
  8. void USART3_Config(u32 BAUD);
  9. void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
  10. void Usart_SendString( USART_TypeDef * pUSARTx, char *str);
  11. void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch);
  12. uint8_t Check_devices(void);
  13. uint8_t Control_devices(void);
  14. #endif /* __USART_H */

USART3配置及其中断配置

  1. void USART3_Config(u32 BAUD)
  2. {
  3. // GPIO端口设置
  4. GPIO_InitTypeDef GPIO_InitStructure; // 声明GPIO初始化结构体变量
  5. USART_InitTypeDef USART_InitStructure; // 声明USART初始化结构体变量
  6. NVIC_InitTypeDef NVIC_InitStructure; // 声明中断向量表初始化结构体变量
  7. // 使能GPIOB时钟和USART3时钟
  8. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能GPIOB时钟
  9. RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // 使能USART3时钟
  10. // USART3 TX -> PB10,RX -> PB11
  11. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // 配置GPIOB的引脚10(USART3的TX引脚)
  12. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 设置为复用推挽输出
  13. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置输出速度为50MHz
  14. GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化GPIOB的引脚10
  15. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // 配置GPIOB的引脚11(USART3的RX引脚)
  16. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 设置为浮空输入
  17. GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化GPIOB的引脚11
  18. // USART3配置
  19. USART_InitStructure.USART_BaudRate = BAUD; // 设置波特率
  20. USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 设置数据位长度为8位
  21. USART_InitStructure.USART_StopBits = USART_StopBits_1; // 设置停止位为1位
  22. USART_InitStructure.USART_Parity = USART_Parity_No; // 设置奇偶校验位为无校验
  23. USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 设置硬件流控制为无
  24. USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 设置USART模式为收发模式
  25. USART_Init(USART3, &USART_InitStructure); // 根据USART_InitStruct中指定的参数初始化USARTx寄存器
  26. // 配置USART3中断
  27. NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn; // 设置USART3中断通道
  28. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置抢占优先级
  29. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 设置响应优先级
  30. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
  31. NVIC_Init(&NVIC_InitStructure); // 根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
  32. // 使能USART3的接收中断
  33. USART_ITConfig(USART3, USART_IT_RXNE, ENABLE); // 使能USART3的接收中断
  34. // 使能USART3
  35. USART_Cmd(USART3, ENABLE); // 使能USART3
  36. }

这个没什么好说的,直接复制就行,注意自己头文件的引用

串口发送函数及printf和scanf的重定向

  1. /***************** 发送一个字符 **********************/
  2. void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
  3. {
  4. /* 发送一个字节数据到USART */
  5. USART_SendData(pUSARTx,ch);
  6. /* 等待发送数据寄存器为空 */
  7. while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
  8. }
  9. /***************** 发送字符串 **********************/
  10. void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
  11. {
  12. unsigned int k=0;
  13. do
  14. {
  15. Usart_SendByte( pUSARTx, *(str + k) );
  16. k++;
  17. } while(*(str + k)!='\0');
  18. /* 等待发送完成 */
  19. while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
  20. {}
  21. }
  22. /***************** 发送一个16位数 **********************/
  23. void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
  24. {
  25. uint8_t temp_h, temp_l;
  26. /* 取出高八位 */
  27. temp_h = (ch&0XFF00)>>8;
  28. /* 取出低八位 */
  29. temp_l = ch&0XFF;
  30. /* 发送高八位 */
  31. USART_SendData(pUSARTx,temp_h);
  32. while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
  33. /* 发送低八位 */
  34. USART_SendData(pUSARTx,temp_l);
  35. while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
  36. }
  37. ///重定向c库函数printf到串口,重定向后可使用printf函数
  38. int fputc(int ch, FILE *f)
  39. {
  40. /* 发送一个字节数据到串口 */
  41. USART_SendData(DEBUG_USARTx, (uint8_t) ch);
  42. /* 等待发送完毕 */
  43. while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
  44. return (ch);
  45. }
  46. ///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
  47. int fgetc(FILE *f)
  48. {
  49. /* 等待串口输入数据 */
  50. while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
  51. return (int)USART_ReceiveData(DEBUG_USARTx);
  52. }

重头戏来了,编写中断服务函数来存放PC端发送的数据

  1. char buffer[BUFFER_SIZE];
  2. // 定义一个大小为 BUFFER_SIZE 的字符数组 buffer,用于存储接收到的数据。
  3. volatile unsigned int buffer_index = 0;
  4. // 定义一个无符号整数变量 buffer_index,表示当前接收到的数据在 buffer 中的索引。
  5. // volatile 修饰表示该变量可能会被中断修改,编译器不会对其进行优化。
  6. uint8_t buffer_ready = 0;
  7. // 定义一个无符号 8 位整数变量 buffer_ready,表示缓冲区是否准备好。
  8. // 0 表示未准备好,1 表示准备好。
  9. void USART3_IRQHandler(void)
  10. {
  11. // 进入 USART3 的中断服务程序
  12. // 检查是否接收到数据
  13. if (USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
  14. {
  15. char data = (char)USART_ReceiveData(USART3); // 读取接收到的数据并存储在 data 中
  16. buffer[buffer_index++] = data;
  17. // 将接收到的数据存储在 buffer 数组中,并将 buffer_index 索引递增
  18. buffer_ready = 1;
  19. // 设置 buffer_ready 标志为 1,表示缓冲区已经准备好了
  20. if (buffer_index > BUFFER_SIZE - 1)
  21. memset(buffer, 0, 100);
  22. // 如果 buffer_index 大于等于 BUFFER_SIZE - 1,则清空 buffer 数组中的内容,大小为 100 字节
  23. }
  24. USART_ClearITPendingBit(USART3, USART_IT_RXNE);
  25. // 清除接收中断标志位,以便下一次接收
  26. }

简单来说,当 USART3 接收到数据时,将数据存储到 buffer 数组中,并设置 buffer_ready 标志为 1 表示缓冲区已经准备好。如果 buffer_index 超出缓冲区大小,则通过 memset 函数将 buffer 数组清空。最后,清除 USART3 的接收中断标志位,以便下一次接收。

编写串口接收检查函数来判断PC端发送的数据,并返回相应的返回值

  1. // 串口接收检查函数
  2. uint8_t Check_devices(void)
  3. {
  4. if (buffer_ready) // 检查缓冲区是否已准备好
  5. {
  6. buffer_ready = 0; // 清除缓冲区准备好的标志位
  7. // 判断接收到的命令
  8. if (strstr((const char*)buffer, "UVC on") != 0)
  9. {
  10. buffer_index = 0; // 重置缓冲区索引,准备下一次接收
  11. memset(buffer, 0, 100); // 清空缓冲区数据
  12. return 1; // 返回命令标识
  13. }
  14. // 判断接收到的命令
  15. if (strstr((const char*)buffer, "UVC off") != 0)
  16. {
  17. buffer_index = 0; // 重置缓冲区索引,准备下一次接收
  18. memset(buffer, 0, 100); // 清空缓冲区数据
  19. return 2; // 返回命令标识
  20. }
  21. // 判断接收到的命令
  22. if (strstr((const char*)buffer, "fan on") != 0)
  23. {
  24. buffer_index = 0; // 重置缓冲区索引,准备下一次接收
  25. memset(buffer, 0, 100); // 清空缓冲区数据
  26. return 3; // 返回命令标识
  27. }
  28. // 判断接收到的命令
  29. if (strstr((const char*)buffer, "fan off") != 0)
  30. {
  31. buffer_index = 0; // 重置缓冲区索引,准备下一次接收
  32. memset(buffer, 0, 100); // 清空缓冲区数据
  33. return 4; // 返回命令标识
  34. }
  35. }
  36. }

编写串口控制设备函数,并且根据接收检查函数的返回值来执行相应的逻辑

  1. // 串口控制设备函数
  2. uint8_t Control_devices(void)
  3. {
  4. if (Check_devices() == 1)
  5. {
  6. GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 点亮LED
  7. Usart_SendString(USART3, "消毒灯已开启\n\r"); // 发送消息到串口
  8. }
  9. if (Check_devices() == 2)
  10. {
  11. GPIO_SetBits(GPIOC, GPIO_Pin_13); // 熄灭LED
  12. Usart_SendString(USART3, "消毒灯已关闭\n\r"); // 发送消息到串口
  13. }
  14. if (Check_devices() == 3)
  15. {
  16. GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 点亮LED
  17. Usart_SendString(USART3, "通风扇已打开\n\r"); // 发送消息到串口
  18. }
  19. if (Check_devices() == 4)
  20. {
  21. GPIO_SetBits(GPIOC, GPIO_Pin_13); // 熄灭LED
  22. Usart_SendString(USART3, "通风扇已关闭\n\r"); // 发送消息到串口
  23. }
  24. }

总的来说,通过上面三段代码实现了一个通过 USART3 接收指令并控制设备的功能。

首先,通过 USART3 接收中断函数 USART3_IRQHandler 接收数据并存储到 buffer 缓冲区中。

然后,通过 Check_devices 函数检查缓冲区中是否有指令,根据指令执行相应的操作,并通过串口发送反馈信息。

Control_devices 函数根据 Check_devices 的返回值执行相应的设备控制操作。

总结:

通过这个问题,我熟悉了使用串口中断实现单片机与STM32之间的高效通信,同时也加深了对于串口通信的理解和掌握。希望这篇博客能够帮助到有类似学习目标的读者,也欢迎大家分享自己的经验和观点。

参考链接:

STM32串口通信—串口的接收和发送详解

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/281163
推荐阅读
相关标签
  

闽ICP备14008679号