摘要

  记录一下如何配置一个好用的工程模板,工程模板启动串口1通信,串口使用DMA方式收发,使用IDLE中断实现不定长数据的接收发送。通过软中断实现串口数据收发处理的实时性,能够在接收到一帧串口数据后立刻触发串口数据处理中断。
  相关工程源码已推送到Gitee:STM32-code

生成工程

  这里以STM32F103C8T6单片机为例,其他型号的单片机配置方法大同小异。

配置外设

1、配置时钟与Debug

  打开STM32CubeMX,选择一款单片机,使能SWD调试功能。启动外部高速时钟源。
在这里插入图片描述
在这里插入图片描述

2、配置串口与DMA

  打开USART1配置界面,启动异步通信,设定波特率115200Bits/s,勾选串口1全局中断,添加USART1_RXUSART1_TXDMA请求。
在这里插入图片描述

3、配置定时器与中断

  使能定时器TIM1功能,将定时器配置为1ms中断,根据查阅STM32数据手册,可知TIM1挂在APB2时钟总线上,最大值频率为72Mhz,所以这里我们将定时器时钟源设为内部时钟源,定时器分频值为72-1,分频后的频率为1Mhz,定时器重载值为1000-1;就可以将定时器中断配置为每经过1ms,进入一次定时器中断,使能定时器中断。 在这里插入图片描述
在这里插入图片描述

  1. 选择TIM1.。
  2. 启用内部时钟源。
  3. 设定定时器72分频,分频后定时器时钟频率为1Mhz。
  4. 定时器重载值1000次,每经过1ms进入一次中断。
  5. 使能自动重载。

  使能定时器更新中断,某些单片机的定时器不能单独使能更新中断,只有全局中断TIMx global interrupt,那么使能全局中断即可。
在这里插入图片描述

配置时钟树

  进入时钟配置界面。设定外部晶振频率,时钟源和系统主频。
在这里插入图片描述

  1. 根据实际情况设定外部晶振频率,这里我使用的外部晶振频率是8MHz。
  2. 设定PLL时钟源为外部时钟源。
  3. 设定系统时钟来自于PLL时钟。
  4. 设定系统主频HCLK为72MHz,确定后,软件自动配置时钟树的各个分频值。

注意:画红框的地方表示挂接定时器TIM1的时钟线APB2时钟速度为72Mhz,定时器配置时设定的分频值与此相同。

配置工程设置

在这里插入图片描述

  1. 进入Project Manager界面。
  2. 先配置工程目录。
  3. 选择工程保存的位置。
  4. 设定工程名称。
  5. 选择开发环境,这里我使用的时Keil-MDK,所以选择MDK-ARM。

在这里插入图片描述

  1. 代码生成设置。
  2. 选则仅复制需要使用的库文件,如果选择第一项复制全部文件,后期编译代码时会比较慢。
  3. 将各个外设初始化函数放在单独的.c/.h文件中,如果不选,则所有外设的初始化函数都放在main.c文件中。

点击GENERATE CODE生成工程

修改源码

配置软中断

  软中断的详细配置说明请转STM32CubeMX系列教程6:外部中断EXTI与软中断SWI查阅。
  打开main.c文件,在while(1)循环前的/* USER CODE BEGIN 2 *//* USER CODE END 2 */之间加入软中断配置语句,这里我使用外部中断线0作为软中断。并设定中断0的抢占优先级为最低,仅高于主循环。以免打断其他中断。如果对串口处理优先级有要求,可以调高。

  /* USER CODE BEGIN 2 */
    EXTI->IMR |= 1<<0;				
    HAL_NVIC_SetPriority(EXTI0_IRQn, 15, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
  /* USER CODE END 2 */

在这里插入图片描述

配置串口

声明变量

  打开usart.c文件,在文件头/* USER CODE BEGIN 0 *//* USER CODE END 0 */之间声明一些变量。

/* USER CODE BEGIN 0 */

volatile uint8_t rx1_len = 0;  //接收一帧数据的长度
volatile uint8_t rec1_end_flag = 0; //一帧数据接收完成标志
uint8_t rx1_buffer[BUFFER_SIZE]={0};  //接收数据缓存数组

/* USER CODE END 0 */

  打开usart.h文件,在文件头/* USER CODE BEGIN Includes *//* USER CODE END Includes */之间对刚才在usart.c文件中声明的变量做外部声明,使别的.c文件也可使使用这些变量,添加stdio.hstring.h两个头文件。

/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
#define BUFFER_SIZE  500  		
	 
extern volatile uint8_t rx1_len;  //接收一帧数据的长度
extern volatile uint8_t rec1_end_flag; //一帧数据接收完成标志
extern uint8_t rx1_buffer[BUFFER_SIZE];  //接收数据缓存数组

void Usart1_Handle(void);
void DMA_Usart1_Send(uint8_t *buf,uint8_t len);//串口发送封装
void Usart1_IDLE(void);

/* USER CODE END Includes */

BUFFER_SIZE表示串口每帧数据的最大长度这里时500个字节。
后面的这三个函数,是后面将要实现的函数,这里先做外部声明,后面在usart.c文件中实现。

重定向printf

  在usart.c文件中,在上面声明变量的后面,/* USER CODE END 0 */的前面,写入下面的代码,重定向printf到串口1上。

#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 
}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
void _sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{ 	
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
	USART1->DR=(uint8_t)ch;      
	return ch;
}
#endif 

注意:在重定义fputc()函数中,USART1->SRUSART1->DR两个寄存器分别为串口1的状态寄存器和数据寄存器,不同型号的单片机这两个寄存器的名称不通,会引起编译报错找不到该寄存器,可查阅单片机寄存器手册找到正确的寄存器名称进行修改,其他代码不用变。如果要重定向到其他串口,仅需修改这两个寄存器即可。

实现IDLE中断处理

  在usart.c文件下面,找到/* USER CODE BEGIN 1 *//* USER CODE END 1 */之间,写入IDLE中断处理函数。

/* USER CODE BEGIN 1 */
void Usart1_IDLE(void)      //USART1的IDLE接收
{
   uint32_t tmp_flag = 0;
   uint32_t temp;

   tmp_flag =__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE); //获取IDLE标志位
   if((tmp_flag != RESET))//idle标志被置位
   {
   		__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位
   		HAL_UART_DMAStop(&huart1); //  停止DMA传输,防止
   		temp  =  __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数
   		rx1_len =  BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
   		rec1_end_flag = 1;	// 接受完成标志位置1	
        EXTI->SWIER |= 1<<0;        //触发软中断
   }
}

void Usart1_IDLE(void)中判断IDLE标志位是否为真,如果为真则代表已经接收完成一帧数据,清除标志位并进行后面的处理。
接收完成1帧数据后,向CPU发起中断,让CPU前往串口处理函数中处理接收到的数据。

封装DMA串口发送函数

  在void Usart1_IDLE(void)函数的下面,实现DMA发送的函数,供其他函数使用。

void DMA_Usart1_Send(uint8_t *buf,uint8_t len) //串口发送封装
{
    if(len == 0) return;
    HAL_StatusTypeDef sta = HAL_UART_Transmit_DMA(&huart1,buf,len);
	if(sta != HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
	{
		Error_Handler();
	}
}

实现串口数据处理函数

  在void DMA_Usart1_Send(uint8_t *buf,uint8_t len)函数下面,实现串口数据处理函数。这里是将收到的数据再发回去,不需要的可以删除。

void Usart1_Handle()     //USART1对接收的一帧数据进行处理
{
   DMA_Usart1_Send(rx1_buffer, rx1_len);	//将接收到的数据重新发送回去。
   rx1_len = 0;//清除计数
   rec1_end_flag = 0;//清除接收结束标志位
   HAL_UART_Receive_DMA(&huart1,rx1_buffer,BUFFER_SIZE);//重新打开DMA接收
}

实现外部中断EXTI0回调函数

  在void Usart1_Handle()函数下面,也是/* USER CODE END 1 */的上面,实现终端回调,当CPU收到中断申请时,将会执行下面的函数。

void EXTI0_IRQHandler(void)
{
    if(EXTI->PR & 1<<0){	//判断中断线是否为EXTI0
        EXTI->PR |= 1<<0;	//清除中断线
        Usart1_Handle();	//调用串口中断处理函数
    }
}
/* USER CODE END 1 */

在函数中判断是否为中断线0,如果是EXTI0,则清除中断标志,并执行串口数据处理函数。

在这里插入图片描述

添加IDLE中断处理函数到串口中断回调函数

  打开stm32f1xx_it.c文件,在文件头加入usart.h头文件。否则不能调用usart.c中的函数。

/* USER CODE BEGIN Includes */
#include "usart.h"
/* USER CODE END Includes */

  在stm32f1xx_it.c文件中找到void USART1_IRQHandler(void)函数,在函数中加入Usart1_IDLE();函数。

  /* USER CODE BEGIN USART1_IRQn 0 */
    Usart1_IDLE();
  /* USER CODE END USART1_IRQn 0 */

在这里插入图片描述

启动串口接收

  打开main.c文件,在while(1)循环前的/* USER CODE BEGIN 2 *//* USER CODE END 2 */中,配置软中断语句的后面,加入启动串口接收与启动定时器的语句。

    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);			 //使能IDLE中断
    HAL_UART_Receive_DMA(&huart1,rx1_buffer,BUFFER_SIZE);		//启动串口DMA接收
    HAL_TIM_Base_Start_IT(&htim1);											//启动定时器1

实现定时器中断回调

  打开tim.c文件,在文件末尾加入中断回调函数。判断是否时TIM1触发了中断

/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim == &htim1)	//判断是否时TIM1触发中断
    {
        
    }
}
/* USER CODE END 1 */

添加用户代码

  在main.c中声明一个全局变量task1用作一个间隔执行的任务标志。并在main.h中做外部声明。
  在主循环中,判断task1是否等于500;如果大于等于500,则清空task1并执行printf("hello world\r\n")语句。

      if(task1>=500){
          task1 = 0;
          printf("hello world\r\n");
      }

在这里插入图片描述

  打开timc文件,在定时器回调函数中写入task1自增的语句。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if(htim == &htim1)  //判断是否时TIM1触发中断
    {
        task1++;
    }
}

编译下载工程

  相关工程源码已推送到Gitee:STM32-code
  可以看到,系统复位后,单片机循环发出数据,并对接收到的数据进行返回。
在这里插入图片描述

异常问题与解决方法

  系统复位后能够接收到单片机发送的数据,但串口助手向单片机发送数据后,单片机卡死。

  1. 检查串口接收是否打开。
  2. 检查IDLE中断是否打开。
  3. 检查IDLE中断处理函数是否加入串口中断回调函数。
  4. 检查软中断配置是否正常。
  5. 检查软中断处理函数名称是否与启动文件startup_stm32f103xb.s中对应的中断线回调函数名称相同。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐