我在高职教STM32——串口通信(5)

时间:2024-10-09 11:34:00

        大家好,我是老耿,高职青椒一枚,一直从事单片机、嵌入式、物联网等课程的教学。对于高职的学生层次,同行应该都懂的,老师在课堂上教学几乎是没什么成就感的。正因如此,才有了借助 **** 平台寻求认同感和成就感的想法。在这里,我准备陆续把自己花了很多心思的教学设计分享出来,主要面向广大师生朋友,单片机老鸟就略过吧。欢迎点赞+关注,各位的支持是本人持续输出的动力,多谢多谢!

        通信,按照传统的理解就是信息的传输与交换。对于像STM32这样的单片机来说,通信则与传感器、存储芯片、外围控制芯片等技术紧密结合,成为整个单片机系统的“神经中枢”。没有通信,单片机所实现的功能仅仅局限于单片机本身,就无法通过其它设备获得有用信息,也无法将自己产生的信息告诉其它设备。如果单片机通信没处理好的话,它和外围器件的合作程度就会受到限制,最终整个系统也无法完成强大的功能,由此可见单片机通信技术的重要性。UART(Universal Asynchronous Receiver/Transmitter,即通用异步收发器)串行通信是单片机最常用的一种通信技术,通常用于单片机和电脑之间、单片机和单片机之间、单片机与外围器件的通信。

【学习目标】

  1. 知道通信基本概念的含义;
  2. 理解通信机制中物理层和协议层分离的理念;
  3. 学会配置STM32的串口功能;
  4. 了解printf()函数“打印”至串口的实现过程;
  5. 掌握使用串口调试软件对单片机的调试方法。

        STM32串口通信涉及的知识较多,为了不让篇幅太长,本章打算分五个部分来讲解,本文是第五部分,详细解读串口收发的工程源码。

五、STM32的USART收发通信实验

5.6 代码剖析

         这两个文件的源码来自正点原子STM32系列的开发板,版权归广州市星翼电子科技有限公司所有。根据笔者自己的开发和教学经验,在STM32串口驱动的完整性和可移植性方面,正点原子的确做的非常优秀,我们不妨就来学习一下人家的优秀之处。

         的源码已在上一部分剖析过了,本节我们来剖析一下 的源码。为了便于阅读,我们将其大致分为三个代码段,如代码清单3所示。

代码清单3 的三个代码段

代码段1

        代码清单4有些抽象,如果初学者看不懂也不要紧。总之,你只需要明白,有了这段代码,printf() 函数“打印”的信息就到USART1上去了。

  1. //------------------------------------------------------
  2. // 代码清单4:的代码段1
  3. //------------------------------------------------------
  4. //加入以下代码,支持printf函数,而不需要选择use MicroLIB
  5. #if 1
  6. #pragma import(__use_no_semihosting)
  7. //标准库需要的支持函数
  8. struct __FILE
  9. {
  10. int handle;
  11. };
  12. FILE __stdout;
  13. //定义_sys_exit()以避免使用半主机模式
  14. void _sys_exit(int x)
  15. {
  16. x = x;
  17. }
  18. //重定义fputc函数
  19. int fputc(int ch, FILE *f)
  20. {
  21. while((USART1->SR&0x40)==0); //循环发送,直到发送完毕
  22. USART1->DR = (u8)ch;
  23. return ch;
  24. }
  25. #endif

        第7行声明提到的半主机是这么一种机制,它使得在ARM目标上跑的代码,如果电脑运行了调试器,那么该代码可以使用该主机电脑的输入输出设备。这点非常重要,因为开发初期,可能开发者根本不知道该ARM器件上有什么输出设备,而半主机机制使得你不必知道ARM器件的外设,利用电脑的外设就可以实现输出调试。所以要利用目标ARM器件的输出设备,首先要关掉半主机机制,然后再将输出重定向到ARM器件上,也就是我们期望的printf()函数输出重定向到USART1。

        那么如何实现重定向呢?第22~27行重写的 fputc() 函数即可实现,因为如果你研究过 printf() 函数的源码,最终调用的是标准库 fputc(int ch,FILE *f) 这个函数。而我们现在自定义了一个与之同名的函数,那 printf() 函数会调用哪个呢?标准库里的?自定义的?还是编译报错呢?答案是使用自定义的。于是我们在 fputc() 函数内部将输出的字符指向USART1的数据寄存器即可。

        第24行的等待发送完毕是通过读取 USART1->SR 状态寄存器里的TC位来实现的。我们把手册里对这个寄存器的描述摘录过来,如图20所示。TC位正好与0x40(01000000)里的那个1在相同位置,(USART1->SR&0x40)==0 意味着TC位为0,说明发送未完成,那就停在对应的while循环里。如果发送完成,TC位变成1,while循环退出。由此实现了“循环发送,直到发送完毕”

图20 USART1->SR状态寄存器TC位的描述

        第25行的 USART1->DR 是数据寄存器,如果是发送,把待发送的字符填入该寄存器,之后会借助移位寄存器将数据一位一位的发送出去。如果是接收,移位寄存器会将接收到的数据一位一位地填入该寄存器。因此,USART1->DR 这个数据寄存器是收发共用的,会根据程序的需要自动改变数据方向。关于该寄存器的详细描述,请参考STM32数据手册。

        我们再回到与“半主机”有关的代码上,为确保没有从C库链接使用半主机的函数,那么 中有些使用半主机的函数要重写,于是就有了第9~19行的代码。至于为何这么写,有兴趣的朋友可以阅读ARM官方的两个文档《RealView编译工具开发指南》和《ARM Developer Suite Compilers and Libraries Guide》,内容比较多,理解起来也有难度。

        最后,我们再补充一个在Keil编译中“使用微库”的问题,即第1行注释里提到的“use MicroLIB”,它其实是Keil编译器的一个配置选项,如图21所示。如果勾选此项,第5~19行的代码也就不需要了,只需要留下重定向 fputc() 函数的代码即可。因此,如果你看到有的STM32工程的串口“打印”没有出现类似的代码段1,不妨看一下是否勾选了此项。

图21 Keil软件里的use MicroLIB选项

        微库可以看作是缺省标准C库的备选库,它用于必须在极少量内存环境下运行的嵌入式应用程序,它只保留了标准库里的一些常用功能,在一定程度上方便了程序的开发和移植,毕竟标准库是成熟和通用的。当然,也有弊端,使用微库编译出来的代码量会比较大,感兴趣的朋友可以在做完该实验之后,使用微库再编译一次,看看代码量究竟差了多少。

代码段2

        这段代码主要就是一个串口初始化函数 uart_init(),完成 USART1_TXUSART1_RX 两个引脚的GPIO初始化、USART1串口中断的初始化以及串口自身参数的初始化,源码见代码清单5。

  1. //----------------------------------------------------------------
  2. // 代码清单5:代码段2
  3. //----------------------------------------------------------------
  4. #if EN_USART1_RX //如果使能了接收
  5. u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
  6. u16 USART_RX_STA = 0; //接收状态标记
  7. /**
  8. ******************************************************************
  9. * 函 数 名:uart_init
  10. * 功 能:串口初始化
  11. * 入口参数:bound --- 波特率
  12. * 出口参数:无
  13. * 说 明:PA9/PA10三方面的初始化:GPIO、USART和NVIC
  14. ******************************************************************
  15. **/
  16. void uart_init(u32 bound)
  17. {
  18. //定义必要的初始化结构体
  19. GPIO_InitTypeDef GPIO_InitStructure;
  20. USART_InitTypeDef USART_InitStructure;
  21. NVIC_InitTypeDef NVIC_InitStructure;
  22. //使能USART1和GPIOA外设时钟
  23. RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
  24. //USART1_TX(PA9),复用推挽模式
  25. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  26. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  27. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  28. GPIO_Init(GPIOA, &GPIO_InitStructure);
  29. //USART1_RX(PA10),浮空输入模式
  30. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  31. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  32. GPIO_Init(GPIOA, &GPIO_InitStructure);
  33. //USART1的中断配置
  34. NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  35. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
  36. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
  37. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  38. NVIC_Init(&NVIC_InitStructure);
  39. //USART1初始化设置:波特率、8位字长、1位停止位、无校验、无硬件流、收发一体
  40. USART_InitStructure.USART_BaudRate = bound;
  41. USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  42. USART_InitStructure.USART_StopBits = USART_StopBits_1;
  43. USART_InitStructure.USART_Parity = USART_Parity_No;
  44. USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  45. USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  46. USART_Init(USART1, &USART_InitStructure);
  47. //开启串口1及接收中断
  48. USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  49. USART_Cmd(USART1, ENABLE);
  50. }

        第5~8行,这几个宏和变量的作用已在前面剖析 源码时解释过了。

        第29~38行是对PA9和PA10两个引脚的GPIO初始化。这里需要注意的是,复用功能下的GPIO模式怎么判定?查看手册得知,如表4所示,配置全双工的串口1,那么 TX(PA9)需要配置为推挽复用输出,RX(PA10)配置为浮空输入或者带上拉输入。

表4 串口GPIO模式配置表

        第41~45行,对于NVIC中断优先级管理,我们在前面章节已有讲解,这里不重复了。需要注意一点,因为使用到了串口的中断接收,必须在 里面设置 EN_USART1_RX 为 1(默认设置就是1)。该函数才会配置中断使能,以及开启USART1的NVIC中断。这里我们把USART1中断放在组2,优先级设置为组2里面的最低。

        第48~54行,我们配置USART1通信参数为:波特率由参数bound传入,字长为8,1个停止位,无校验位,不使用硬件流控制,收发一体工作模式,然后调用 USART_Init() 库函数完成配置。

        第57~58行,配置完NVIC之后调用 USART_ITConfig() 函数使能USART1接收中断,最后调用 USART_Cmd() 函数使能USART1。

代码段3

        这段代码是USART1的中断服务函数,处理串口接收到的数据,源码见代码清单6。

  1. //----------------------------------------------------------------
  2. // 代码清单6:代码段3
  3. //----------------------------------------------------------------
  4. /**
  5. ******************************************************************
  6. * 函 数 名:USART1_IRQHandler
  7. * 功 能:串口中断处理函数
  8. * 入口参数:无
  9. * 出口参数:无
  10. * 说 明:开发板若收到串口助手发来的消息,则进入此中断
  11. ******************************************************************
  12. **/
  13. void USART1_IRQHandler(void)
  14. {
  15. u8 Res;
  16. //接收到消息(必须是0x0d 0x0a结尾)
  17. if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
  18. {
  19. Res = USART_ReceiveData(USART1); //读取接收到的数据
  20. if((USART_RX_STA&0x8000)==0) //接收未完成
  21. {
  22. if(USART_RX_STA&0x4000) //接收到了0x0d
  23. {
  24. if(Res!=0x0a) USART_RX_STA = 0; //接收错误,重新开始
  25. else USART_RX_STA |= 0x8000; //接收完成了
  26. }
  27. else //还没收到0X0D
  28. {
  29. if(Res==0x0d)
  30. USART_RX_STA |= 0x4000;
  31. else
  32. {
  33. USART_RX_BUF[USART_RX_STA&0X3FFF] = Res;
  34. USART_RX_STA++;
  35. //接收数据错误,重新开始接收
  36. if(USART_RX_STA>(USART_REC_LEN-1))
  37. USART_RX_STA = 0;
  38. }
  39. }
  40. }
  41. }
  42. }
  43. #endif

        这个函数的实现思路是这样的,当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 数组中,同时在接收状态寄存器 USART_RX_STA 中计数接收到的有效数据个数,当收到回车的第一个字节 0x0d 时,计数器将不再增加,等待 0x0a 的到来,而如果 0x0a 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0x0a,则标记 USART_RX_STA 的第15位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0x0d,那么在接收数据超过 USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。

        第48行的 #endif 配对的是代码段2的第1行 #if EN_USART1_RX。当需要使用串口接收的时候,我们只要在 里面设置 EN_USART1_RX 为1就可以了。不使用的时候,设置 EN_USART1_RX 为0即可,这样可以省出部分SRAM和FLASH,我们默认设置 EN_USART1_RX 为1,也就是开启串口接收。

5.7 源码剖析

        主程序的源码见代码清单7,请结合图22的实验效果来阅读,控制思路已在注释中交待。

图22 本实验的串口收发效果
  1. /************************************************
  2. * 代码清单7:
  3. * 项 目:串口收发测试
  4. * 任务描述:发送和接收
  5. * 实验平台:OneNET STM32开发板
  6. * 作 者:老耿
  7. * 日 期:2024/4/23
  8. ***********************************************/
  9. //-----------------------------------------------------
  10. // 必要的头文件
  11. //-----------------------------------------------------
  12. #include ""
  13. #include ""
  14. #include ""
  15. #include ""
  16. //-----------------------------------------------------
  17. // 主函数
  18. //-----------------------------------------------------
  19. int main()
  20. {
  21. u16 time = 0; //控制闪烁的时长
  22. u16 len; //存放接收的长度
  23. u8 i; //循环下标
  24. //执行必要的初始化
  25. NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组
  26. delay_init(); //延时初始化
  27. Led_Init(); //LED初始化
  28. uart_init(9600); //初始化串口
  29. //上电后等待1s打印开机信息
  30. delay_ms(1000);
  31. printf("STM32 USART Test\r\n");
  32. printf("LaoGeng 014428\r\n");
  33. //绿灯闪烁表示程序运行,
  34. //串口等待用户发消息,若用户发来消息,则回显该消息
  35. while(1)
  36. {
  37. //加到1s变一次绿灯
  38. if(time == 100)
  39. {
  40. GREEN_TOG;
  41. time = 0;
  42. }
  43. //处理接收到的消息
  44. if(USART_RX_STA & 0x8000) //确认接收完毕
  45. {
  46. len = USART_RX_STA & 0x3fff; //取得数据长度
  47. printf("\r\nMessages you send:\r\n"); //回显前的提示语
  48. for(i=0; i<len; i++)
  49. {
  50. USART1->DR = USART_RX_BUF[i]; //收到的内容填入发送寄存器
  51. while((USART1->SR & 0x40) == 0); //等待发送完成
  52. }
  53. printf("\r\n"); //换行分隔前后消息
  54. USART_RX_STA = 0; //清空接收标志
  55. }
  56. //每10ms累加1次
  57. delay_ms(10);
  58. time++;
  59. }
  60. }

5.8 测试与验证

        把程序下载到麒麟座开发板中,可以看到板子上的绿色LED开始闪烁,说明程序已经在跑了。串口调试助手软件很多,我们用的是正点原子的串口调试助手XCOM,从笔者的使用体验上,这款软件的界面和中文支持不错,接下来就可以按照图22里的步骤来测试了。至此我们就完成了串口“发送”和“接收”实验,后续其他实验也将继续沿用串口功能,尤其是“打印”必要的测量数据和调试信息。

(第五部分完,共五部分)