CKS32F4xx

在上一讲,我们讲过CKS32F4xx系列的6个串口都支持DMA传输。因此本节我们对CKS32F4xx系列的DMA进行介绍,同时利用DMA对串口数据进行传输。

DMA介绍

DMA,全称为:Direct Memory Access,即直接存储器访问。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU的效率大为提高。

CKS32F4xx系列最多有2个DMA控制器(DMA1和DMA2),共16个数据流(每个控制器8个),每个数据流总共可以有多达8个通道(或称请求)。CKS32F4xx系列的DMA支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。,存储器一般是指片内SRAM、外部存储器、片内Flash等等。

外设到存储器:把外设数据寄存器内容转移到指定的内存空间。比如进行ADC采集时我们可以利用DMA传输把AD转换的数据转移到我们定义的存储区中。 

存储器到外设:把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信,比如SPI、I2C和串口等。

存储器到存储器:就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于C语言内存拷贝函数memcpy,不过利用DMA传输可以达到更高的传输效率。

注意:只有DMA2控制器支持存储器到存储器的传输,DMA1不支持。

DMA功能框图

1.png

标号1处是外设通道,每一个数据流(标号2)对应着8个外设通道,外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。DMA1各个通道的请求映像如下表所示:

2.png

DMA2各个通道的请求映像如下表所示:

3.png

比如我们利用DMA将存储器数据传输到串口3,然后发送出去,那么根据上表可知,我们可以选择DMA1的数据流3通道4,或者DMA1的数据流4通道7。而将串口3的数据传输到存储器则可以选择DMA1的数据流1通道4。

标号2处是数据流和仲裁器,一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个DMA控制器 (DMA1 或DMA2) 多个外设请求时,那必然需要同时使用多个数据流,这时就需要仲裁器对每一个数据流的优先级进行划分。数据流的优先级可以通过配置DMA_SxCR寄存器 PL[1:0]位,可以设置为非常高、高、中和 低四个级别。如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流2优先级高于数据流 3。

标号3处是FIFO,用于在源数据传输到目标地址之前临时存放数据用的。可以通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值,分别为1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。

标号4和5处是存储器端口、外设端口。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输。DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。而DMA1的存储区端口相比DMA2的要减少AHB2外设的访问权,同时DMA1外设端口是没有连接至总线矩阵的,只有连接到APB1外设,所以 DMA1不能实现存储器到存储器传输。

标号6处是编程端口,AHB从器件编程端口是连接至AHB2外设的,AHB2外设在使用 DMA传输时需要相关控制信号。

DMA软件配置

在标准库函数中对DMA进行初始化主要是对结构体DMA_InitTypeDef内的各类参数进行初始化。DMA_InitTypeDef结构体参数及介绍如下:

typedef struct
{  
    uint32_t DMA_Channel;            
    uint32_t DMA_PeripheralBaseAddr;   
    uint32_t DMA_Memory0BaseAddr;     
    uint32_t DMA_DIR;                 
    uint32_t DMA_BufferSize;          
    uint32_t DMA_PeripheralInc;       
    uint32_t DMA_MemoryInc;         
    uint32_t DMA_PeripheralDataSize;   
    uint32_t DMA_MemoryDataSize;      
    uint32_t DMA_Mode;               
    uint32_t DMA_Priority;            
    uint32_t DMA_FIFOMode;           
    uint32_t DMA_FIFOThreshold;       
    uint32_t DMA_MemoryBurst;       
    uint32_t DMA_PeripheralBurst;   
}DMA_InitTypeDef;

1)DMA_Channel:DMA请求通道的选择,每个外设对应固定的通道,具体设置值需要查25.2节中的DMA1和DMA2各个通道的请求映像表。该值在标准库中可供选择的参数值如下:

#define DMA_Channel_0                     ((uint32_t)0x00000000)
#define DMA_Channel_1                     ((uint32_t)0x02000000)
#define DMA_Channel_2                     ((uint32_t)0x04000000)
#define DMA_Channel_3                     ((uint32_t)0x06000000)
#define DMA_Channel_4                     ((uint32_t)0x08000000)
#define DMA_Channel_5                     ((uint32_t)0x0A000000)
#define DMA_Channel_6                     ((uint32_t)0x0C000000)
#define DMA_Channel_7                     ((uint32_t)0x0E000000)

2)DMA_PeripheralBaseAddr:外设地址,设定DMA_SxPAR寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区地址。比如要进行串口DMA传输,那么外设基地址为串口接收发送数据存储器USART1->DR的地址,表示方法为&USART1->DR。

3)DMA_Memory0BaseAddr:存储器0地址,设定DMA_SxM0AR寄存器值;一般设置为我们自定义存储区的首地址。比如我们程序中自己定义的用来存放数据的数组,此时这个值设置为数组名即可。

4) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。该值在标准库中可供选择的参数值如下:

DMA_DIR_PeripheralToMemory  //外设到存储器
DMA_DIR_MemoryToPeripheral  //存储器到外设
DMA_DIR_MemoryToMemory  //存储器到存储器

5)DMA_BufferSize:设定待传输数据数目,初始化设定DMA_SxNDTR寄存器的值。

6)DMA_PeripheralInc:是否使能外设地址自动递增功能,它设定DMA_SxCR寄存器的PINC位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。该值在标准库中可供选择的参数值如下:

DMA_PeripheralInc_Enable      //使能
DMA_PeripheralInc_Disable     //不使能

7)DMA_MemoryInc:是否使能存储器地址自动递增功能,它设定DMA_SxCR寄存器的MINC位的值;我们自定义的存储区一般都是存放多个数据的,所以一般是使能存储器地址自动递增功能。该值在标准库中可供选择的参数值如下:

DMA_MemoryInc_Enable      //使能
DMA_MemoryInc_Disable     //不使能

8)DMA_PeripheralDataSize:外设数据宽度,可选字节(8位)、半字(16位)和字(32位),根据外设数据长度进行选择。该值在标准库中可供选择的参数值如下:

DMA_PeripheralDataSize_Byte      //字节
DMA_PeripheralDataSize_HalfWord  //半字
DMA_PeripheralDataSize_Word     //字

9) DMA_MemoryDataSize:存储器数据宽度,可选字节(8位)、半字(16位)和字(32位),和外设数据宽度相对应。该值在标准库中可供选择的参数值如下:

DMA_MemoryDataSize_Byte      //字节
DMA_MemoryDataSize_HalfWord  //半字
DMA_MemoryDataSize_Word     //字

10) DMA_Mode:DMA传输模式选择,可选一次传输或者循环传输。该值在标准库中可供选择的参数值如下:

DMA_Mode_Normal      //一次传输
DMA_Mode_Circular     //循环传输

11) DMA_Priority:软件设置数据流的优先级,有4个可选优先级分别为非常高、高、中和低。DMA优先级只有在多个DMA数据流同时使用时才有意义,如果只有一个数据流的话,设置成非常高优先级就可以了。该值在标准库中可供选择的参数值如下:

DMA_Priority_Low         //低
DMA_Priority_Medium     //中
DMA_Priority_High       //高
DMA_Priority_VeryHigh   //非常高

12) DMA_FIFOMode:FIFO模式使能,如果设置为DMA_FIFOMode_Enable 表示使能FIFO模式功能;如果采用直接传输模式,则不需要使用FIFO模式。直接模式下,DMA直接进行数据从源地址到目的地址的传输。而FIFO模式下,可以将要传输的多个数据(或字节)累计存储在FIFO缓冲器中,然后在FIFO缓冲器中设置存储阈值,当到达阈值时,FIFO会自动把所有存储的数据一次性的发送到目标地址。该值在标准库中可供选择的参数值如下:

DMA_FIFOMode_Disable  //不使能
DMA_FIFOMode_Enable  //使能

13) DMA_FIFOThreshold:FIFO阈值选择,可选4种状态分别为FIFO容量的1/4、1/2、3/4和满;不使用FIFO模式时,该设置改值无效。该值在标准库中可供选择的参数值如下:

DMA_FIFOThreshold_1QuarterFull         //1/4
DMA_FIFOThreshold_HalfFull            //1/2
DMA_FIFOThreshold_3QuartersFull       //3/4
DMA_FIFOThreshold_Full              //满

14) DMA_MemoryBurst:存储器突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式。单次传输模式下,一次操作(软件)只能传输一次,突发传输模式下,一次操作可以传输多次,如4次,8次,16次。该值在标准库中可供选择的参数值如下:

DMA_MemoryBurst_Single            //单次
DMA_MemoryBurst_INC4            //4节拍
DMA_MemoryBurst_INC8           //8节拍
DMA_MemoryBurst_INC16          //16节拍

15) DMA_PeripheralBurst:外设突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式。该值在标准库中可供选择的参数值如下:

DMA_PeripheralBurst_Single        //单次
DMA_PeripheralBurst_INC4         //4节拍
DMA_PeripheralBurst_INC8         //8节拍
DMA_PeripheralBurst_INC16        //16节拍

串口DMA接发通信实验

串口的DMA接发通信实验是存储器到外设和外设到存储器的数据传输。在第24课串口通信的基础上编写而成。

1.编程要点

1)配置USART通信功能;

2)配置DMA通信功能,存储器到外设和外设到存储器两种模式的配置。

3)使能指定的DMA数据流中断;

4)使能USART3的DMA发送和接收请求;

5)开始一次DMA传输。

2.代码分析

代码中有关串口的配置的程序在第24课已经详细讲过了,这里就不再讲述。主要是对DMA相关的代码进行分析,相关程序在dma.c文件里。

代码清单1:DMA初始化配置

void MYDMA_Config(void)
{   
    DMA_InitTypeDef  DMA_InitStructure;  
    NVIC_InitTypeDef NVIC_InitStructure;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);  
    while (DMA_GetCmdStatus(DMA1_Stream3) != DISABLE){}   
    //存储器到外设的DMA配置  
    DMA_DeInit(DMA1_Stream3);  
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;  //通道选择  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART3->DR);//DMA外设地址  
    DMA_InitStructure.DMA_Memory0BaseAddr =(uint32_t)USART3_TX_BUF;//DMA 存储器0地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式  
    DMA_InitStructure.DMA_BufferSize = USART3_MAX_TX_LEN;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级  
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;           
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;  
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输  
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输  
    DMA_Init(DMA1_Stream3, &DMA_InitStructure);//初始化DMA Stream  
    DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_TCIF3);                         //清除标志位      
    DMA_Cmd(DMA1_Stream3, DISABLE);                                          //关闭DMA  
    DMA_ITConfig(DMA1_Stream3,DMA_IT_TC,ENABLE);     
    NVIC_InitStructure.NVIC_IRQChannel                   = DMA1_Stream3_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;  
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;  
    NVIC_Init(&NVIC_InitStructure);  USART_DMACmd(USART3,USART_DMAReq_Tx,ENABLE);       
    //外设到存储器的DMA配置  
    DMA_DeInit(DMA1_Stream1);  
    while (DMA_GetCmdStatus(DMA1_Stream1) != DISABLE){}//等待DMA可配置   
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;  //通道选择  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART3->DR);;//DMA外设地址  
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)USART3_RX_BUF;//DMA 存储器0地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//存储器到外设模式  
    DMA_InitStructure.DMA_BufferSize = USART3_MAX_RX_LEN;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;//最高优先级  
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;           
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;  
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输  
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输  
    DMA_Init(DMA1_Stream1, &DMA_InitStructure);//初始化DMA Stream  
    DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1);                         //清除标志位      
    DMA_Cmd(DMA1_Stream1, ENABLE);                                          //关闭DMA  
    DMA_ITConfig(DMA1_Stream1,DMA_IT_TC,ENABLE);   //使能DMA1数据流1的传输完成中断  
    NVIC_InitStructure.NVIC_IRQChannel                   = DMA1_Stream1_IRQn;  
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;  
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;  
    NVIC_Init(&NVIC_InitStructure);  
    USART_DMACmd(USART3,USART_DMAReq_Rx,ENABLE);     
}

这段代码主要是根据第25.3节中讲解的内容对DMA进行配置的。串口发送数据我们利用的是DMA数据流3通道4,存储器是我们定义的一个数组USART3_TX_BUF,外设是串口的数据寄存器USART_DR,表示方法为&USART3->DR。串口接收数据我们利用的是DMA数据流1通道4,存储器是我们定义的一个数组USART3_RX_BUF,外设是串口的数据寄存器USART_DR,表示方法为&USART3->DR。

代码清单2:DMA1_Stream3_IRQHandler函数

void DMA1_Stream3_IRQHandler(void)  //DMA发送数据流中断服务函数
{    
    if(DMA_GetITStatus(DMA1_Stream3,DMA_IT_TCIF3) != RESET)       
    {          
        DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_TCIF3);         
        DMA_Cmd(DMA1_Stream3,DISABLE); //关闭DMA          
        USART3_DMA_Tx_flag = 0; //发送完成    
    }       
}

该函数是DMA1数据流3中断服务函数,在函数里我们通过对DMA_IT_TCIF3标志位的判断,可以知道数据流3的数据是否传输完成,然后清除相应的标志位和关闭DMA传输。

代码清单3:DMA1_Stream1_IRQHandler函数

void DMA1_Stream1_IRQHandler(void) //DMA接收数据流中断服务函数
{    
    if(DMA_GetITStatus(DMA1_Stream1,DMA_IT_TCIF1) != RESET)       
    {          
        DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1); //清除标志位            
        DMA_Cmd(DMA1_Stream1,DISABLE); //关闭DMA      
    }   
}

该函数的功能和DMA1数据流3中断服务函数的功能是一样的。

代码清单4:USART3_IRQHandler函数

void USART3_IRQHandler(void)                  
{   
    if(USART_GetITStatus(USART3 , USART_IT_IDLE) != RESET)     
    {    
        USART_ReceiveData(USART3);//清除空闲中断标志    
        DMA_Cmd(DMA1_Stream1,DISABLE);//关闭DMA ,防止干扰      
        USART3_RX_STA = USART3_MAX_RX_LEN - DMA_GetCurrDataCounter(DMA1_Stream1);//获得接收到的字节数      
        DMA_SetCurrDataCounter(DMA1_Stream1,USART3_MAX_RX_LEN);     
        DMA_ClearITPendingBit(DMA1_Stream1,DMA_IT_TCIF1);//比f103多的一句         
        DMA_Cmd(DMA1_Stream1,ENABLE);// DMA 开启,等待数据。    
        USART3_RX_BUF[USART3_RX_STA&0X7FFF]='\0';    
        USART3_RX_STA|=0x8000;   //最高位置1,标记接收完成了                                                                                                   
    }  
    if(USART_GetFlagStatus(USART3,USART_FLAG_ORE) == SET)  // 检查 ORE 标志,防止开关总中断死机,放在接收中断前面  
    {    
        USART_ClearFlag(USART3,USART_FLAG_ORE);    
        USART_ReceiveData(USART3);  
    }
}

该函数是串口3的中断服务函数。在函数里判断接收完成是通过串口空闲中断的方式实现,即当串口数据流停止后,就会产生IDLE中断。然后在中断里依次做了以下事情:处理接收buff的数据;关闭串口接收DMA通道,防止后面接收到数据产生串扰;计算接收到的字节数存储在USART3_RX_STA变量中;重新设置DMA下次要接收的数据字节数;清除DMA1数据流1接收完成标志位;开启DMA通道,等待下一次的数据接收;置位标志位USART3_RX_STA,表示接收完成了。同时为了防止串口溢出错误,程序卡死在串口中断里,在中断服务函数里增加了溢出错误的处理。

代码清单5:主函数

int main(void)
{  
    GPIO_Configuration();  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2  
    USART_Configuration();  
    printf("start\r\n");  
    while (1)  
    {    
        if(USART3_RX_STA&0x8000)    
        {        
            //printf("USART3_Read:%s",USART3_RX_BUF);      
            printf("USART3_Read:%s",USART3_RX_BUF);      
            USART3_RX_STA=0;      
            memset(USART3_RX_BUF,0,sizeof(USART3_RX_BUF));    
        }  
    }
}

主函数的编写逻辑比较简单,首先是各类外设的初始化,包括GPIO初始化、NVIC中断初始化、串口初始化。然后在while循环里等待接收完成标志置位,将接收到的数据再发送到串口调试助手,并利用memset函数将存储数据的数组USART3_RX_BUF清零。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 12

串口通讯 (Serial Communication) 是一种设备间非常常用的串行通讯方式,因为它简单便捷,大部分电子设备都支持该通讯方式。串口在CKS32上应用最多的莫过于“打印”程序信息,一般在硬件设计时都会预留一个串口连接电脑,用于在调试程序时可以把一些调试信息“打印”在电脑端的串口调试助手工具上,从而了解程序运行是否正确、指出程序运行出错位置等等。

CKS32F4xx系列产品串口介绍

CKS32F4xx系列最多可提供6路串口,其中四个USART和两个UART。USART和UART在引脚上的区别是:UART只有RX和TX引脚,而USART除了这两个引脚之外,还有流控引脚RTS和CTS,以及时钟引脚SCLK。CKS32F4xx系列产品的USART1和USART6时钟来源于APB2总线时钟,其最大频率为84MHz,因此这两个串口的通信速度最高可达10.5Mbit/s。而其它四个的时钟来源于APB1总线时钟,其最大频率为42MHz,因此这四个串口的通信速度最高可达5.25Mbit/s。因为USART有SCLK引脚,因此CKS32F4xx系列产品的USART具有同步通信功能,而UART只有异步通信功能。同时USART还支持ISO7816的智能卡接口。但是当USART和UART都用在异步通信的时候,两者是没有什么区别的。CKS32F4xx系列的6个串口都支持DMA传输。

CKS32F4xx系列产品的串口在发送数据时,当发送使能位TE置1之后,发送器开始会先发送一个空闲帧(一个数据帧长度的高电平),然后就可以往USART_DR寄存器写入要发送的数据。在写入最后一个数据后,需要等USART状态寄存器(USART_SR)的TC位为1,表示数据传输完成,如果USART_CR1寄存器的TCIE位置1,将产生中断。串口发送的一个字符帧由三个部分组成:起始位+数据帧+停止位。起始位是一个位周期的低电平;数据帧就是我们要发送的8位或9位数据,数据是从最低位开始传输的;停止位是一定时间周期的高电平。停止位时间长短是可以通过USART控制寄存器2(USART_CR2)的STOP[1:0]位控制,可选0.5个、1个、1.5个和2个停止位。默认使用1个停止位。2个停止位适用于正常USART模式、单线模式和调制解调器模式。0.5个和1.5个停止位用于智能卡模式。

CKS32F4xx系列产品的串口在接收数据时,需要先将USART_CR1寄存器的RE 位置1,使能USART接收,使得接收器在RX线开始搜索起始位。在确定到起始位后就根据RX线电平状态把数据存放在接收移位寄存器内。接收完成后就把接收移位寄存器数据移到RDR内,并把USART_SR寄存器的RXNE位置1,同时如果 USART_CR2寄存器的RXNEIE置1的话可以产生中断。

CKS32F4xx系列产品控制器的USART支持奇偶校验。当使用校验位时,串口传输的长度将是8位的数据帧加上1位的校验位总共9位,奇偶校验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收数据时自动验证校验位。接收数据时如果出现奇偶校验位验证失败,则可以产生奇偶校验中断。使能了奇偶校验控制后,每个字符帧的格式将变成:起始位+数据帧 +校验位+停止位。

USART有多个中断请求事件,具体如下表所示:在串口的中断服务函数里,通过对这些中断事件标志的检测,就可以判断出是何种事件发生,然后再做出相应的处理。

1.jpg

CKS32F4xx系列产品串口的配置

接下来我们讲解如何利用CKS32F4xx系列固件库来完成对串口的配置使用。首先标准库函数定义了一个串口初始化结构体USART_InitTypeDef,结构体成员用于设置串口的工作参数,并由外设初始化配置函数USART_Init()调用,从而完成对串口相应寄存器的配置,进一步达到完成对串口配置的目的。

typedef struct 
{ 
    uint32_t USART_BaudRate;   // 波特率 
    uint16_t USART_WordLength; // 字长 
    uint16_t USART_StopBits;    // 停止位 
    uint16_t USART_Parity;      // 校验位 
    uint16_t USART_Mode;      // USART 模式 
    uint16_t USART_HardwareFlowControl; // 硬件流控制 
} USART_InitTypeDef;

结构体中各个成员变量的介绍及初始化时可被赋的值如下:

1) USART_BaudRate:波特率设置。一般设置为 2400、9600、19200、115200。标准库函数会根据设定值计算得到USARTDIV值,并设置USART_BRR寄存器值。

2) USART_WordLength:数据帧字长,可选8位或9位。它设定USART_CR1 寄存器的M位的值。如果没有使能奇偶校验控制,一般使用8位数据帧长;如果使能了奇偶校验则一般设置为9位数据帧长。

#define USART_WordLength_8b                  ((uint16_t)0x0000)
#define USART_WordLength_9b                  ((uint16_t)0x1000)

3) USART_StopBits: 停止位设置,可选0.5个、1个、1.5个和 2个停止位,它设定USART_CR2寄存器的STOP[1:0]位的值,一般我们选择1个停止位。

#define USART_StopBits_1                     ((uint16_t)0x0000)
#define USART_StopBits_0_5                   ((uint16_t)0x1000)
#define USART_StopBits_2                     ((uint16_t)0x2000)
#define USART_StopBits_1_5                   ((uint16_t)0x3000)

4) USART_Parity: 奇偶校验控制选择,可选USART_Parity_No(无校验)、USART_Parity_Even(偶校验)以及USART_Parity_Odd(奇校验),它设定 USART_CR1寄存器的PCE位和PS位的值。

#define USART_Parity_No                      ((uint16_t)0x0000)
#define USART_Parity_Even                    ((uint16_t)0x0400)
#define USART_Parity_Odd                     ((uint16_t)0x0600)

5) USART_Mode: USART模式选择,有USART_Mode_Rx和USART_Mode_Tx,允许使用逻辑或运算选择两个,它设定USART_CR1寄存器的RE位和TE位。

#define USART_Mode_Rx                        ((uint16_t)0x0004)
#define USART_Mode_Tx                        ((uint16_t)0x0008)

6) USART_HardwareFlowControl: 硬件流控制选择,只有在硬件流控制模式才有效,可选使能RTS、使能CTS、同时使能RTS和CTS、不使能硬件流。

#define USART_HardwareFlowControl_None       ((uint16_t)0x0000)
#define USART_HardwareFlowControl_RTS        ((uint16_t)0x0100)
#define USART_HardwareFlowControl_CTS        ((uint16_t)0x0200)
#define USART_HardwareFlowControl_RTS_CTS    ((uint16_t)0x0300)

当使用同步模式时需要配置SCLK引脚输出脉冲的属性,标准库使用一个时钟初始化结构体USART_ClockInitTypeDef来设置,不使用时不需要设置。

typedef struct 
{ 
    uint16_t USART_Clock; // 时钟使能控制 
    uint16_t USART_CPOL; // 时钟极性 
    uint16_t USART_CPHA; // 时钟相位 
    uint16_t USART_LastBit; // 最尾位时钟脉冲 
} USART_ClockInitTypeDef;

结构体中各个成员变量的介绍及初始化时可被赋的值如下:

1) USART_Clock: 同步模式下SCLK引脚上时钟输出使能控制,可选禁止时钟输出(USART_Clock_Disable)或开启时钟输出(USART_Clock_Enable);如果使用同步模式发送,一般都需要开启时钟。它设定USART_CR2寄存器的CLKEN位的值。

#define USART_Clock_Disable                  ((uint16_t)0x0000)
#define USART_Clock_Enable                   ((uint16_t)0x0800)

2) USART_CPOL: 同步模式下SCLK引脚上输出时钟极性设置,可设置在空闲时SCLK引脚为低电平(USART_CPOL_Low)或高电平(USART_CPOL_High)。它设定USART_CR2寄存器的CPOL位的值。

#define USART_CPOL_Low                       ((uint16_t)0x0000)
#define USART_CPOL_High                      ((uint16_t)0x0400)

3) USART_CPHA: 同步模式下SCLK引脚上输出时钟相位设置,可设置在时钟第一个变化沿捕获数据(USART_CPHA_1Edge)或在时钟第二个变化沿捕获数据。它设定USART_CR2寄存器的CPHA位的值。USART_CPHA与USART_CPOL配合使用可以获得多种模式时钟关系。

#define USART_CPHA_1Edge                     ((uint16_t)0x0000)
#define USART_CPHA_2Edge                     ((uint16_t)0x0200)

4) USART_LastBit: 选择在发送最后一个数据位的时候时钟脉冲是否在 SCLK引脚输出,可以是不输出脉冲(USART_LastBit_Disable)、输出脉冲 (USART_LastBit_Enable)。它设定USART_CR2寄存器的LBCL位的值。

#define USART_LastBit_Disable                ((uint16_t)0x0000)
#define USART_LastBit_Enable                 ((uint16_t)0x0100)

要完成串口正常的收发数据,还需要标准库中的这些函数配合使用。

(1) void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)函数:

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{  
    assert_param(IS_USART_ALL_PERIPH(USARTx));  
    assert_param(IS_USART_DATA(Data));   
    USARTx->DR = (Data & (uint16_t)0x01FF);
}

该函数的功能是向串口寄存器USART_DR写入一个数据,有两个入口参数,第一个是选择是哪个串口,其可选择的值为USART1、USART2、USART3、USART6、UART4、UART5。第二个参数是待发送的数据,其值只要满足如下条件即可:

#define IS_USART_DATA(DATA) ((DATA) <= 0x1FF)

(2) uint16_t USART_ReceiveData(USART_TypeDef* USARTx)函数:

uint16_t USART_ReceiveData(USART_TypeDef* USARTx)
{  
    assert_param(IS_USART_ALL_PERIPH(USARTx));  
    return (uint16_t)(USARTx->DR & (uint16_t)0x01FF);
}

该函数的功能是从USART_DR寄存器读取串口接收到的数据,只有一个入口参数,即选择是哪个串口。

(3) void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState)函数:

void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState)
{  
    assert_param(IS_USART_ALL_PERIPH(USARTx));  
    assert_param(IS_FUNCTIONAL_STATE(NewState));  
    if (NewState != DISABLE)  
    {    
        USARTx->CR1 |= USART_CR1_UE;  
    }  
    else  
    {  
        USARTx->CR1 &= (uint16_t)~((uint16_t)USART_CR1_UE);  
    }
}

要完成串口正常的收发数据,还需要标准库中的这些函数配合使用。

该函数的功能是使能串口。有两个入口参数,第一个是选择是哪个串口,其可选择的值为USART1、USART2、USART3、USART6、UART4、UART5。第二个参数是使能或者不使能,其值为DISABLE或者ENABLE。

(4) void USART3_IRQHandler(void) 串口中断服务程序函数:

当发生中断的时候,程序就会执行中断服务函数。然后我们在中断服务函数中编写我们相应的逻辑代码即可。

(5) FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG)函数:

该函数的功能是读取串口的状态,第一个入口参数和上面的一样。这里重点讲解第二个入口参数,它是标示我们要查看串口的哪种状态,可选的值及其代表的意义如表格所示:

2.jpg

(6) ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT)函数:

当我们使能了某个中断的时候,当该中断发生了,就会设置状态寄存器中的某个标志位。经常我们在中断处理函数中,要判断该中断是哪种中断,这时候就会使用该函数。第二个入口参数可选的值及其代表的意义如表格所示:

3.jpg

串口接发通信实验

接下来我们根据上面讲解的串口通信的知识,实际编写一个软件程序实现串口的接发通信。代码实现的现象是在开发板一上电时会通过print函数发送一串字符串“start”给电脑,然后开发板进入中断接收等待状态。如果电脑有发送数据过来,开发板就会产生中断, 我们在中断服务函数里接收数据,并将接收到数据标志位置1,在主函数里对标志位经过判断之后再把数据返回发送给电脑。

1.编程要点

1) 使能RX和TX引脚GPIO时钟和USART3时钟;

2) 初始化GPIO,并将GPIO复用到USART3上;

3) 配置USART3参数;

4) 配置中断控制器并使能USART3接收中断;

5) 使能USART3;

6) 在USART3接收中断服务函数里接收数据并将接收到数据的标志位置1。

2.代码分析

代码清单1:USART3初始化配置

其初始化串口的过程和我们前面讲解的编程要点中的过程是一致的。因为我们使用到了串口的中断接收,因此需要开启串口3的NVIC中断并对其进行配置。

void uart_init(u32 bound)
{    
    GPIO_InitTypeDef GPIO_InitStructure;  
    USART_InitTypeDef USART_InitStructure;  
    NVIC_InitTypeDef NVIC_InitStructure;
    //使能RX和TX引脚GPIO时钟  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
    //使能USART3时钟  
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);  
    //初始化GPIO,并将GPIO复用到USART3上  
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource10,GPIO_AF_USART3);   
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource11,GPIO_AF_USART3);   
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;   
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;   
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;   
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;   
    GPIO_Init(GPIOB,&GPIO_InitStructure);    
    //配置USART3参数  
    USART_InitStructure.USART_BaudRate = bound;    //波特率设置  
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;   
    USART_InitStructure.USART_StopBits = USART_StopBits_1;   
    USART_InitStructure.USART_Parity = USART_Parity_No;   
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;   
    USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;      
    USART_Init(USART3, &USART_InitStructure); 
    //使能USART3    
    USART_Cmd(USART3, ENABLE); 
    //配置中断控制器并使能USART3接收中断;  
    #if EN_USART3_RX    
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);   
    NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;   
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;  
    NVIC_InitStructure.NVIC_IRQChannelSubPriority =3;    
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;      
    NVIC_Init(&NVIC_InitStructure);  
    #endif  
}

代码清单2:USART3中断服务函数

当USART3有接收到数据时就会执行USART3_IRQHandler函数。然使用if 语句来判断是否是真的产生USART3数据接收这个中断事件,如果是真的就使用 USART数据读取函数USART_ReceiveData读取数据到指定存储区Res,并将自己定义的一个标志位Rxflag置1。

void USART3_IRQHandler(void)                {  if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)   {    Res =USART_ReceiveData(USART3);//(USART1->DR);      Rxflag=1;           } }

代码清单3:字符函数

//发送一个字符函数
static void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch )
{  
    USART_SendData(pUSARTx,ch);  
    while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);  
}
//发送指定长度字符的函数
void Usart_SendStr_length( USART_TypeDef * pUSARTx, uint8_t *str,uint32_t strlen )
{  
    unsigned int k=0;    
    do     
    {        
        Usart_SendByte( pUSARTx, *(str + k) );        
        k++;    
    } while(k < strlen);
}

Usart_SendByte函数用来在指定USART发送一个ASCLL码值字符,它有两个形参,第一个为USART,第二个为待发送的字符。它是通过调用库函数 USART_SendData来实现的,并且增加了等待发送完成功能。

Usart_SendString函数用来发送一个字符串,它实际是调用 Usart_SendByte函数发送每个字符,直到遇到空字符才停止发送。最后使用循环检测发送完成的事件标志来实现保证数据发送完成后才退出函数。

代码清单4:printf函数支持

#if 1
#pragma import(__use_no_semihosting)                             
struct __FILE 
{   
    int handle; 
}; 
FILE __stdout;         
void _sys_exit(int x) 
{   
    x = x; 
} 
int fputc(int ch, FILE *f)
{     
    while((USART3->SR&0X40)==0);//循环发送,直到发送完毕     
    USART3->DR = (u8) ch;        
    return ch;
}
#endif

这段代码是引入printf函数支持所必须的,加入这段代码加入之后便可以通过printf函数向串口发送我们需要的内容,方便开发过程中查看代码执行情况以及一些变量值。如果我们使用不同的串口,对这段代码的修改一般也只是用来改变 printf 函数针对的串口号,比如将上述代码中的USART3改成USART1即可。

代码清单5:主函数

u8 Res;u8 Rxflag;
u8 USART_RX_BUF[USART_REC_LEN];  
u8 usRxCount=0; int main(void)
{   
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  
    delay_init(168);       
    uart_init(115200);     
    printf("start\r\n");  
    while(1)  
    {    
        if(Rxflag)    
        {      
            if (usRxCount < sizeof(USART_RX_BUF))      
            {        
                USART_RX_BUF[usRxCount++] = Res;      
            }      
            else      
            {        
                usRxCount = 0;      
            }      
            /* 遇到换行字符,就把数据发送到串口助手*/    
            if (Res == 0x0A)  /* 换行字符 */    
            {         
                Usart_SendStr_length(USART3,USART_RX_BUF, usRxCount );        
                usRxCount = 0;      
            }      Rxflag=0;    
        }  
    }
}

首先我们调用NVIC_PriorityGroupConfig函数完成对NVIC的初始化,然后调用uart_init函数完成对串口的初始化,这里将串口波特率设置成可115200(位/秒)。接着利用printf函数发送一次“start”到串口调试助手。然后对Rxflag的值进行判断,当接收到了数据,即Rxflag的值为1时,对接收的数据长度进行判断,USART_REC_LEN是我们定义的接收最大字节数,这个值可以根据自己的需要进行修改。当接收的数据在最大字节数范围之内时,把接收到的数据赋值到数组USART_RX_BUF里,同时当接收到的数据为0x0A,即换行字符时,利用Usart_SendStr_length函数将接收到的数据发送出去。因此在利用串口调试助手向MCU发送数据时,要勾选“加回车换行符”。

在本程序中我们设置串口进入中断的方式为数据寄存器非空即进一次中断,因此每个字节的接收都会进一次中断,这会导致CPU的效率大大降低,因此在下一节我们将会讲解利用DMA的方式对串口的数据进行发送和接收。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 21

中科芯CKS32F4xx系列产品内部提供两个看门狗定时器单元,独立型看门狗IWDG(Independent Watchdog)和窗口型看门狗WWDG(Window Watchdog),本文主要介绍WWDG的应用,关于IWDG的详情,请参看我们的IWDG微课堂内容。

WWDG简介

通过前面的课程,我们知道IWDG独立看门狗的工作原理就是一个递减计数器在LSI时钟的驱动下不断的往下递减计数,当减到0之前如果没有刷新递减计数器的值(俗称喂狗),便会触发系统复位。

WWDG窗口看门狗也是一个递减计数器不断的往下递减计数。但和IWDG独立看门狗有四处不同:(1)IWDG由独立RC振荡器产生的LSI驱动,不受主时钟影响,但精度不高;WWDG由APB1分频时钟驱动,受主时钟影响,精度高;

(2)IWDG直接由VDD电压域供电,即使在MCU停止模式和待机模式下仍然能照常工作;

(3)IWDG计数器减到下限0时触发复位,WWDG计数器减到下限0x40时触发复位,下限值不同;(4)IWDG计数器只有复位下限值0,WWDG计数器不仅有复位下限0x40,还有一个复位上限X(用户设定),上限值与下限值之间形成一个复位窗口。WWDG计数器的值在减到上限X之前(窗口外)喂狗,同样也会产生复位。因此WWDG必须在窗口上限和窗口下限之间才可以喂狗,这是窗口看门狗的特殊之处。

1.jpg

2.jpg

WWDG详细介绍

1.WWDG功能框图解析

下图是窗口看门狗的功能框图,分4个部分进行说明:

3.jpg

① WWDG时钟:窗口看门狗的时钟来自APB1时钟线分频出的PCLK1,最大42MHz,由RCC时钟控制器控制开启;

② 计数器时钟和WWDG_CFR寄存器:CK计时器时钟 = PCLK1 / 4096,计数器时钟由CK计时器时钟经过预分频器分频得到,分频系数由配置寄存器WWDG_CFR的WWDGTB[1:0]控制,可以是0~3,因此计数器的时钟CNT_CK = PCLK1 / 4096 / (2^WWDGTB),进而可算出一个计数周期时间是T = 1 / CNT_CK;

③ 计数器和WWDG_CR寄存器:WWDG的计数器是一个7位递减计数器,其值存放在控制器寄存器WWDG_CR的T[6:0]中,当该计数器的值从0x40变为0x3F时,产生复位,因此计数器的值只能是在0x7F~0x40之间,有效位是T[5:0]。WWDG_CR的WDGA位则是窗口看门狗的激活位,可由软件置1来启动WWDG,要注意WDGA一旦置1,只能在硬件复位后才能清零;

④ 窗口上限值:WWDG的窗口下限值固定是0x40,但窗口上限值可以人为设定,具体可由WWDG_CFR配置寄存器的W[6:0]设置,其值必须大于0x40且小于0x7F,否则就失去了意义。

最后介绍的是状态寄存器(WWDG_SR)和EWI(Early wakeup interrupt)中断,该寄存器用来记录当前是否有提前唤醒EWI的中断标志。该寄存器仅有位0有效,其他都是保留位。当计数器值达到0x40时,此位由硬件置1,它必须通过软件写0来清除。在初始化WWDG时,一般会先通过软件写0清除一次。用户可以在EWI(Early wakeup interrupt)中断服务程序里进行一些必要的操作。

2.计算看门狗超时时间

窗口看门狗的时序图如下:

4.jpg

假设PCLK1时钟为42MHz,按照上述计算方法,可以得到WWDG的超时时间计算公式Twwdg =1 / PCLK1 x 4096 x 2^WDGTB x (T[5:0]+1),范围如下表:

5.jpg

3.WWDG库函数配置步骤

接下来介绍如何驱动CKS32F4xx系列产品的WWDG工作。窗口看门狗相关的库操作函数在文件cks32f4xx_wwdg.c和对应的头文件cks32f4xx_wwdg.h中。具体配置步骤如下:

(1)使能WWDG时钟

不同于IWDG,IWDG有自己独立的32KHz时钟,不需要使能。而WWDG使用的PCLK1时钟需要先进行使能。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能

(2)设置WWDG窗口上限和WDGTB分频系数

void WWDG_SetWindowValue(uint8_t WindowValue);      //设置WWDG 窗口上限
void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);    //设置WDGTB分频系数

(3)开启WWDG中断并分组

WWDG_ClearFlag(); //清除提前唤醒中断标志位
NVIC_Init();      //设置中断优先级
WWDG_EnableIT();  //开启窗口看门狗中断

(4)设置计数器初始值并使能看门狗

void WWDG_Enable(uint8_t Counter);  //设置计数器值并使能看门狗

完成以上4个步骤之后,我们就可以使用CKS32F4的窗口看门狗了。之后在程序里面就必须周期性的在窗口上下限范围内进行喂狗(一般调用WWDG_SetCounter函数),否则将导致系统复位。

4.WWDG的应用场景

WWDG一般被用来监测由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。比如一个十分关键的程序段正常运行的时间是50ms以内,在运行完这个段程序之后紧接着进行喂狗,如果在规定的时间窗口内还没有喂狗,那就说明我们监控的程序并没有在预期的50ms内运行完成,可以认为出故障了或跑飞了,那么此时产生系统复位来解除这个故障,让程序重新运行。

实验例程

为本期微课堂配套了一个例子,整体功能如下:

(1)系统上电后,LED1~LED4依次点亮后(流水动态效果)保持常亮,配置窗口看门狗,进入主程序while循环,主程序中不断去读WWDG计数器的当前值,并与WWDG窗口值进行比较,一旦发现计数器的值落入窗口范围内,就进行喂狗操作。

(2)按下USER按键,进入死循环,从而造成无法执行喂狗程序,WWDG触发系统复位,可重新看到LED1~LED4依次点亮(流水动态效果)现象。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 19

看门狗简介

中科芯CKS32F4xx系列产品内部提供两个看门狗定时器单元,独立型看门狗IWDG(Independent Watchdog)和窗口型看门狗WWDG(Window Watchdog),它们在安全性、时间精确性和使用灵活性方面变现得非常优秀。两个看门狗定时器单元都可用来检测由软件错误引起的故障,具体表现为当计数器达到给定的超时值或未能在指定时间窗口内刷新计数器的值,会触发系统复位。

IWDG由MCU内部独立RC振荡器产生的低速时钟LSI(Low-speed Internal)驱动,因此即使主时钟发生故障它也仍然有效。而WWDG是由从APB1分频后得到的时钟驱动,通过可配置的时间窗口来检测应用程序非正常的过迟或过早的操作。IWDG最适合应用于那些需要看门狗作为一个在主程序之外,能够完全独立工作,并且对时间精度要求较低的场合,比如检测由程序跑飞或死机引起的故障。WWDG最适合那些需要看门狗在精确计时窗口时间内起作用的应用程序,比如检测由外部干扰或不可预见的逻辑条件造成的应用程序背离正常运行序列而产生的软件故障。

本文主要介绍IWDG的应用,关于窗口看门狗的详情,请参看我们的WWDG微课堂内容。

IWDG详细介绍

IWDG通俗的解释它是一个12位的递减计数器,当计数器的值从某个值一直减到0的时候,就会产生一个系统复位信号,即IWDG_RESET。如果在计数器没减到0之前,“刷新”计数器的值,就不会产生复位信号,“刷新”这个动作就是我们经常说的喂狗。IWDG直接由VDD电压域供电,即使在MCU停止模式和待机模式下仍然能照常工作。 

1.IWDG功能框图解析

下图是独立看门狗的功能框图,分6个部分进行说明:

1.png

① LSI时钟:IWDG的时钟由专门的32KHz低速时钟LSI驱动,即使主时钟发生故障它也仍然有效,非常独立。这里需要注意的是,由于RC振荡器的原理和特性(根据温度和环境会有一定的漂移),IWDG并不是严格准确的32KHz,只是我们在应用的时候,默认以32KHz的频率来估算。所以IWDG的定时时间并不一定非常精确,只适用于对时间精度要求比较低的场合。

② 计数器时钟和IWDG_PR寄存器:递减计数器的时钟由LSI经过一个8位的预分频器得到,预分频器寄存器IWDG_PR的值决定分频因子,分频因子可以是:4、8、16、32、64、128、256。分频因子(假设为W)和IWDG_PR值的关系是W = 4 * 2^IWDG_PR。

③ 状态寄存器IWDG_SR:顾名思义,IWDG_SR表示独立看门狗模块的当前状态,该寄存器只有位0:PVU(Prescaler Value Update)和位1:RVU(Reload Value Update)有效,且只能读不能写。PVU置1指示预分频值的更新正在进行中,更新完成后由硬件置0。RVU置1表示重装载值的更新正在进行中,更新完毕之后由硬件置0。只有当RVU或PVU等于0的时候才可以进行下一次更新操作。

④ 重载寄存器IWDG_RLR:重载寄存器是一个12位的寄存器,里面装着要刷新到计数器的值,这个值的大小决定着独立看门狗的溢出时间。溢出时间Tout(s) = (4 * 2^IWDG_PR) / 32KHz * IWDG_RLR,根据这个公式,可以计算出当LSI为32KHz时,IWDG的理论溢出时间最小值和最大值分别是125us和32.768s。

⑤ 递减计数器:IWDG的递减计数器是一个12位寄存器,设置范围是0~4095,一个计数器时钟计数器就减1,当计数器减到0时,IWDG会产生一个系统复位信号IWDG_RESET,让程序重新启动运行,如果在计数器减到0之前刷新计数器的值(重新写入新值),就不会产生复位信号,重新刷新计数器值的这个动作俗称喂狗。

⑥ 密钥寄存器IWDG_KR:密钥寄存器IWDG_KR是独立看门狗IWDG的一个核心控制寄存器,主要有三种寄存器值对应三种控制效果。

2.IWDG库函数配置步骤

我们接下来介绍如何驱动CKS32F4xx系列产品的IWDG工作。独立看门狗相关的库操作函数在文件cks32f4xx_iwdg.c和对应的头文件cks32f4xx_iwdg.h中。具体配置步骤如下:

(1)解除寄存器写保护(向IWDG_KR写入0x5555)

IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  //使能写权限

(2)设置IWDG预分频因子和重装载值

void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); //设置IWDG预分频值void IWDG_SetReload(uint16_t Reload); //设置IWDG重装载值

此时可以计算出看门狗溢出时间,比如我们最终设定IWDG_PR值为 4,IWDG_RLR值500,那么就可以得到 Tout = (4 * 2^IWDG_PR) / 32KHz * IWDG_RLR = 64 / 32 * 500 = 1000ms,看门狗的溢出时间是1s,只要在一秒钟之内,写入0xAAAA到IWDG_KR,就不会触发看门狗复位(一秒内写入多次也是可以的)。这里需要提醒大家的是,由于看门狗的时钟不是准确的32KHz,所以喂狗时间应适当提前。

(3)重载计数值喂狗(向IWDG_KR写入0xAAAA) 

IWDG_ReloadCounter();  //把重装载寄存器IWDG_RLR的值放到计数器中

(4)开启看门狗(向IWDG_KR写入0xCCCC)

IWDG_Enable();  //使能 IWDG

通过上面4个步骤,就可以启动CKS32F4的IWDG独立看门狗了,之后在程序里面就必须周期性的进行喂狗(一般会使用定时器定时的调用IWDG_ReloadCounter函数),否则将导致系统复位。注意IWDG在一旦开启,系统运行时就不能再被关闭,想要关闭,只能重启,并且重启之后要迅速关闭IWDG。

实验例程

为本期微课堂配套了一个例子,整体功能如下:

(1)系统上电后,LED1~LED4依次点亮后熄灭(流水动态效果),配置看门狗,进入主程序while循环,LED1~LED4周期性闪烁,主程序中设置周期性喂狗操作。

(2)按下USER按键,LED1~LED4保持点亮不熄灭,停止周期性喂狗操作,从而造成无法执行喂狗程序,IWDG触发系统复位,可重新看到LED1~LED4依次点亮后熄灭(流水动态效果)现象。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 14

CKS32F4xx系列产品提供了可编程电压检测器PVD,用于对MCU供电电压VDD进行监控,当检测到电压低于或者高于PVD设置的阈值时,会向内核产生一个PVD中断(EXTI线中断)以使内核在复位前进行紧急处理。该电压阈值可通过PWR_CSR进行设置。只要电压超过芯片最低运行电压,芯片就能正常工作,而不是低于阀值就不能工作。在实际运用中,如果是用电池给芯片供电,当电池电量不足时,电池的供电电压就会下降,下降到低于阀值时就会触发PVD中断,此功能可以通知用户,使系统进入特别保护状态,执行紧急关闭任务程序进行相关处理,比如进行数据备份处理或者进行低电量提醒等操作。

PVD可配置8个等级,通过电源控制寄存器中的PLS[2:0]位可以用来设定检测电压的阀值,详见下表。

1.jpg

PVD模块操作流程

一、 系统启动后启动PVD,并开启相应的中断:

首先通过固件库函数RCC_APB1PeriphClockCmd配置PWR模块时钟,然后配置检测电压的阀值,根据表1设置参数。PVD中断连接EXTI_Line16中断线,配置中断顺序依次为:中断线、中断模式、触发方式及中断线使能。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR时钟

/* 设定检测阀值 */
PWR_PVDLevelConfig(PWR_PVDLevel_5);  
  
EXTI_StructInit(&EXTI_InitStructure); 
/*  PVD连接到中断线16上 */
EXTI_InitStructure.EXTI_Line = EXTI_Line16;      
/* 使用中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 电压低于阀值时产生中断 */     
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Raising;  
/* 使能中断线  */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;   
/* 初始化中断  */

EXTI_InitStructure.EXTI_Trigger配置定义:

EXTI_Trigger_Rising---表示电压从高下降到低于设定阀值时产生中断。

EXTI_Trigger_Falling---表示电压从低上升到高于设定阀值时产生中断。

EXTI_Trigger_Rising_Falling---表示电压上升或下降越过设定阀值时都产生中断。

二、配置PVD中断优先级:

void NVIC_Configuration(void)
{  
   NVIC_InitTypeDef NVIC_InitStructure;  
   /* 嵌套向量中断控制器组选择 */  
   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);  
   /* 配置USART为中断源 */  
   NVIC_InitStructure.NVIC_IRQChannel = PVD_IRQn;  
   /* 抢断优先级为1 */  
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  
   /* 子优先级为1 */  
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;  
   /* 使能中断 */  
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
   /* 初始化配置NVIC */  
   NVIC_Init(&NVIC_InitStructure);
}

三、使能PVD模块:

PWR_PVDCmd(ENABLE);             // 使能PVD

四、产生PVD中断,在中断程序中进行相应的处理:

void PVD_IRQHandler(void) 
{     
    EXTI_ClearITPendingBit(EXTI_Line16);     
    …… // 用户添加紧急处理代码处
}

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 31

本课将为大家讲解CKS32F4xx系列产品的中断优先级管理单元NVIC。CM4内核共支持256个中断,其中包含了16个内核中断和240个外部中断,具有256级可编程中断设置。但CKS32F4xx系列只使用了CM4内核的一部分,共有98个中断,包括16个带有FPU核的CM4中断和82个可屏蔽中断,而我们常用的就是这82个可屏蔽中断。

NVIC寄存器简介

MDK为NVIC相关寄存器其定义了如下的结构体:

typedef struct 
{ 
    __IO uint32_t ISER[8]; /*!< Interrupt Set Enable Register */ 
    uint32_t RESERVED0[24]; 
    __IO uint32_t ICER[8]; 
    /*!< Interrupt Clear Enable Register */ 
    uint32_t RSERVED1[24]; 
    __IO uint32_t ISPR[8]; 
    /*!< Interrupt Set Pending Register */ 
    uint32_t RESERVED2[24]; 
    __IO uint32_t ICPR[8]; 
    /*!< Interrupt Clear Pending Register */ 
    uint32_t RESERVED3[24]; 
    __IO uint32_t IABR[8]; 
    /*!< Interrupt Active bit Register */ 
    uint32_t RESERVED4[56]; 
    __IO uint8_t IP[240]; 
    /*!< Interrupt Priority Register, 8Bit wide */ 
    uint32_t RESERVED5[644]; 
    __O uint32_t STIR; 
    /*!< Software Trigger Interrupt Register */ 
} NVIC_Type;

CKS32F4xx系列的中断在这些寄存器控制下有序执行,只有了解这些中断寄存器后,才能方便的使用CKS32F4xx系列中断功能。下面重点介绍这几个寄存器:

ISER[8]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面说了CM4内核支持256个中断,这里用8个32位寄存器来控制,每个位控制一个中断。但是CKS32F4xx系列的可屏蔽中断最多只有82个,所以对我们来说,有用的就是三个(ISER[0~2]),ISER[0]的bit 0~31分别对应中断0~31;ISER[1]的bit 0~31对应中断32~63;ISER[2]的bit 0~17对应中断64~81,共82个中断。你要使能某个中断,必须设置相应的ISER位为 1,使该中断被使能,另外还要配合中断分组、屏蔽、IO口映射等设置才算是一个完整的中断设置。具体每一位对应哪个中断,请参考CKS32f4xx.h。

ICER[8]:全称是:Interrupt Clear-Enable Registers,是一个中断除能寄存器组。该寄存器组与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄存器都是写 1 有效的,写 0 是无效的。

ISPR[8]:全称是:Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位对应的中断和ISER是一样的。通过置1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写0是无效的。

ICPR[8]:全称是:Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作用与ISPR相反,对应位也和 ISER是一样的。通过设置1,可以将挂起的中断接挂。写 0 无效。

IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和ISER一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。

IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。CKS32F4的中断分组与这个寄存器组密切相关。IP寄存器组由240个8bit的寄存器组成,每个可屏蔽中断占用8bit,这样总共可以表示240个可屏蔽中断。而CKS32F4只用到了其中的82个。IP[81]~IP[0]分别对应中断 81~0。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是只用了高4位。这4位,又分为抢占优先级和响应优先级。抢占优先级在前,响应优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。

NVIC中断分组

这里简单介绍一下 CKS32F4 的中断分组:CKS32F4将中断分为5个组,组0~4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如下:

1.jpg

通过这个表,我们就可以清楚的看到组0~4对应的配置关系,例如组设置为3,那么此时所有的82个中断,每个中断的中断优先寄存器的高四位中的最高3位是抢占优先级,低1位是响应优先级。每个中断,你可以设置抢占优先级为0~7,响应优先级为1或0。抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。

这里需要注意两点:

第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;

第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。

结合实例说明一下:假定设置中断优先级组为2,然后设置中断3(RTC_WKUP中断)的抢占优先级为2,响应优先级为1。中断6(外部中断0)的抢占优先级为3,响应优先级为0。中断7(外部中断1)的抢占优先级为2,响应优先级为0。那么这3个中断的优先级顺序为:中断7>中断3>中断6。其中中断3和中断7都可以打断中断6的中断。而中断7和中断3却不可以相互打断!

软件实现

通过以上介绍,我们熟悉了CKS32F4中断设置的大致过程。接下来我们介绍如何使用库函数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化。NVIC中断管理函数主要在misc.c文件里面。

首先要讲解的是中断优先级分组函数NVIC_PriorityGroupConfig,其函数申明如下:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分组确定就最好不要更改。这个函数我们可以找到其实现:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) 
{ 
  assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup)); 
  SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; 
}

从函数体可以看出,这个函数唯一目的就是通过设置SCB->AIRCR寄存器来设置中断优先级分组,查看其定义为:

#define IS_NVIC_PRIORITY_GROUP(GROUP) 
(((GROUP) == NVIC_PriorityGroup_0) || 
((GROUP) == NVIC_PriorityGroup_1) || \ 
((GROUP) == NVIC_PriorityGroup_2) || \ 
((GROUP) == NVIC_PriorityGroup_3) || \ 
((GROUP) == NVIC_PriorityGroup_4))

可以看到这个定义对应上表中的分组范围0-4。如果我们需要设置系统的中断优先级分组值为2,那么方法是:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);这样就确定了一共为“2位抢占优先级,2位响应优先级”。设置好了系统中断分组,接下来设置中断的抢占优先级和响应优先级,这里需要用到一个重要的函数为中断初始化函数NVIC_Init,其函数声明为:voidNVIC_Init(NVIC_InitTypeDef*NVIC_InitStruct),其中NVIC_InitTypeDef是一个结构体,我们可以看看结构体的成员变量:

typedef struct 
{ 
 uint8_t NVIC_IRQChannel; 
 uint8_t NVIC_IRQChannelPreemptionPriority; 
 uint8_t NVIC_IRQChannelSubPriority; 
 FunctionalState NVIC_IRQChannelCmd; 
} NVIC_InitTypeDef;

NVIC_InitTypeDef结构体中间有四个成员变量,这四个成员变量的作用是:

NVIC_IRQChannel:

定义初始化的是哪个中断,这个我们可以在CKS32f4xx.h中定义的枚举类型IRQn的成员变量中可以找到每个中断对应的名字。例如串口1对应USART1_IRQn。

NVIC_IRQChannelPreemptionPriority:

定义这个中断的抢占优先级别;

NVIC_IRQChannelSubPriority:

定义这个中断的响应优先级别。

NVIC_IRQChannelCmd:

该中断通道是否使能。比如我们要使能串口1的中断,同时设置抢占优先级为1,响应优先级位2,初始化的方法是:

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//>>串口 1 中断 
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;//>> 抢占优先级为 1 
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;//>> 响应优先级位 2 
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //>>IRQ 通道使能 
NVIC_Init(&NVIC_InitStructure);

这里我们讲解了中断的分组的概念以及设定优先级值的方法,至于每种优先级还有一些关于清除中断,查看中断状态,这在后面课堂中我们讲解每个中断的时候会详细讲解到。

课程总结

中断优先级设置的步骤:

1.系统初始化的时候设置中断分组:

确定组号,也就是确定抢占优先级和响应优先级的分配位数。调用函数为NVIC_PriorityGroupConfig();

2.设置所用到的中断的中断优先级别:

对每个中断调用函数NVIC_Init();确定具体的抢占优先级和响应优先级,并使能通道。

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 38

GPIO(General-purpose input/output)是通用输入输出端口的简称,CKS32F4xx系列产品通过GPIO引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。最基本的输出功能是由CKS32F4xx系列产品控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED灯,那就可以控制LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。最基本的输入功能是检测外部输入电平,如把 GPIO引脚连接到按键,通过电平高低区分按键是否被按下。

GPIO硬件结构框图

1.png

该图从最右端看起,最右端标注着“I/O”的就是代表CKS32F4xx系列产品引出的GPIO引脚,其余部件都位于芯片内部。引脚处的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD_FT时,上方的二极管导通,当引脚电压低于VSS时,下方的二极管导通,由此可以防止不正常电压引入芯片导致芯片的烧毁。这里要特别注意VDD_FT 代表IO口兼容3.3V和5V,如果没有标注“FT”,就代表着不兼容5V。在芯片数据手册的引脚定义中,会看到有“电平I/O”一列,有FT标注的即为支持5V,如下图所示:

2.png

标号1处是上拉、下拉电阻,从它的结构我们可以看出,通过上、下拉对应的开关配置,我们可以控制引脚默认状态下的电压,开启上拉的时候引脚电压为高电平,开启下拉的时候引脚电压为低电平。同时也可以设置“既不上拉也不下拉模式”,我们也把这种状态称为浮空模式。

标号2处是一个由P-MOS和N-MOS管组成的单元电路。这个结构使GPIO具有了“推挽输出”和“开漏输出”两种模式。上方的P-MOS管高电平导通,低电平关闭,下方的N-MOS低电平导通,高电平关闭。

标号3处是输出数据寄存器,它为标号2处的双MOS管结构电路提供输入控制信号,因此通过修改输出数据寄存器的值就可以修改GPIO引脚的输出电平。而图中“置位/复位寄存器 GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响电路的输出。

标号4处是复用功能输出,“复用”是指CKS32F4xx系列产品的其它片上外设可以对GPIO引脚进行控制,此时GPIO引脚用作该外设功能的一部分,算是第二用途。例如我们使用USART串口通讯时,需要用到某个GPIO引脚作为通讯发送引脚,这个时候就可以把该GPIO引脚配置成USART串口复用功能,由串口外设控制该引脚,发送数据。

标号5处是输入数据寄存器,它连接到图中的TTL施密特触发器,触发器的基本原理是当输入电压高于正向阈值电压时,输出为高;当输入电压低于负向阈值电压时,输出为低;IO口信号经过触发器后,模拟信号转化为0和1的数字信号,也就是高低电平,并且是TTL电平协议, 然后存储在“输入数据寄存器。因此,通过读取该寄存器就可以了解GPIO引脚的电平状态。

标号6处是复用功能输入,与“复用功能输出”模式类似,同样,如果我们使用USART串口通讯时,需要用到某个GPIO引脚作为通讯接收引脚,这个时候就可以把该GPIO引脚配置成USART串口复用功能,使USART可以通过该通讯引脚接收远端数据。

标号7处是模拟输入输出,当 GPIO引脚用于ADC采集电压的输入通道时,用作“模拟输入”功能,此时信号是不经过施密特触发器的,因为经过施密特触发器后信号只有0、1 两种状态,所以ADC外设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。同样的,当 GPIO引脚用于DAC作为模拟电压输出通道时,此时作为“模拟输出”功能,DAC 的模拟信号输出就不经过双MOS管结构了,在GPIO结构框图的右下角处,模拟信号直接输出到引脚。

GPIO的工作模式

- 4种输入模式 -

1.浮空输入

浮空输入状态下,IO的电平状态是不确定的,完全由外部输入决定。如果在该引脚悬空的情况下,读取该端口的电平是不确定的。

2.上拉输入

在该模式下,如果IO口外部没有信号输入或者引脚悬空,IO口默认为高电平。如果I/O口输入低电平,那么引脚就为低电平,MCU读取到的就是低电平。

3.下拉输入

在该模式下如果IO口外部没有信号输入或者引脚悬空,IO口默认为低电平,如果I/O口输入高电平,那么引脚就为高电平,MCU读取到的就是高电平。

4.模拟功能

当GPIO用于模拟功能时,引脚的上、下拉电阻是不起作用的,这个时候即使配置了上拉或下拉模式,也不会影响到模拟信号的输入输出。除了ADC和DAC要将IO配置为模拟功能模式之外,其他外设功能一律要配置为复用功能模式。

- 4种输出模式 -

1.开漏输出(带上拉或者下拉)

在该模式下,若MCU控制输出为高电平1时,输出指令是不会起到作用的。此时I/O端口的电平就不会由输出的高电平决定,而是由I/O端口外部的上拉或者下拉决定,如果没有上拉或者下拉,IO口就处于高阻态。虽然通过软件设置内部上拉,也可以输出高电平,但是CKS32F4xx系列产品内部上拉是"弱上拉",即通过此上拉输出的电流是很弱的,驱动能力很弱。但是在该模式下,当MCU控制输出为低电平0时,即使没有上拉或者下拉,I/O端口也会输出低电平。另一方面,在开漏模式下,施密特触发器是打开的,即输入可用,可以通过输入数据寄存器GPIOx_IDR读取I/O的实际状态。开漏输出主要有以下两点作用:

a. I/O端口设置成开漏输出模式时,可以用来连接不同电平的器件,用来匹配电平,因为开漏引脚不连接外部的上拉电阻时,只能输出低电平,如果需要同时具备输出高电平的功能,则需要接上拉电阻。因此我们通过改变上拉电源的电压,便可以改变传输电平。比如通过加上上拉电阻就可以提供TTL电平-CMOS电平的输出。

b.当多个设置为开漏输出的引脚连接到一条总线上时。通过外加一个上拉电阻,在不增加任何器件的情况下,这些引脚形成了“与逻辑”关系,即“线与”。如果有一个引脚输出为逻辑0,相当于接地,那么与之并联的回路“相当于被一根导线短路”,所以总线上的逻辑电平便为0。只有都为高电平时,总线上的电平才为1。在IIC通信中,引脚通常设置为开漏输出模式。

2.复用开漏输出(带上拉或者下拉)

此时GPIO复用为其他外设,输出数据寄存器GPIOx_ODR无效;即输出的高低电平来源于其它外设,除了输出信号的来源改变之外,其他的与开漏输出功能相同。

3.推挽输出(带上拉或者下拉)

在该模式下,如果我们控制输出为0,低电平,则I/O端口的电平就是低电平。若控制输出为1,高电平,则I/O端口的电平就是高电平。此时,外部上拉和下拉的作用是控制在没有输出IO口的默认电平。在该模式下,施密特触发器也是打开的,即输入可用,通过输入数据寄存器GPIOx_IDR可读取I/O的实际状态。

4.复用推挽输出(带上拉或者下拉)

此时GPIO复用为其他外设,输出数据寄存器GPIOx_ODR无效;即输出的高低电平来源于其它外设,除了输出信号的来源改变之外,其他的与推挽输出功能相同。

- 4种输出速度 -


1. 2MHZ(低速)

2. 25MHZ(中速)

3. 50MHZ(快速)

4. 100MHZ(高速)

GPIO的引脚速度又称输出驱动电路的响应速度,即一个驱动电路可以不失真地通过信号的最大频率。比如信号频率为10MHz,而我们把GPIO速度配置成了2MHz,则10MHz的方波很可能就变成了正弦波,发生了失真。芯片内部在I/O口的输出部分安排了多个响应速度不同的输出驱动电路,我们可以根据自己的需要选择合适的驱动电路,通过选择速度来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。因为GPIO口的速度配置越高,噪声越大,功耗也越大。比如在USART串口通信中,若最大波特率只需115.2k,那用2M的速度就够了,既省电,噪声也小;而在SPI接口中,若使用18M或9M的波特率,则需要选用50M的GPIO的引脚速度。

注意:CKS32F407用于配置PA13输出速度的GPIOA_OSPEEDR寄存器的端口 13配置位(GPIOA_OSPEEDR_OSPEEDR13[1:0])初始值为00,即PA13的I/O输出速度默认为低速。因此客户在实际使用CKS32F407时,要按照参考手册中的介绍来配置GPIOA_OSPEED寄存器中 OSPEEDR13[1:0]位,从而来选择PA13的I/O输出指定的速度。

GPIO的配置

接下来我们讲解如何利用CKS32F4xx系列固件库对GPIO口的工作模式进行配置。首先,固件库中定义了一个如下的结构体:

typedef struct
{  
  uint32_t GPIO_Pin;               
  GPIOMode_TypeDef GPIO_Mode;      
  GPIOSpeed_TypeDef GPIO_Speed;    
  GPIOOType_TypeDef GPIO_OType;    
  GPIOPuPd_TypeDef GPIO_PuPd;    
} GPIO_InitTypeDef;

通过对该结构体成员中各个变量的初始化,就可以完成对2.2小节中所讲的GPIO口的工作模式配置。结构体中各个成员变量的介绍及初始化时可被赋的值如下:

1)GPIO_Pin:用来选择要控制的GPIO引脚,在标准库函数中可选择的值及其定义如下:

#define GPIO_Pin_0                 ((uint16_t)0x0001) 
#define GPIO_Pin_1                 ((uint16_t)0x0002) 
#define GPIO_Pin_2                 ((uint16_t)0x0004)  
#define GPIO_Pin_3                 ((uint16_t)0x0008) 
#define GPIO_Pin_4                 ((uint16_t)0x0010)  
#define GPIO_Pin_5                 ((uint16_t)0x0020)  
#define GPIO_Pin_6                 ((uint16_t)0x0040) 
#define GPIO_Pin_7                 ((uint16_t)0x0080)  
#define GPIO_Pin_8                 ((uint16_t)0x0100) 
#define GPIO_Pin_9                 ((uint16_t)0x0200)  
#define GPIO_Pin_10                ((uint16_t)0x0400) 
#define GPIO_Pin_11                ((uint16_t)0x0800) 
#define GPIO_Pin_12                ((uint16_t)0x1000) 
#define GPIO_Pin_13                ((uint16_t)0x2000) 
#define GPIO_Pin_14                ((uint16_t)0x4000)  
#define GPIO_Pin_15                ((uint16_t)0x8000) 
#define GPIO_Pin_All               ((uint16_t)0xFFFF)

2) GPIO_Mode:用来设置已经选择的GPIO引脚的模式,在标准库函数中可选择的值及其定义如下:

typedef enum
{   
   GPIO_Mode_IN   = 0x00, /*!设置为输入模式 */  
   GPIO_Mode_OUT  = 0x01, /*!设置为输出模式*/  
   GPIO_Mode_AF   = 0x02, /*!设置为复用模式 */  
   GPIO_Mode_AN   = 0x03  /*!设置为模拟模式*/
}GPIOMode_TypeDef;

3) GPIO_Speed:用来设置已经选择的GPIO引脚的速度,在标准库函数中可选择的值及其定义如下:

#define  GPIO_Speed_2MHz    GPIO_Low_Speed    
#define  GPIO_Speed_25MHz   GPIO_Medium_Speed 
#define  GPIO_Speed_50MHz   GPIO_Fast_Speed 
#define  GPIO_Speed_100MHz  GPIO_High_Speed

4) GPIO_OType:用来设置已经选择的GPIO引脚的输出模式,只有输出模式才需要该配置,输入模式下不需要该配置。在标准库函数中可选择的值及其定义如下:

typedef enum
{   
   GPIO_OType_PP = 0x00, /*!设置为推挽输出模式 */  
   GPIO_OType_OD = 0x01  /*!设置为开漏输出模式 */
}GPIOOType_TypeDef;

5) GPIO_PuPd:用来设置已经选择的GPIO引脚的上下拉,在标准库函数中可选择的值及其定义如下:

typedef enum
{   
   GPIO_PuPd_NOPULL = 0x00, /*!设置为既不上拉也不下拉/浮空模式 */  
   GPIO_PuPd_UP     = 0x01, /*!设置为上拉模式*/  
   GPIO_PuPd_DOWN   = 0x02  /*!设置为下拉模式*/
}GPIOPuPd_TypeDef;

根据上面所讲解的配置方法,我们讲解标准库下的3个实际配置实例。

1) 作为普通的GPIO口输出,控制LED灯的亮灭,其GPIO口初始化函数如下:

void LED_GPIO_Init(void)
{         
   GPIO_InitTypeDef  GPIO_InitStructure;  
   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);//使能GPIOF时钟  
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;//LED1对应的IO口  
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//输出模式  
   GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽模式  
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//速度100MHz  
   GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉  
   GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化GPIO
}

2) 复用为CAN外设的输出。

void CAN1_GPIO_Init(void)
{    
   GPIO_InitTypeDef         
   GPIO_InitStructure;   
   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//使能PORTA时钟                                          
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11| GPIO_Pin_12;    
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;  //复用功能    
   GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出    
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz    
   GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;  //上拉    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化PA11,PA12   
   GPIO_PinAFConfig(GPIOA,GPIO_PinSource11,GPIO_AF_CAN1); //GPIOA11复用为CAN1   
   GPIO_PinAFConfig(GPIOA,GPIO_PinSource12,GPIO_AF_CAN1); //GPIOA12复用为CAN1

3) 当ADC采集的输入通道,作为普通模拟输入。

void  ADC_Init(void)
{      
    GPIO_InitTypeDef  GPIO_InitStructure;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//使能GPIOA时钟  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//PA5 通道5  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;//模拟输入  
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;//不带上下拉  
    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化
 }

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 111

作为MCU运行的基础,时钟是单片机各个模块工作时序的最小时间单位,推动单片机的各指令执行,是MCU选型的一个重要指标。CKS32F4xx系列产品具有众多的外设,但并非所有的外设均需要系统时钟的高频率,并且高时钟频率将导致功耗增加、抗电磁干扰能力变弱,因此,CKS32F4xx系列产品内部具备多个时钟源。本文将对CKS32F4xx系列产品时钟组成进行分析,并讲解该系统单片机的时钟的配置方法,以能够让用户更加简单的对系统时钟进行配置。

CKS32F4xx系列产品时钟树

在CKS32F4xx系列产品中,有HSI、HSE、LSI、LSE、PLL五个重要的时钟源,其中PLL分为主PLL和专用PLL两部分。从时钟频率来分可以分为高速时钟源(HIS、HIS、PLL)和低速时钟源(LSI、LSE);从来源可分为外部时钟源(HSE、LSE)和内部时钟源(HIS、LSI、PLL)。

1.jpg

①:LSI是低速内部RC振荡器,频率为32kHz。供独立看门狗和RTC单元使用。
②:LSE是低速外部时钟,接频率为32.768kHz晶振。主要供RTC单元使用。
③:HSE是高速外部时钟晶振输入,频率范围为4MHz~26MHz。。
④:HSI是高速内部RC振荡器,频率为16MHz。可以直接作为系统时钟或者用作PLL输入。
⑤:PLL为锁相环倍频输出,有两个PLL:
    1)主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号,并具有两个不同的输出时钟。
     其一PLLP用于生成高速的系统时钟(最高 168MHz)
    其二PLLQ用于生成 USB_OTG_FS(48MHz)、随机数发生器SDIO时钟。
    2)专用 PLL(PLLI2S)用于生成精确时钟,用于实现I2S高品质音频性能。

CKS32F4xx系列系统时钟配置

在CKS32F4xx系列固件库system_cks32f4xx.c文件中定义了函数SystemInit(void),并在其中调用了SetSysClock()函数来配置系统关键时钟寄存器,其处理流程如下:

先使能外部时钟HSE,等待HSE稳定之后,配置AHB、APB1、APB2时钟相关的分频因子;等待这些都配置完成之后,打开主PLL时钟并设置主PLL作为系统SYSCLK时钟源。如果HSE不能达到就绪状态则依然以HSI作为系统时钟源头。

在设置主PLL时钟时,需要设置一系列的分频系数和倍频参数,代码如下:

RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |                        (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);

 PLL_M、PLL_N、PLL_P宏定义均在在System_cks32f4xx.c文件中定义,当采用8MHz外部晶振时,主PLL时钟计算方法如下:

PLL = 8MHz * PLL_N / (PLL_M * PLL_P) = 8MHz * 336 /(8 * 2) = 168MHz

用户可根据实际需求,根据SetSysClock函数内的注释进行实际修改,可用的时钟源配置宏定义位于cks32f4xx.h中,如RCC_CR_HSION、RCC_CR_HSEON等。

CKS32F4xx系列外设时钟配置

在系统初始化之后,在使用部分外设时,我们还需要根据外设需求修改某些时钟源配置。在CKS32F4xx系列固件库中,时钟源的选择以及时钟使能等函数均在RCC相关固件库文件 cks32f4xx_rcc.h 和 cks32f

void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState);
void RCC_AHB2PeriphClockCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState);
void RCC_AHB3PeriphClockCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

以上5个系统时钟使能函数分别控制AHB1、AHB2、AHB3、APB1、APB2总线。要使能某个外设,调用对应的总线外设时钟使能函数即可。

例如,如果我们要使能GPIOA,那么我们可以在头文件 cks32f4xx_rcc.h 里面查看到宏定义标识符RCC_AHB1Periph_GPIOA挂载在AHB1总线之下,因此我们调用方式入如下:‍

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);

 同理,如果我们要使能USART1的时钟,那么我们调用的函数为:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

还有一类时钟使能函数是时钟源使能函数,前面我们已经讲解过CKS32F4xx系列有5类时钟源。这里我们列出来几种重要的时钟源使能函数:

void RCC_HSICmd(FunctionalState NewState);
void RCC_LSICmd(FunctionalState NewState);
void RCC_PLLCmd(FunctionalState NewState);
void RCC_PLLI2SCmd(FunctionalState NewState);
void RCC_PLLSAICmd(FunctionalStateNewState);
void RCC_RTCCLKCmd(FunctionalState NewState);

具体调用方法如下:

RCC_PLLCmd(ENABLE);

 第二类时钟功能函数:时钟源选择和分频因子配置函数用来选择相应的时钟源以及配置相应的时钟分频系数,比如配置HSI、HSE、PLL三个中的一个时钟源为系统时钟。以下为几种时钟源配置函数:

void RCC_LSEConfig(uint8_t RCC_LSE);
void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource);
void RCC_HCLKConfig(uint32_t RCC_SYSCLK);
void RCC_PCLK1Config(uint32_t RCC_HCLK);
void RCC_PCLK2Config(uint32_t RCC_HCLK);
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t PLLM,uint32_t PLLN, int32_t PLLP, uint32_t PLLQ);

比如我们要设置系统时钟源为 HSI,则可以调用系统时钟源配置函数:

RCC_HCLKConfig(RCC_SYSCLKSource_HSI);

第三类外设复位函数如下:

void RCC_AHB1PeriphResetCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState);void RCC_AHB2PeriphResetCmd(uint32_t RCC_AHB2Periph, FunctionalState NewState);void RCC_AHB3PeriphResetCmd(uint32_t RCC_AHB3Periph, FunctionalState NewState);void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

这类函数和前面讲解的外设时钟函数使用方法基本一致,不同的是一个是用来使能外设时钟,一个是用来复位对应的外设。对于这些时钟操作函数,我们就不一一列举出来,大家可以打开 RCC 对应的文件仔细了解。有问题欢迎邮件咨询:wangb@cksmcu.com.cn

来源:中科芯MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 43

页面

订阅 RSS - CKS32F4xx