[其他ST产品] STM32串口通信(基于缓冲区)编程及遇到的问题总结

[复制链接]
1248|21
 楼主| 远芳侵古道 发表于 2022-7-26 13:36 | 显示全部楼层 |阅读模式
    在写串口通信前阅读了STM32中文参考手册,然后满心澎湃地写代码。在这个过程中遇一些让人郁闷的事情,目前这些问题目前已经解决了(嘿嘿嘿),特此来总结一番。串口的使用步骤大概如下(51单片机、STM32、QT或VS编写PC串口上位机都是如此)

1、初始化串口参数(波特率、数据位、停止位、校验位、流控制、开启接收/发送)
2、配置串口中断

3、数据传输

        那么STM32怎么使用串口呢?上面已经说了嘛,所以按照以上步骤即可。可是由于STM32的引脚是可以复用的,我们还需要设置串口通信引脚(GPIO)的工作方式,然后再设置串口参数、串口中断。

        下文函数基于STM32固件库V3.5,以STM32F103RC的串口1的使用为例。

评论

———————————————— 版权声明:本文为CSDN博主「培培哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/u014695839/article/details/62037269  发表于 2022-7-26 13:59
 楼主| 远芳侵古道 发表于 2022-7-26 14:00 | 显示全部楼层
一、串口的初始化和中断设置

1、初始化GPIO:

        根据手册的8.1.11节,我们可以找到下表:
7978562df82c78e4a9.png
 楼主| 远芳侵古道 发表于 2022-7-26 14:00 | 显示全部楼层
     在全双工的模式下,发送引脚需要设置为推挽复用输出,接收引脚则设置为浮空输入或带上拉的输入。因为一般不用同步和流量控制的方式,所以CK、RST、CTS引脚不作配置。
 楼主| 远芳侵古道 发表于 2022-7-26 14:13 | 显示全部楼层
当然啦,在使用STM32外设的时候不要忘记打开外设时钟(GPIO和USART的RCC)。
 楼主| 远芳侵古道 发表于 2022-7-26 14:13 | 显示全部楼层
  1. GPIO_InitTypeDef GPIO_InitStructure;
  2.        
  3. //开启串口和GPIO的时钟
  4. RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
  5.        
  6. //配置发送引脚
  7. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  8. //发送引脚设置为推挽复用
  9. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  10. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  11. GPIO_Init(GPIOA, &GPIO_InitStructure);
  12.        
  13. //配置接收引脚
  14. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  15. //接收引脚设置为浮空输入
  16. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  17. GPIO_Init(GPIOA, &GPIO_InitStructure);
 楼主| 远芳侵古道 发表于 2022-7-26 14:15 | 显示全部楼层
2、配置串口参数

        有专门用于初始化串口的库函数(USART_Init)和对应的结构体(USART_InitTypeDef),好像每个外设都有这样的配套,具体内容可参看《STM32F10xxx固件库_3.xx.pdf》。
 楼主| 远芳侵古道 发表于 2022-7-26 14:15 | 显示全部楼层
  1. USART_InitTypeDef USART_InitStructure;
  2. //波特率
  3. USART_InitStructure.USART_BaudRate = 9600;                                       
  4. //数据长度
  5. USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  6. //停止位
  7. USART_InitStructure.USART_StopBits = USART_StopBits_1;
  8. //校验位
  9. USART_InitStructure.USART_Parity = USART_Parity_No;
  10. //流控制
  11. USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  12. //打开发送和接收模式
  13. USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  14. //初始化串口1
  15. USART_Init(USART1, &USART_InitStructure);
 楼主| 远芳侵古道 发表于 2022-7-26 14:18 | 显示全部楼层
3、中断配置
        在使用STM32的中断前,要对NVIC中断控制器进行配置,设置中断的优先级。
 楼主| 远芳侵古道 发表于 2022-7-26 14:20 | 显示全部楼层
  1. //配置中断优先级
  2. NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  3. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  4. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  5. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  6. NVIC_Init(&NVIC_InitStructure);
 楼主| 远芳侵古道 发表于 2022-7-26 14:22 | 显示全部楼层
4、使能串口及串口中断
        注意1:初始化时不要随意打开TXE中断!只要TX-DR寄存器为空,TX和TXE标志都会马上被置位而立即会产生中断(参考《STM32中文参考手册》的25.3.2节),即使中断标志被清除,也会被重新置位。因此,我采用的是TC中断而不是采用TXE中断。
 楼主| 远芳侵古道 发表于 2022-7-26 14:23 | 显示全部楼层
 楼主| 远芳侵古道 发表于 2022-7-26 14:25 | 显示全部楼层
注意2:不要采用在一个中断配置函数中同时打开两个中断!例如:USART_ITConfig(USART1, USART_IT_TC | USART_IT_RXNE, ENABLE);    咋眼一看,明明只打开TC中断和RX中断,然而却会同时把TXE中断也打开。
 楼主| 远芳侵古道 发表于 2022-7-26 14:25 | 显示全部楼层
  1. //串口1使能
  2. USART_Cmd(USART1, ENABLE);
  3. //清除接收中断标记
  4. USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  5. //清除发送完成中断标记
  6. USART_ClearITPendingBit(USART1, USART_IT_TC);
  7. //打开串口1发送完中断
  8. USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  9. //打开串口1接收中断                两个中断不能在一个函数中同时打开!!!太坑了T_T
  10. USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
 楼主| 远芳侵古道 发表于 2022-7-26 14:38 | 显示全部楼层
   这样,串口1配置好了。但代码一运行就会发现不妥!为什么每次初始化完成就马上进入中断了呢???遇到这种现象千万不要大惊小怪,我很淡(dan)定(teng)地做了个实验,发现处理器复位后,串口的SR寄存器中的TC标志会被置位。而根《STM32中文参考手册》25.3.2节,在串口使能后会自动发送一个空闲帧,发送完毕后TC也会置位,所以初始化将导致串口初始化完毕后马上进入TC中断。为了避免这种情况,可以在串口使能后等待空闲帧发送完毕,再打开TC中断。
 楼主| 远芳侵古道 发表于 2022-7-26 14:41 | 显示全部楼层
具体看下面完整的初始化代码:
  1. //配置串口1
  2. void USART1_Config()
  3. {
  4.         GPIO_InitTypeDef GPIO_InitStructure;
  5.         USART_InitTypeDef USART_InitStructure;
  6.         NVIC_InitTypeDef NVIC_InitStructure;
  7.        
  8.         RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
  9.        
  10.         //配置发送引脚
  11.         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  12.         //发送引脚设置为推挽复用
  13.         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  14.         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  15.         GPIO_Init(GPIOA, &GPIO_InitStructure);
  16.        
  17.         //配置接收引脚
  18.         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  19.         //接收引脚设置为浮空输入
  20.         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  21.         GPIO_Init(GPIOA, &GPIO_InitStructure);
  22.        
  23.         //波特率
  24.         USART_InitStructure.USART_BaudRate = 9600;                                       
  25.         //数据长度
  26.         USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  27.         //停止位
  28.         USART_InitStructure.USART_StopBits = USART_StopBits_1;
  29.         //校验位
  30.         USART_InitStructure.USART_Parity = USART_Parity_No;
  31.         //流控制
  32.         USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  33.         //打开发送和接收模式
  34.         USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  35.         //初始化串口1
  36.         USART_Init(USART1, &USART_InitStructure);

  37.         //USART1->SR寄存器复位后TC位为1,在此清零
  38.         USART_ClearFlag(USART1, USART_FLAG_TC);               
  39.         //串口1使能
  40.         USART_Cmd(USART1, ENABLE);
  41.        
  42.         //使能后串口发送一个空闲帧,等待空闲帧发送完毕后将TC标记位清零
  43.         while(USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET);
  44.         //否则开启TC中断后会马上中断
  45.         USART_ClearFlag(USART1, USART_FLAG_TC);
  46.                
  47.         //配置中断优先级
  48.         NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  49.         NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  50.         NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  51.         NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  52.         NVIC_Init(&NVIC_InitStructure);
  53.        
  54.         /***************************************************************
  55.                 注意:初始化时不要随意打开TXE中断,
  56.                 因为只要TX-DR寄存器为空,TX和TXE都会马上被置位而立即会产生中断
  57.         ***************************************************************/
  58.        
  59.         //清除接收中断标记
  60.         USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  61.         //清除发送完成中断标记
  62.         USART_ClearITPendingBit(USART1, USART_IT_TC);
  63.         //打开串口1发送完中断
  64.         USART_ITConfig(USART1, USART_IT_TC, ENABLE);
  65.         //打开串口1接收中断                两个中断不能在一个函数中同时打开!!!太坑了T_T
  66.         USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  67. }
 楼主| 远芳侵古道 发表于 2022-7-26 15:41 | 显示全部楼层
二、数据的发送和接收(注:以下代码有bug,博主至今还未找到原因T-T,仅供思路以参考!)
        为了提高代码的效率,我使用基于环形缓冲的串口通信方式。

        发送数据原理:把要发送的数据全部加入到缓冲区中,让处理器开始发送。一个数据发送结束后,即会产生TC中断,此时在中断服务程序中发送下一个数据。像吃饭看电视,在夹菜(发数据)的时候才要把注意力放到菜盘子上,嚼饭的时候(数据发送中)可以看电视,在开始发送数据到数据发送完毕触发中断的这段时间里,处理器可以去做别的事情。

        接收数据原理:当一个数据接收完毕后,将数据立存入缓冲区而不处理,并在未处理数据的计数器上加1。等到处理器空闲,再从缓冲区读取这些数据做并处理(不在中断函数中)。

        如此一来,串口的收发速率并不受影响,还能保证处理器在数据收发的过程中并行执行其他任务。
 楼主| 远芳侵古道 发表于 2022-7-26 15:43 | 显示全部楼层
  1. #include "usart1.h"
  2. #include "string.h"

  3. //发送缓冲区
  4. u8 Usart1_SendBuffer[USART_SendBufferSize];
  5. //接收缓冲区
  6. u8 Usart1_RecvBuffer[USART_RecvBufferSize];

  7. //发送缓冲区指针
  8. int Usart1_SendPointer = 0;
  9. //接收缓冲区指针
  10. int Usart1_RecvPointer = 0;

  11. //发送字符队列的长度
  12. int Usart1_SendDataSize = 0;
  13. //接收未处理字符数
  14. int Usart1_RecvDataSize = 0;

  15. //串口1发送状态
  16. int Usart1_SendStatus = USART_Status_Idle;

  17. //生成字符串的缓冲区
  18. char StringBuffer[100];

  19. //发送字符串
  20. void USART1_SendString(char *str)
  21. {
  22. //         while(*str)
  23. //         {
  24. //                 USART1_SendByte(*str++);
  25. //         }
  26.         USART1_SendArray((u8*)str, strlen(str));
  27. }

  28. //发送字节队列
  29. void USART1_SendArray(u8 *DataArray, int count)
  30. {
  31.         if(count <= 0)
  32.         {
  33.                 return;
  34.         }
  35.        
  36.         while(count)
  37.         {
  38.                 USART1_SendByte(*DataArray++);
  39.                 count --;
  40.         }
  41. }

  42. //发送一个字节
  43. void USART1_SendByte(u8 data)
  44. {
  45.         int pos;

  46.         //如果缓冲区满了,要等待
  47.         while(Usart1_SendDataSize >= USART_SendBufferSize);

  48.         //计算数据在缓冲区的位置
  49.         pos = Usart1_SendPointer + Usart1_SendDataSize;
  50.        
  51.         //数据位置超过缓冲区尾地址
  52.         if(pos >= USART_SendBufferSize)
  53.         {       
  54.                 //重新计算位置
  55.                 pos = pos - USART_SendBufferSize;
  56.         }
  57.         Usart1_SendBuffer[pos] = data;
  58.         Usart1_SendDataSize ++;
  59.        
  60.         //如果串口空闲,立即发送
  61.         if(Usart1_SendStatus == USART_Status_Idle)
  62.         {
  63.                 Usart1_SendStatus = USART_Status_Busy;
  64.                 USART_SendData(USART1, Usart1_SendBuffer[Usart1_SendPointer++]);
  65.                
  66.                 //指针移动到缓冲区尾地址后,循环到缓冲区首地址
  67.                 if(Usart1_SendPointer == USART_SendBufferSize)
  68.                 {
  69.                         Usart1_SendPointer = 0;                                       
  70.                 }
  71.                        
  72.                 Usart1_SendDataSize --;
  73.         }
  74. }


  75. //串口1中断服务程序
  76. void USART1_IRQHandler()
  77. {
  78.         //判断发送完成中断
  79.         if(USART_GetITStatus(USART1, USART_IT_TC) == SET)
  80.         {       
  81.                 //清空发送完成TC标记位
  82.                 USART_ClearFlag(USART1, USART_FLAG_TC);
  83.                 //清空串口发送完成中断TCIE标记
  84.                 USART_ClearITPendingBit(USART1, USART_IT_TC);
  85.                
  86.                 if(Usart1_SendDataSize > 0)
  87.                 {
  88.                         //发送下一个数据
  89.                         USART_SendData(USART1, Usart1_SendBuffer[Usart1_SendPointer++]);
  90.                        
  91.                         //指针移动到缓冲区尾地址后,循环到缓冲区首地址
  92.                         if(Usart1_SendPointer == USART_SendBufferSize)
  93.                         {
  94.                                 Usart1_SendPointer = 0;                                       
  95.                         }
  96.                        
  97.                         //待发送数据减1
  98.                         Usart1_SendDataSize --;
  99.                 }
  100.                 else
  101.                 {
  102.                         //发送完毕,串口1发送状态:空闲
  103.                         Usart1_SendStatus = USART_Status_Idle;
  104.                 }               
  105.         }
  106.        
  107.         //接收中断
  108.         if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
  109.         {       
  110.                 //清空串口接收标记
  111.                 USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  112.                
  113.                 //获取缓冲区数据          
  114.                 Usart1_RecvBuffer[Usart1_RecvPointer++] = USART_ReceiveData(USART1);
  115.                
  116.                 //如果没有溢出,待处理数据+1。否则丢弃该数据
  117.                 if(Usart1_RecvDataSize < USART_RecvBufferSize)
  118.                 {
  119.                         Usart1_RecvDataSize ++;
  120.                 }
  121.                
  122.                 //指针移动到缓冲区尾地址后,循环到缓冲区首地址
  123.                 if(Usart1_RecvPointer == USART_RecvBufferSize)
  124.                 {       
  125.                         Usart1_RecvPointer = 0;                                       
  126.                 }
  127.         }
  128. }
玛尼玛尼哄 发表于 2022-7-27 18:34 | 显示全部楼层
总结得很全面
Wordsworth 发表于 2022-10-4 10:20 | 显示全部楼层

显然直接调用的话,那么调用线程会被阻塞暂停
Uriah 发表于 2022-10-4 14:22 | 显示全部楼层

我们从以前就有了 DefineGender 方法,要求提供的输入值必须始终为 0 或 1。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

86

主题

887

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部