STM32F103 UART中断服务函数:实现中断处理

在程序中,CPU对外界突发事件进行处理的方式又两种:

1》轮询系统:(在main中,使用while循环,进行循环判断外界事物是否发生)
        while(1){
            
        }

2》前后台系统:(此时main中的while中的程序是在处理其它事务,当中断来到时,就处理中断服务函数)
        轮询系统+中断 

中断的过程:

在主程序执行的过程中,中断突然发生,此时主程序停止往下执行,并将CPU的当前状态保持在内核栈中(即:现场保护)。然后跳转到中断服务函数的入口,并执行中断服务函数,当中断服务函数执行完后,再将之前保存在内核栈中的状态全部进行出栈,将状态恢复到发生中断之前(即:现场恢复),此时对于CPU来说,就好像从来没有发生过中断一样。

中断管理器——异常处理器NVIC


 我们可以在这里寻找到,NVIC的中断优先级分组。

中断异常处理器—–NVIC—专门用于管理中断的片上外设
        管理中断的方式:(注意:整个程序, 管理中断的方式,只能五选其一,如果设置多个,那么只有最后一个设置生效)
          *      NVIC_PriorityGroup_0:    0 bits for pre-emption priority     //没有主优先级,即:0个位来表示主优先级
          *                                                  4 bits for subpriority                  //16种次优先级,即:4个位来表示次优先级
          *      NVIC_PriorityGroup_1:    1 bits for pre-emption priority    //2种主优先级,即:1个位来表示主优先级
          *                                                  3 bits for subpriority                  //8种次优先级,即:3个位来表示主优先级

 NVIC_PriorityGroup_1的分配如下图:

          *      NVIC_PriorityGroup_2:     2 bits for pre-emption priority
          *                                                  2 bits for subpriority
          *      NVIC_PriorityGroup_3:     3 bits for pre-emption priority
          *                                                  1 bits for subpriority
          *      NVIC_PriorityGroup_4:     4 bits for pre-emption priority
          *                                                  0 bits for subpriority
         

        NVIC给每个中断源都配置两种优先级,分别是:(数值越小优先级越高)
        抢占优先级(主优先级):具备抢占功能,优先级高的中断可以抢占优先级低的中断,但是一个中断不能抢占另外一个相同优先级的中断。由此可知,一个中断不能抢占它自己,因为自己与自己的优先级相同。
        执行优先级(次优先级):不具备抢占功能,只有在两个主优先级相同的中断,并且它们同时发生的情况下,那么次优先级高的先执行。

总结:配置中断的3个步骤(不管什么时候,中断配置都是这四步)

        1.配置中断源

        2.配置中断源的抢占和执行优先级

        3.编写中断服务函数

        4.如果是程序的第一个中断,需要设置优先级分组(最好写在主程序中)

配置中断的4步细节描述:(以UART中断为例)

配置一个中断需要做以下几件事情:
        1,配置中断源
            void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState)
      

         2,给这个中断源配置相应的抢占优先级和执行优先级
            void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
            typedef struct
            {
              uint8_t NVIC_IRQChannel; //中断通道号 refer to stm32f10x.h 
                //typedef enum IRQn
              uint8_t NVIC_IRQChannelPreemptionPriority; //主优先级
              uint8_t NVIC_IRQChannelSubPriority;//次优先级        
              FunctionalState NVIC_IRQChannelCmd; //使能中断通道         
            } NVIC_InitTypeDef;
        

        3,编写中断服务函数—-中断服务函数的函数名,已经在启动文件中定义,所以中断服务函数的函数名在启动文件中找
        
       注意:中断服务函数写在,stm32f10x_it.c文件中,该文件在apps文件夹下
            void USART1_IRQHandler(void)
            {
                //判断标志位
                //清除中断标志位
            }
        

        4,如果是程序的第一个中断,需要设置优先级分钟(最好写在main函数中,写在子函数中会被子函数重复调用)
            void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)

怎样查看中断源的通道号:

         我们可以在stm32f10x.h文件中找到,在与我们所使用的启动文件名字(即:STM32F10X_HD)一样的那部分定义。再到需要使用的中断源的名字,它的右边便是中断通道号。我们还注意到了这部分的通道号是从18开始的,是因为这些通道号是这款芯片专用的通道号,0~17通道号在该文件的前面,它是通用的通道号。

        注意:中断服务函数的函数名不能乱写。必须在启动文件中寻找到中断服务函数的名字。因为函数名就是函数的入口地址,这个地址在启动文件中就定义好了的。中断服务函数的函数名如果写错了会导致中断服务函数无法执行。

UART中断配置函数

UART中断源配置函数。

USART_IT_RXNE:表示当UART串口接收到数据的时候,就触发UART中断。

 中断标志位,如果相应的事件发生中断,则该标志位置1

 

 NVIC中断管理器配置函数。

UART接收数据函数,即UART_RX接收数据进CPU来。

 

UART发送数据函数,即UART_TX发送数据出去。

编写程序

void USART1_Config(void)
{
    GPIO_InitTypeDef  GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    NVIC_InitTypeDef  NVIC_InitStruct;
    // 1,打开时钟—GPIOA,串口1,AFIO 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO|RCC_APB2Periph_USART1,ENABLE);

    // 2,GPIO初始化
    GPIO_InitStruct.GPIO_Pin    =GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode   =GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed  =GPIO_Speed_2MHz;
    GPIO_Init(GPIOA,&GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin    =GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode   =GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA,&GPIO_InitStruct);    

    // 3,串口初始化
    USART_InitStruct.USART_BaudRate      =115200;
    USART_InitStruct.USART_HardwareFlowControl  =USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode                 =USART_Mode_Rx|USART_Mode_Tx;
    USART_InitStruct.USART_Parity               =USART_Parity_No;
    USART_InitStruct.USART_StopBits             =USART_StopBits_1;
    USART_InitStruct.USART_WordLength           =USART_WordLength_8b;
    USART_Init(USART1,&USART_InitStruct);

    USART_ClearFlag(USART1, USART_FLAG_TC|USART_FLAG_TXE);

    // 1,配置中断源
    USART_ITConfig(USART1,USART_IT_RXNE, ENABLE);        //当UART串口接收到数据的时候,就触发UART中断。

    // 2,给这个中断源配置相应的抢占优先级和执行优先级
    NVIC_InitStruct.NVIC_IRQChannel =USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority  =0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority         =0;
    NVIC_InitStruct.NVIC_IRQChannelCmd                 =ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 4,使能串口
    USART_Cmd(USART1,ENABLE);
}

//中断服务函数(写在stm32f10x_it.c文件中的指定位置,这个位置如下图)

 该文件的这个位置是专门写外设中断的地方,其中USART1_IRQHandler为中断函数名

/*中断服务函数:功能:pc端的串口助手通过UART_RX发送数据给MCU,接收到数据后,MCU通过UART_TX发送数据给PC端的串口助手*/

void USART1_IRQHandler(void)        //
{
    if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){    //如果UART串口接收到了数据,那么该标志位置1
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);        //手动清空标志位,一定要清空!
        USART_SendData(USART1,USART_ReceiveData(USART1));        //通过UART_TX发送数据给PC端的串口助手
    }
}

注意:无论什么中断,在进入中断后必须进行中断标志位的判断 ,即:

        if(SET==USART_GetITStatus(USART1,USART_IT_RXNE))…..,因为由上面可知,触发USART1中断的事件有很多种,我们并不知道是哪一种事件触发了USART1中断,所以,我们需要对相应的中断触发事件标志位进行判断,然后做相应的中断程序处理。

        注意:中断服务函数越短越好。如果是定时器中断,那么定时器中断服务函数执行的时间大于定时的时间的话,每当执行完定时器中断函数后,就立马又加入该中断。因为定时器中断在进入中断后,就立马重新开始计数,当计数完后,触发下一次中断,但是如果此时中断函数还没有执行完,那么定时器不会马上执行下一次中断(因为自己不能抢占自己),并且计数器此时不会再重新计数,而是等待中断函数执行后,立马又进入定时中断,然后计算器重新开始计数。这样的话,其他中断就没机会抢到cpu的控制权了。就算不是定时器中断,那么如果中断执行时间长的话,main函数就没时间去执行了,而且其他中断也没有机会抢到cpu的控制权。所以,在中断中不要加mS级的延时(中断执行时间都是uS级的)!!!。

        注意:外部中断触发也是如此,如果在触发中断的时候,上一次中断还没执行完,由于自己不能抢断自己,它就会等待。等到上一次中断执行完后,它就马上执行这一次的中断。

int main(void)
{
    
    RCC_ConfigTo72M();//将系统时钟配置成72MHZ 
    Systick_Config(72);
    USART1_Config();

    // 4,如果是程序的第一个中断,需要设置优先级分钟
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    while(1){
        LED_CTRL(LEDR,LED_CLOSE);
        Systick_NmsDelay(3000);
        LED_CTRL(LEDR,LED_OPEN);
        Systick_NmsDelay(3000);
   
    }

}

串口助手给MCU发送字符,控制LED的亮/灭

功能:串口助手给MCU发送open字符串,控制LED的亮,发送close字符串,控制LED的灭

方法一:(接收标志位中断)

//串口中断服务函数

/*每次MCU接收到从串口助手发送来的一个字节,MCU就会进入一次USART1中断服务函数。*/

void USART1_IRQHandler(void)
{
    if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);        //手动清空标志位,一定要清空!
        USART_SendData(USART1,USART_ReceiveData(USART1));
        USART1_Data[indexs]=USART_ReceiveData(USART1);        //将接收到的一个字节数据,保存在数组 USART1_Data[]中
        indexs++;        //数组下标后移
        USART1_Data[indexs]='\0';        //让接收到的最末尾字符的后面为'\0'
        if((USART1_Data[0]!='o')&&(USART1_Data[0]!='c')){        /*如果串口助手发送过来的第一个字符既不是字母‘o’也不是字母‘c’,那么就没必要再进行下去了,数组USART1_Data[]的索引回到0*/
            indexs=0;
        }else{
            if(indexs>3){         //open为4个字符,如果串口助手发送过来4个字符,那么有可能发送过来的是open
                if(0==strcmp("open",(const char *)USART1_Data)){        //数组中的内容与open字符串进行对比
                    LED_CTRL(LEDR,LED_OPEN);        //开灯
                    indexs=0;
                    memset(USART1_Data,0,sizeof(USART1_Data));
                }
                if(0==strcmp("close",(const char *)USART1_Data)){         //数组中的内容与close字符串进行对比
                    LED_CTRL(LEDR,LED_CLOSE);        //关灯
                    indexs=0;
                    memset(USART1_Data,0,sizeof(USART1_Data));
                }                
            
            }
        }
        if(indexs==5){        /*如果前面对比失败,说明串口助手发送过来的即不是open也不是close。又因为close为5个字符,串口助手发送过来的字符数量为5个,数组索引回归0*/
            indexs=0;
            memset(USART1_Data,0,sizeof(USART1_Data));            
        }
    }
}

方法二:(空闲总线中断)

//串口中断服务函数

void USART1_IRQHandler(void)
{

    if(SET==USART_GetITStatus(USART1,USART_IT_RXNE)){
        
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);       
        USART_SendData(USART1,USART_ReceiveData(USART1));
        USART1_Data[indexs]=USART_ReceiveData(USART1);
        
        if(USART1_Data[indexs]==0x0D || USART1_Data[indexs]==0x0A)    /*0XOD是回车,0XOA是换行,串口调试助手会在每帧数据末尾,发回车+换行符,我们需要把它两个清除*/
            USART1_Data[indexs] = '\0';
        
        indexs++;

    }
    
    if(SET==USART_GetITStatus(USART1,USART_IT_IDLE)){        //空闲总线中断
        
        USART_ReceiveData(USART1);        //清除空闲总线标志位

        if(0==strcasecmp("open",(const char *)USART1_Data)){
            LED_CTRL(LEDR,LED_OPEN);        //开灯
            
        }
        if(0==strcasecmp("close",(const char *)USART1_Data)){
            LED_CTRL(LEDR,LED_CLOSE);         //关灯
            
        }

        indexs =  0;
        memset(USART1_Data,0,sizeof(USART1_Data));
    
    }

}

我们可以看到方法二更加的方便和简洁。那么空闲总线中断是什么?

        USART1,USART_IT_IDLE:空闲总线中断标志位,当MCU的UART接收完一帧数据后,该标志位就会置1。也就是,串口助手发送一串数据给MCU。MCU则是一个字节一个字节的接收。当接收完这一串数据后,过一小段时间,如果没有数据发送过来,那么空闲总线中断标志位就会自动置1,表示MCU将这一帧数据接收完成。

单片机串行通信里面的数据帧是怎么理解?一帧数据的位数可以改变吗?

        串行通信中,帧信息一般是根据需要自己约定而确定的。其内容一般是由多个8位单字节数据组成,比如你所说的传感器,需要采集电压值,电流值等信息,假设这些信息需要10个字节,那么你的一帧信息最少需要10个字节,也就是收发两方都需要计数,计数到10时才能说明通讯完成。这是最简单的,但大多数应用中规范的做法一帧信息都会包含帧头标识符、帧长度、信息内容及校验信息。

        注意:在中断配置中如果打开了中断源,那么一定要在中断服务函数中手动将中断标志位清除。否则,中断触发的时候,标志置1,当再次进入到中断后,由于上一次中断没有清除标志位,那么程序就可能会卡在中断服务函数中。

物联沃分享整理
物联沃-IOTWORD物联网 » STM32F103 UART中断服务函数:实现中断处理

发表评论