CKS32F4xx

在上一讲单通道ADC电压采集的基础上,本节主要介绍CKS32F4xx系列产品基于DMA传输的ADC多通道电压采集转换实现。

DMA传输在ADC中的应用

DMA是直接存储器存取,通常在使用ADC时,需要通过MCU内核不停的读取数据,如果使用DMA,那么读取的过程会绕过MCU,减轻MCU内核的处理压力,这样有利于资源的充分利用,提高ADC数据的处理效率。由于ADC规则通道组只有一个数据寄存器中,当转换多个通道时,使用DMA还可以避免丢失已经存储在ADC_DR寄存器中的数据。在使能DMA模式的情况下,每完成规则通道组中的一个通道转换后,都会生成一个DMA请求,便可将转换的数据从ADC_DR寄存器传输到用指定的目标内存位置。这样取代单通道实验使用中断服务的读取方法,可以实现多通道ADC应用中高速高效的采集。

软件设计要点

跟单通道例程一样,编写两个ADC驱动文件,bsp_adc.h和bsp_adc.c,用来存放ADC所用IO引脚的初始化函数以及ADC和DMA相关配置函数,主要流程为:

(1)初始化配置ADC目标引脚为模拟输入模式;

(2)使能ADC时钟和DMA时钟;

(3)配置DMA从ADC数据寄存器传输数据到指定的存储区;

(4)配置通用ADC为独立模式;

(5)设置ADC为12位分辨率,启动扫描,连续转换,不需要外部触发;

(6)设置ADC转换通道顺序及采样时间;

(7)使能DMA请求,DMA在AD转换完自动传输数据到指定的存储区;

(8)启动ADC模块;

(9)软件使能触发ADC转换。

这里需要注意的是,在使用ADC+DMA功能时,如果在启动ADC转换之后使能DMA,ADC采样数据可能会出现异常。因此建议先配置ADC及DMA相关参数,最后启动ADC转换。

代码实现

受篇幅限制,这里只介绍核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本课程配套的例程。相关核心代码实现如下:

(1)ADC宏定义

#define TEMP_NOFCHANEL      3
/*=====================通道1 IO======================*/
// PB0 ADC IO宏定义,可用杜邦线接 3V3 或者 GND 来实验
#define TEMP_ADC_GPIO_PORT1    GPIOB
#define TEMP_ADC_GPIO_PIN1     GPIO_Pin_0
#define TEMP_ADC_GPIO_CLK1     RCC_AHB1Periph_GPIOB
#define TEMP_ADC_CHANNEL1      ADC_Channel_8
/*=====================通道2 IO ======================*/
// PB1 ADC IO宏定义,可用杜邦线接 3V3 或者 GND 来实验
#define TEMP_ADC_GPIO_PORT2    GPIOB
#define TEMP_ADC_GPIO_PIN2     GPIO_Pin_1
#define TEMP_ADC_GPIO_CLK2     RCC_AHB1Periph_GPIOB
#define TEMP_ADC_CHANNEL2      ADC_Channel_9
/*=====================通道3 IO ======================*/
// PA6 ADC IO宏定义,可用杜邦线接 3V3 或者 GND 来实验
#define TEMP_ADC_GPIO_PORT3    GPIOA
#define TEMP_ADC_GPIO_PIN3     GPIO_Pin_6
#define TEMP_ADC_GPIO_CLK3     RCC_AHB1Periph_GPIOA
#define TEMP_ADC_CHANNEL3     ADC_Channel_6
// ADC 序号宏定义
#define TEMP_ADC              ADC1
#define TEMP_ADC_CLK          RCC_APB2Periph_ADC1
// ADC DR寄存器宏定义,ADC转换后的数字值则存放在这里
#define TEMP_ADC_DR_ADDR    ((u32)ADC1+0x4c)
// ADC DMA 通道宏定义,使用DMA传输
#define TEMP_ADC_DMA_CLK      RCC_AHB1Periph_DMA2
#define TEMP_ADC_DMA_CHANNEL  DMA_Channel_0
#define TEMP_ADC_DMA_STREAM   DMA2_Stream0

定义多个通道进行多通道ADC实验,并且定义DMA相关配置。

(2)ADC GPIO初始化

static void Temp_ADC_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    /*=====================通道1======================*/
    RCC_AHB1PeriphClockCmd(TEMP_ADC_GPIO_CLK1,ENABLE); // 使能 GPIO 时钟
    GPIO_InitStructure.GPIO_Pin = TEMP_ADC_GPIO_PIN1; // 配置 IO
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;     //不上拉不下拉
    GPIO_Init(TEMP_ADC_GPIO_PORT1, &GPIO_InitStructure);
    /*=====================通道2======================*/
    RCC_AHB1PeriphClockCmd(TEMP_ADC_GPIO_CLK2,ENABLE); // 使能 GPIO 时钟
    GPIO_InitStructure.GPIO_Pin = TEMP_ADC_GPIO_PIN2; // 配置 IO
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;     //不上拉不下拉
    GPIO_Init(TEMP_ADC_GPIO_PORT2, &GPIO_InitStructure);
    /*=====================通道3=======================*/
    RCC_AHB1PeriphClockCmd(TEMP_ADC_GPIO_CLK3,ENABLE); // 使能 GPIO 时钟
    GPIO_InitStructure.GPIO_Pin = TEMP_ADC_GPIO_PIN3;  // 配置 IO
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;   //不上拉不下拉
    GPIO_Init(TEMP_ADC_GPIO_PORT3, &GPIO_InitStructure);
}

使用到GPIO时候都必须开启对应的GPIO时钟,GPIO用于AD转换功能必须配置为模拟输入模式。

(3)配置ADC工作模式

static void Temp_ADC_Mode_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;     
    // --------------DMA Init 结构体参数初始化-------------     
    // ADC1使用DMA2,数据流0,通道0,
    RCC_AHB1PeriphClockCmd(TEMP_ADC_DMA_CLK, ENABLE);   // 开启DMA时钟
    DMA_InitStructure.DMA_PeripheralBaseAddr = TEMP_ADC_DR_ADDR; // 外设基址为:ADC 数据寄存器地址
    DMA_InitStructure.DMA_Memory0BaseAddr = (u32)ADC_ConvertedValue;  // AD值存储地址
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;   // 数据传输方向为外设到存储器
    DMA_InitStructure.DMA_BufferSize = TEMP_NOFCHANEL; // 缓冲区大小,指一次传输的数据量
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;// 外设寄存器只有一个,地址不递增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;   // 存储器地址固定
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  // 外设数据大小为半字
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;   // 存储器数据大小也为半字
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环传输模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;// DMA传输通道优先级为高
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;    // 禁止DMA FIFO ,使用直连模式
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // FIFO大小,FIFO禁止时不用配置
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;  
    DMA_InitStructure.DMA_Channel = TEMP_ADC_DMA_CHANNEL; // 选择 DMA 通道,通道存在于流中
    DMA_Init(TEMP_ADC_DMA_STREAM, &DMA_InitStructure);//初始化DMA流,
    DMA_Cmd(TEMP_ADC_DMA_STREAM, ENABLE); // 使能DMA流

    RCC_APB2PeriphClockCmd(TEMP_ADC_CLK , ENABLE);// 开启ADC时钟     
    // -------------ADC Common 结构体 参数初始化----------------
    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;// 独立ADC模式
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;// 时钟为fpclk x分频
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; // 禁止DMA直接访问模式     
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;  // 采样时间间隔
    ADC_CommonInit(&ADC_CommonInitStructure);     
    // -------------------ADC Init 结构体 参数初始化--------------------------
    ADC_StructInit(&ADC_InitStructure);
    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;// ADC 分辨率
    ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式,多通道采集需要
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;//禁止外部边沿触发
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;  //外部触发通道,使用软件触发时此值随便赋值即可
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据右对齐ADC_InitStructure.ADC_NbrOfConversion = TEMP_NOFCHANEL;      //转换通道3个                             
    ADC_Init(TEMP_ADC, &ADC_InitStructure);

    
  // 配置 ADC 通道转换顺序和采样时间周期
  ADC_RegularChannelConfig(TEMP_ADC, TEMP_ADC_CHANNEL1, 1, ADC_SampleTime_3Cycles);
  ADC_RegularChannelConfig(TEMP_ADC, TEMP_ADC_CHANNEL2, 2, ADC_SampleTime_3Cycles);
  ADC_RegularChannelConfig(TEMP_ADC, TEMP_ADC_CHANNEL3, 3, ADC_SampleTime_3Cycles);

    ADC_DMARequestAfterLastTransferCmd(TEMP_ADC, ENABLE); // 使能DMA请求      
    ADC_DMACmd(TEMP_ADC, ENABLE);// 使能ADC DMA
    ADC_Cmd(TEMP_ADC, ENABLE);  // 使能ADC
    ADC_SoftwareStartConv(TEMP_ADC);//开始ADC转换,软件触发
}

首先,使用DMA_InitTypeDef定义了DMA初始化类型变量,另外使用ADC_InitTypeDef和ADC_CommonInitTypeDef结构体分别定义一个ADC初始化和ADC通用类型变量。

调用RCC_APB2PeriphClockCmd()开启ADC时钟以及RCC_AHB1PeriphClockCmd()开启DMA时钟。

对DMA进行必要的配置。首先设置外设基地址就是ADC的规则数据寄存器地址;存储器的地址就是指定的数据存储区空间,ADC_ConvertedValue是我们定义的一个全局数组名,它是一个无符号16位含有3个元素的整数数组;ADC规则转换对应只有一个数据寄存器,所以地址不能递增,而定义的存储区是专门用来存放不同通道数据的,所以需要自动地址递增。ADC的规则数据寄存器只有低16位有效,实际存放的数据只有12位而已,所以设置数据大小为半字大小。ADC配置为连续转换模式,DMA也设置为循环传输模式。设置好DMA相关参数后就使能DMA的ADC通道。

接下来使用ADC_CommonInitTypeDef结构体变量ADC_CommonInitStructure来配置ADC为独立模式、分频系数4、20个周期的采样延迟,并调用ADC_CommonInit函数完成ADC通用工作环境配置。

使用ADC_InitTypeDef结构体变量ADC_InitStructure来配置ADC1为12位分辨率、使能扫描模式、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数据格式、转换通道为3,并调用ADC_Init函数完成ADC1工作环境配置。

ADC_RegularChannelConfifig函数用来绑定ADC通道转换顺序和采样时间。分别绑定3个ADC通道引脚并设置相应的转换顺序。

ADC_DMARequestAfterLastTransferCmd函数控制是否使能ADC的DMA请求,如果使能请求,并调用ADC_DMACmd函数使能DMA,则在ADC转换完成后就请求DMA实现数据传输。ADC_Cmd函数控制ADC转换启动和停止。

最后使用软件触发调用ADC_SoftwareStartConvCmd函数进行使能配置。

(4)Main程序

/**  主函数  */
int main(void)
{
    Debug_USART_Config();
    Temp_Init();
    while (1)
    {
        ADC_ConvertedValueLocal[0] =(float) ADC_ConvertedValue[0]/4096*(float)3.3;
        ADC_ConvertedValueLocal[1] =(float) ADC_ConvertedValue[1]/4096*(float)3.3;
        ADC_ConvertedValueLocal[2] =(float) ADC_ConvertedValue[2]/4096*(float)3.3;
        printf("\r\n PB0 value = %f V \r\n",ADC_ConvertedValueLocal[0]);
        printf("\r\n PB1 value = %f V \r\n",ADC_ConvertedValueLocal[1]);
        printf("\r\n PA6 value = %f V \r\n",ADC_ConvertedValueLocal[2]);
        Delay(0xffffff);  
    }
}

主函数先调用Debug_USART_Config函数配置调试串口相关参数,接下来调用Temp_Init函数进行ADC初始化配置并启动ADC。配置了DMA数据传输,它会自动把ADC转换完成后数据保存到数组ADC_ConvertedValue内,我们只要使用数组就可以了。经过简单地计算就可以得到每个通道对应的实际电压。

来源:中科芯MCU

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

围观 11

ADC可以将现实世界中连续变化的模拟量,如温度、压力、流量、速度、光强等,转换成离散的数字量,输入到计算机中进行处理。按照原理不同,ADC可以分为积分型、逐次逼近型(SAR)、并行比较型、Σ-∆调制型、电容阵列逐次比较型及压频变换型。逐次逼近型(SAR)ADC由比较器和DA转换器构成,它从最高有效MSB位开始通过逐次比较,按顺序以位位单位对输入电压与内置DA转换器输出电压进行比较,经过多次比较输出相应的数字值。逐次逼近型(SAR)ADC的电路规模属于中等,它的优点是速度较快、功耗低、成本适中。

ADC简介

CKS32F4xx系列产品内嵌3个12位SAR型ADC,每个ADC多达19个复用通道,可测量来自16个外部、2个内部和VBAT通道的信号,具有独立模式、双重模式和三种模式,并支持单次、连续、扫描或间断采样模式下进行A/D转换,对于不同AD转换要求几乎都有合适的模式可选,转换的结果可以按照左对齐或右对齐的方式存储在16位数据寄存器中。下图是CKS32F4xx系列产品ADC的结构框图,每个模块的具体描述和技术参数可以参阅《CKS32F4xx参考手册》。

1.png

ADC主要特性

▲ 分辨率支持12位,10位,8位,6位,可软件配置

▲ 支持转换结束产生中断、包括规则通道和注入通道

▲ 支持单次、连续、间隔转换模式

▲ 可独立设置各通道采样时间

▲ 规则通道转换和注入通道转换均有外部触发选项,支持软件或者硬件触发转换

▲ 支持模拟看门狗

▲ 支持双重或者三重模式(具有2个或以上ADC的器件)

▲ 支持双重或者三重模式下可配置的DMA数据存储

▲ 可配置双重或者三重交替采样模式下的转换延迟时间间隔

▲ ADC供电要求:全速模式下为2.4V到3.6V,低速模式下为1.8V

▲ ADC输入电压范围:Vref-≤Vin≤Vref+

▲ 规则通道转换期间可产生DMA请求

固件库中与ADC相关的主要API

CKS32F4xx系列固件库中与ADC相关的API定义在cks32f4xx_adc.h和cks32f4xx_adc.c两个文件中,前者为头文件,包含寄存器地址、常量定义、API函数声明,后者为API的具体实现。CKS32F4xx系列产品最多有3个ADC,编号分别为ADC1、ADC2、ADC3,大多数API的第一个参数为ADC_TypeDef*ADCx,表示操作的ADC实体编号。下表列出了常用的API函数:

API函数原型

函数功能

void ADC_DeInit(void)

复位ADC外设寄存器

void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct)

根据ADC_InitStruct中参数初始化ADC

void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct)

复位结构体成员

void ADC_CommonInit(ADC_CommonInitTypeDef* ADC_CommonInitStruct)

根据结构体参数初始化ADC外设

void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能或者禁止ADC外设

void ADC_TempSensorVrefintCmd(FunctionalState NewState)

使能或者禁止温度传感器通道

void ADC_VBATCmd(FunctionalState NewState)

使能或者禁止VBAT通道

void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)

设置ADC规则通道的参数和采样时间

void ADC_SoftwareStartConv(ADC_TypeDef* ADCx)

软件启动ADC转换

FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx)

得到ADC转换状态

void ADC_EOCOnEachRegularChannelCmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能或者禁止每个规则转换通道的EOC

void ADC_ContinuousModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能或者禁止ADC的连续转换模式

void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number)

配置ADC规则组通道的间断模式

void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能或者禁止ADC的规则组通道

uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx)

得到规则通道最后一次转换的数值

void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能或者禁止ADC的DMA请求

void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)

设置ADC注入通道的参数和采样时间

void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length)

配置注入通道序列长度

void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset)

设置注入通道转换值偏移

void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv)

配置注入通道的外部触发

void ADC_SoftwareStartInjectedConv(ADC_TypeDef* ADCx)

配置注入通道转换

void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState)

使能注入通道组的间断模式

void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState)

使能或者禁止ADC中断

FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG)

检测ADC特定标志是否设置

void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG)

清除ADC标志

ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT)

通过相应标志检测是否发生相应中断

void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT)

清除中断挂起位

实现ADC单通道电压采集

CKS32F4xx系列的ADC功能繁多,我们设计几个实验尽量完整的展示ADC的功能。本次课堂主要介绍比较基础实用的单通道电压采集,实现开发板上引脚电压的采集并通过串口打印至PC端串口调试助手。单通道电压采集适用AD转换完成中断,在中断服务函数中读取数据,不使用DMA传输,在后期多通道采集实验中再使用DMA传输。相关核心代码实现如下:

(1)ADC宏定义

// ADC GPIO 宏定义
#define TEMP_ADC_GPIO_PORT GPIOB
#define TEMP_ADC_GPIO_PIN GPIO_Pin_0
#define TEMP_ADC_GPIO_CLK RCC_AHB1Periph_GPIOB
// ADC 序号宏定义
#define TEMP_ADC ADC1
#define TEMP_ADC_CLK RCC_APB2Periph_ADC1
#define TEMP_ADC_CHANNEL ADC_Channel_8
// ADC 中断宏定义
#define Temp_ADC_IRQ ADC_IRQn
#define Temp_ADC_INT_FUNCTION ADC_IRQHandler

(2)ADC GPIO初始化

static void Temp_ADC_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_AHB1PeriphClockCmd(TEMP_ADC_GPIO_CLK, ENABLE);// 使能 GPIO 时钟
    GPIO_InitStructure.GPIO_Pin = TEMP_ADC_GPIO_PIN;  // 配置 IO
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;    // 配置为模拟输入
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;  //不上拉不下拉
    GPIO_Init(TEMP_ADC_GPIO_PORT, &GPIO_InitStructure);
}

使用到GPIO时候都必须开启对应的GPIO时钟,GPIO用于AD转换功能必须配置为模拟输入模式。

(3)配置ADC工作模式

static void Temp_ADC_Mode_Config(void)
{
    ADC_InitTypeDef ADC_InitStructure;
    ADC_CommonInitTypeDef ADC_CommonInitStructure;
    RCC_APB2PeriphClockCmd(TEMP_ADC_CLK , ENABLE);     // 开启ADC时钟 
    
    // -------------------ADC Common 结构体 参数 初始化------------------------
    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 独立ADC模式
    ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;// 时钟为fpclk x分频
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;// 禁止DMA直接访问模式
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;   // 采样时间间隔 ADC_CommonInit(&ADC_CommonInitStructure);

    // -------------------ADC Init 结构体 参数 初始化--------------------------
    ADC_StructInit(&ADC_InitStructure);
    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // ADC 分辨率
    ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 禁止扫描模式,多通道采集才需要
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;//禁止外部边沿触发
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; //外部触发通道,本例子使用软件触发,此值随便赋值即可
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据右对齐
    ADC_InitStructure.ADC_NbrOfConversion = 1;         //转换通道 1个                      
    ADC_Init(TEMP_ADC, &ADC_InitStructure);

    // 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
    ADC_RegularChannelConfig(TEMP_ADC, TEMP_ADC_CHANNEL, 1, ADC_SampleTime_56Cycles);
    // ADC 转换结束产生中断,在中断服务程序中读取转换值
    ADC_ITConfig(TEMP_ADC, ADC_IT_EOC, ENABLE);
    ADC_Cmd(TEMP_ADC, ENABLE);  // 使能ADC
    ADC_SoftwareStartConv(TEMP_ADC);//开始ADC转换,软件触发
}

首先,使用ADC_InitTypeDef和ADC_CommonInitTypeDef结构体分别定义一个ADC初始化和ADC通用类型变量。调用RCC_APB2PeriphClockCmd()开启ADC时钟。接下来使用ADC_CommonInitTypeDef结构体变量ADC_CommonInitStructure来配置ADC为独立模式、分频系数为2、不需要设置DMA模式、20个周期的采样延迟,并调用ADC_CommonInit函数完成ADC通用工作环境配置。我们使用ADC_InitTypeDef结构体变量ADC_InitStructure来配置ADC1为12位分辨率、单通道采集不需要扫描、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数据格式转换通道为1,并调用ADC_Init函数完成ADC1工作环境配置。

ADC_RegularChannelConfifig函数用来绑定ADC通道转换顺序和时间。它接收4个形参,第一个形参选择ADC外设,可为ADC1、ADC2或ADC3;第二个形参通道选择,总共可选18个通道;第三个形参为转换顺序,可选为1到16;第四个形参为采样周期选择,采样周期越短,ADC转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC转换数据输出周期就越长同时数据精度越高。PC3对应ADC通道ADC_Channel_13,这里我们选择ADC_SampleTime_56Cycles即56周期的采样时间。

利用ADC转换完成中断可以非常方便的保证我们读取到的数据是转换完成后的数据而不用担心该数据可能是ADC正在转换时“不稳定”数据。我们使用ADC_ITConfifig函数使能ADC转换完成中断,并在中断服务函数中读取转换结果数据。ADC_Cmd函数控制ADC转换启动和停止。

最后使用软件触发调用ADC_SoftwareStartConvCmd函数进行使能配置。

4)在Temp_ADC_NVIC_Config函数中配置ADC转换完成中断的优先级分组和优先级。

static void Temp_ADC_NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 配置优先级分组
    NVIC_InitStructure.NVIC_IRQChannel = Temp_ADC_IRQ;  // 配置中断优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

(5)ADC中断服务程序

void ADC_IRQHandler(void)
{
    if(ADC_GetITStatus(TEMP_ADC,ADC_IT_EOC)==SET)
    {
        ADC_ConvertedValue = ADC_GetConversionValue(TEMP_ADC);  // 读取ADC的转换值
    }
    ADC_ClearITPendingBit(TEMP_ADC,ADC_IT_EOC);
}

中断服务函数一般定义在cks32f4xx_it.c文件内,我们使能了ADC转换完成中断,因此在ADC转换完成后就会进入中断服务函数,在中断服务函数内直接读取ADC转换结果保存在变量ADC_ConvertedValue(在main.c中定义)中。ADC_GetConversionValue函数是获取ADC转换结果值的库函数,只有一个形参为ADC外设,可选为ADC1、ADC2或ADC3,该函数返回一个16位的ADC转换结果值。

(6)Main程序

int main(void)
{
    Debug_USART_Config(); //初始化USART1
    Temp_Init();while(1)
    {   
        ADC_Vol =(float) ADC_ConvertedValue/4096*(float)3.3; // 读取转换的AD值
        printf("\r\n The current AD value = %f V \r\n",ADC_Vol);     
        Delay(0xffffee);  
    }
}

主函数先配置调试串口相关参数,接下来调用Temp_Init函数进行ADC初始化配置并启动ADC。在ADC中断服务函数中把AD转换结果保存在变量ADC_ConvertedValue中,可以计算出对应的IO口电压值,最后把相关数据打印至串口调试助手。

来源:中科芯MCU

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

围观 20

本章中,我们主要对CKS32F4xx系列的待机模式(STANDBY)做详细介绍。在该模式下,芯片功耗最低,1.2V供电区域、PLL、HSI和HSE振荡器也完全被关闭。除备份域(RTC寄存器、RTC备份寄存器和备份SRAM)和待机电路中的寄存器外,SRAM和寄存器内容丢失。因此,从待机模式唤醒后,只能从头开始执行程序,那我们如何进入STANDBY模式及唤醒方式,可以按照下述表格中的步骤执行即可:

1.jpg

CKS32F4xx系列标准库把进入STANDBY模式这部分的操作封装到PWR_EnterSTANDBYMode函数中了,需要先通过PWR_CR设置PDDS位以及SLEEPDEEP位,使得芯片进入深度睡眠时进入待机模式,接着调用__force_stores函数确保存储操作完毕后再调用WFI指令,从而进入待机模式。需要注意的是,调用本函数前,还需要清空WUF 寄存器位才能进入待机模式。

RTC时钟简介

CKS32F4xx系列的RTC,是一个独立的BCD定时器/计数器,RTC提供一个日历时钟(包含年月日时分秒信息)、两个可编程闹钟(ALARM A和ALARM B)中断,以及一个具有中断功能的周期性可编程唤醒标志。RTC还包含用于管理低功耗模式的自动唤醒单元。两个32位寄存器包含二进码十进数格式(BCD)的秒、分钟、小时(12或24小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为28、29(闰年)、30和31天。并且还可以进行夏令时补偿。其它32位寄存器还包含可编程的闹钟亚秒、秒、分钟、小时、星期几和日期。此外,还可以使用数字校准功能对晶振精度的偏差进行补偿。RTC模块和时钟配置是在后备区域,即在系统复位或从待机模式唤醒后RTC的设置和时间维持不变,只要后备区域供电正常,那么RTC将可以一直运行。但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前,先要取消备份区域(BKP)写保护。RTC的框图,如下图所示:

2.png

采用RTC周期性唤醒STANDBY模式实验

程序设计主要要点如下:

① RTC初始化;

② RTC周期性自动唤醒;

③ 清除WUF标志位,进入待机状态。

1)初始化RTC配置函数

void CKS_RTC_Init(void)
{  
    uint16_t retry = 0x1FFF;   
    RTC_InitTypeDef  RTC_InitStructure;
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);   
    PWR_BackupAccessCmd(ENABLE); 
   
   RCC_LSEConfig(RCC_LSE_ON);     
   while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)    
   {    
       retry--;     
       Delay(0xffff);  
   }  
   if(retry == 0)  
   {    
       return 1;   
   }  
   RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);       
   RCC_RTCCLKCmd(ENABLE);   
   
   RTC_InitStructure.RTC_AsynchPrediv  =  0x7F;  
   RTC_InitStructure.RTC_SynchPrediv   =  0xFF;  
   RTC_InitStructure.RTC_HourFormat   =  RTC_HourFormat_24;  
   RTC_Init(&RTC_InitStructure);
}

在CKS_RTC_Init函数中,用来初始化RTC配置以及日期和时钟,但只在首次设置时间,随后重新上电/复位都不再进行时间设置(前提是备份电池有电)。为了时间更为精准,这里选用了LSE,即外部32.768kHz晶振作为RTC_CLK的时钟源,而RTC时钟核心,要求提供1Hz的时钟,所以接着是设置RTC_CLK的预分频系数,包括异步和同步两个,这里设置异步分频因子为ASYNCHPREDIV为(127),同步分频因子为ASYNCHPREDIV(255),则产生的时钟CK_SPRE=32.768/(127+1)*(255+1)=1HZ,即每秒更新一次。

2)RTC周期性唤醒配置函数

void RTC_Set_WakeUp(uint32_t wksel, uint16_t cnt)
{   
    NVIC_InitTypeDef   NVIC_InitStructure;  
    EXTI_InitTypeDef   EXTI_InitStructure;
   
   RTC_WakeUpCmd(DISABLE);  
   RTC_WakeUpClockConfig(wksel);   
   RTC_SetWakeUpCounter(cnt-1); 
  
   RTC_ClearITPendingBit(RTC_IT_WUT);   
   EXTI_ClearITPendingBit(EXTI_Line22);    
   
   RTC_ITConfig(RTC_IT_WUT, ENABLE);   
   RTC_WakeUpCmd(ENABLE); 
   
   EXTI_InitStructure.EXTI_Line = EXTI_Line22;  
   EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;   
   EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;   
   EXTI_InitStructure.EXTI_LineCmd = ENABLE;  
   EXTI_Init(&EXTI_InitStructure); 
   
   NVIC_InitStructure.NVIC_IRQChannel = RTC_WKUP_IRQn;   
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;  
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;  
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;   
   NVIC_Init(&NVIC_InitStructure); 
}

在RTC_Set_WakeUp函数中,首先通过RTC_WakeUpCmd函数,关闭WakeUp,接着调用RTC_WakeUpClockConfig函数,配置WakeUp时钟分频系数及来源,然后通过调用RTC_SetWakeUpCounter,设置WakeUp自动装载寄存器,随后使能WakeUp,最后开启配置闹钟中断以及NVIC中断优先级。鉴于此处为RTC唤醒待机实验,仅做demo例程使用,因而不用编写中断服务函数。

3)芯片进入STANDBY模式

查阅CKS32F4xx系列标准库及相关手册,我们了解到使能RTC周期性唤醒,在进入STANDBY模式前,需要进行以下操作,代码如下:

void CKS_Set_Standby_Mode(void)
{      
    RTC_ITConfig(RTC_IT_TS|RTC_IT_WUT|RTC_IT_ALRB|RTC_IT_ALRA, DISABLE);   
    RTC_ClearITPendingBit(RTC_IT_TS|RTC_IT_WUT|RTC_IT_ALRB|RTC_IT_ALRA);   
    PWR_ClearFlag(PWR_FLAG_WU);    
    
    RTC_Set_WakeUp(RTC_WakeUpClock_CK_SPRE_16bits, 3);   
    
    PWR_EnterSTANDBYMode();         
}

在CKS_Set_Standby_Mode函数中,先禁止RTC中断(ALRAIE、ALRBIE、TSIE、WUTIE和TAMPIE等),接着清零对应中断标志位,以及清除PWR唤醒(WUF)标志,然后调用RTC_Set_WakeUp函数,设置每3s后唤醒STANDBY模式,同时该函数中也重新使能RTC对应中断,最后调用PWR_EnterSTANDBYMode进入STANDBY模式。

4)主函数配置

本例程中主函数主要对上文所述函数调用,程序编译下载至开发板,先进行相关外设初始化后,直接进入STANDBY模式,每隔3s由RTC唤醒,随即又进入STANDBY模式,循环往复,主函数代码如下:

int main(void)
{    
    CKS_RTC_Init();          
    while (1)  
    {            
        CKS_Set_Standby_Mode();      
    }
}

来源:中科芯MCU

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

围观 11

本课讲为大家讲解CKS32F4xx系列产品的低功耗模式之睡眠(Sleep)模式。MCU为满足某些应用场景:如小型化低容量设备,长期监测设备等,低功耗应运而生,其中根据需求,用户可以选择睡眠模式、停止模式及待机模式,今天本课将带大家一起配置睡眠模式。

电源系统及低功耗

首先,MCU要做到低功耗,必须要知道电源是怎么分配的,CKS32F4xx系列的电源系统框图如下所示:

1.png

电源框图中,第1部分是备份域电路,由VDD或Vbat电池供电,接入3V纽扣电池后,可保证VDD掉电时,能够保留关键数据。

第2部分为电压调压器供电,由于其输出约为1.2V,又称1.2V域。1.2V域给除备份域和待机电路以外的所有数字电路供电,在低功耗的三种模式中,1.2V域也对应三种状态:正常开启、低功耗和关闭。

第3部分为A/D转换器及参考电压供电,故为VDDA供电区,目的是使用独立电源能更好的滤波,从而提高精度,也可以在Vref上外接高精度电源,进一步提高进度。

所谓低功耗,即是关闭相应功耗路线,1)睡眠模式仅关闭第二部分中的内核时钟;2)停止模式关闭所有时钟,且调压器可选择正常开启或低功耗运行;3)待机模式关闭所有时钟以及调压器,仅保留备份域及待机电路;

外部中断唤醒睡眠模式验证

接下来举一个低功耗睡眠模式且由外部中断唤醒的例子,睡眠模式比较简单,直接调用编译器内置函数__WFE()或__WFI(),取决于使用事件唤醒还是中断唤醒;另外默认SCR(系统控制寄存器)的SLEEPDEEP位和SLEEPONEXIT位置0,效果是立即触发睡眠,如果将SLEEPONEXIT置1可以设置成中断退出后触发睡眠,也就是说内核将会只在中断内工作。

验证流程如下图,电源串联万用表电流档,直观比较睡眠模式开启前后的电流大小。

2.png

程序配置及中断服务函数

1)开启时钟及GPIO初始化如下,按键直连PE10,按下为低电平。

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE ,ENABLE);     
//开启按键GPIO口的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); 
//由于使用外部中断需要使能 SYSCFG 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;           //选择按键PE10的引脚   
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;     //设置引脚为输入模式
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;  //不上拉也不下拉
GPIO_Init( GPIOE, &GPIO_InitStructure);            //使用上面的结构体初始化按键

2)总中断NVIC初始化如下,GPIO外部中断源一共对应7个通道,PX0~PX4分别对应EXTI0~EXTI4的5个通道,PX5~PX9对应通道EXTI_9_5,PX10~PX15 对应通道EXTI_15_10,故PE10对应中断服务函数EXTI15_10_IRQHandler()。

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);          //配置NVIC为优先级组1
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;   //按键PE10使用中断源EXTI15_10
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //配置抢占优先级:1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;       //配置子优先级:1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;      //使能中断通道
NVIC_Init(&NVIC_InitStructure);                   //使用上面的结构体初始化总中断

3)外部中断配置如下,主要在初始化中对中断屏蔽寄存器EXTI_IMR及下降沿触发选择寄存器EXTI_FTSR配置,将EXTI_IMR的MR10位置1,可开放相应通道中断请求。

SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource10); //连接外部中断源到PE10
EXTI_InitStructure.EXTI_Line = EXTI_Line10; //选择EXTI中断源
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //选择为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能中断/事件线
EXTI_Init(&EXTI_InitStructure); //使用上面的结构体初始化外部中断:EXTI_IMR的MR10位置1等

4)中断服务函数如下,触发后验证标志位,确保发生的是外部线中断;其次外部中断线发生事件后,中断挂起寄存器EXTI_PR会置1,通过 EXTI_ClearITPendingBit再次在PR写入1,从而清除它,进入下一次外部中断等待。

void EXTI15_10_IRQHandler(void)
{  
    if(EXTI_GetITStatus(EXTI_Line10) != RESET)        //验证是否产生了外部线中断  
    {    
        Delay(0x1FFFFFF);                         //简单延时验证电流    
        EXTI_ClearITPendingBit(EXTI_Line10);         //清除标志位  
    }  
}

主函数及现象

Main函数首先初始化GPIO、NVIC中断、外部中断,进入主循环后先延迟观测电流,测得19.3mA,延迟后,__WFI()自动触发睡眠模式,电流测得13.3mA,如果不进行按键操作,MCU将一直睡眠,不再执行任何代码,此时按下轻触开关,触发外部中断,电流回升至19.3mA,睡眠模式唤醒时间无延迟,执行完中断服务程序后,会回到代码睡眠时的句段,继续执行。PS:如果需要设置成中断退出后触发睡眠模式,则将SLEEPONEXIT置1。

int main(void)
{ 
    EXTI_Config();      //配置及初始化GPIO、NVIC中断、外部中断  
    while(1)  
    {    
        Delay(0x1FFFFFF);        
        //SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk;  //如需设置成中断退出后触发睡眠模式    
        __WFI();            //进入睡眠模式,电流从19.3mA降至13.3mA  
    }
}

来源:中科芯MCU

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

围观 6

在本章节中,首先向大家介绍电源系统结构,接着简述三种低功耗模式,最后重点介绍CKS32F4xx系列如何进入STOP模式以及使用按键实现中断唤醒。

CKS32F4xx电源系统介绍

1.png

CKS32F4xx系列的电源系统框图

CKS32F4xx系列的器件工作电压(VDD)为1.8~3.6V。

①备份域电路:包含LSE振荡器、RTC、备份寄存器及备份SRAM。电源开关自动切换VDD和VBAT供电。

②调压器电路(1.2V域):为备份域及待机电路外的所有数字电路供电,其中包括内核、数字外设及RAM。

③ADC电路和参考电压:ADC工作电源使用VDDA引脚输入,使用VSSA作为独立的地连接,VREF引脚则为ADC提供测量使用的参考电压。

低功耗模式简介

CKS32F4xx系列按功耗由高到低排列,分别是运行、睡眠、停止(STOP)和待机(STANDBY)四种工作模式。上电复位后,CKS32F4xx系列会处于正常运行状态,当不需要继续运行内核时,就可以选择进入后面的三种低功耗模式降低功耗。这三种模式中,电源消耗不同、唤醒时间不同、唤醒源不同,用户需要根据应用需求,选择最佳的低功耗模式。其中,最低功耗的是待机模式,停机模式(STOP)是次低功耗的,最后是睡眠模式。低功耗相关寄存器主要有SCB_SCR、PWR_CR、PWR_CSR,进入各种低功耗需要调用的指令有WFI或WFE,以下做简要介绍。

1.SCB_SCR(系统控制寄存器)

2.png

3.jpg

若进入停止模式or待机模式,需将SLEEPDEEP置1。

2.PWR_CR(电源控制寄存器)

4.png

若进入停止模式,需将PDDS清0,LPDS选调节器模式;若进入待机模式,需将PDDS置1,清除唤醒位CWUF。

3.PWR_CSR(电源控制/状态寄存器)

5.png

若芯片处于待机模式下,需使用使用WKUP引脚唤醒并需要清除WUF标记位。

注:WKUP上升沿才能唤醒待机状态,清除WUF标记位实则需操作CWUF位。

4.WFI与WFE指令

实质上都是内核指令,在库文件core_cmInstr.h中把这些指令封装成了函数,调用它们都能进入低功耗模式。调用时,需要使用函数的格式“__WFI();”和“__WFE();”,这是因为__wfi及__wfe是编译器内置的函数,函数内部使用调用了相应的汇编指令。其中WFI指令决定了它需要用中断唤醒,而WFE则决定了它可用事件来唤醒。

以下表格是对三种低功耗模式的简要概述。

6.jpg

本章中,我们主要对CKS32F4xx系列的低功耗模式—停止模式(STOP)做详细介绍。该模式中,由于其1.2V区域的部分电源没有关闭,会保留内核的寄存器、内存的信息,所以从STOP模式唤醒,并重新开启时钟后,还可以从上次停止处继续执行代码。停止模式可以由任意一个外部中断(EXTI)唤醒。在停止模式中可以选择电压调节器为开模式或低功耗模式,可选择内部FLASH工作在正常模式或掉电模式。那我们如何进入STOP模式,可以按照下述表格中的步骤执行即可:

7.jpg

CKS32F4xx系列标准库把进入STOP模式这部分的操作封装到PWR_EnterSTOPMode函数中了,并且需要注意的是进入STOP模式后,CKS32F4xx系列的所有IO都保持在停止前的状态,且当它被唤醒时,CKS32F4xx系列使用HSI作为系统时钟运行,由于系统时钟会影响很多外设的工作状态,所以一般我们在唤醒后会重新开启HSE,把系统时钟设置为原来的状态。上面表格也提到在停止模式中,还可以控制FLASH的供电,使用库函数PWR_FlashPowerDownCmd配置进入掉电状态还是正常供电状态,本质上是封装了一个对FPDS寄存器位操作的语句,使用时需要再进入停止模式前被调用,可以进一步降低功耗。

采用EXTI唤醒STOP模式实验

程序设计主要要点如下:

① 初始化用于唤醒的中断按键;

② 配置不用的I/O端口;

③ 设置停滞状态是FLASH掉电以及选择电压调节器的工作模式并进入停止状态;

④ 使用按键中断唤醒芯片;

⑤ 重启HSE时钟,使系统完全恢复停止之前的状态。

1)初始化用于唤醒的中断按键

此处选择连接PB1引脚的按键用于将芯片从STOP模式唤醒,代码如下:

void CKS_EXTI_Init(void)
{  
    GPIO_InitTypeDef   GPIO_InitStructure;  
    EXTI_InitTypeDef  EXTI_InitStructure;  
    NVIC_InitTypeDef  NVIC_InitStructure;
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
    
  /* Configure PB1 as EXTI */  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;  
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;    
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;  
  GPIO_Init(GPIOB, &GPIO_InitStructure); 
  
  /* Configure PB1 to EXTI line 1 */    
  SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOB,EXTI_PinSource1);
  
  /* EXTI configuration */  
  EXTI_InitStructure.EXTI_Line = EXTI_Line1;  
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;  
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;  
  EXTI_Init(&EXTI_InitStructure);
  
  /* NVIC configuration */  
  NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;  
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;  
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
  NVIC_Init(&NVIC_InitStructure);
}

在CKS_EXTI_Init函数中,首先使能GPIOB时钟,又因为用到外部中断,所以必须先使能SYSCFG时钟,接着对GPIOB初始化为上拉输入,并调用函数 SYSCFG_EXTILineConfig配置GPIOB.1连接到中断线1,最后初始化EXTI中断线以及NVIC中断优先级。

void EXTI1_IRQHandler(void)
{  
    if(EXTI_GetITStatus(EXTI_Line1) != RESET)   
    {       
        EXTI_ClearITPendingBit(EXTI_Line1);       
    }  
}

在中断服务函数EXTI1_IRQHandler内,主要是清除LINE1上的中断标志位。

2)配置未使用的I/O口

进入STOP模式之前,需要对I/O进行处理,若不处理很多配置为输入浮空的I/O口在受到外界干扰的时候,状态不定,消耗大量的电流,代码如下:

void CKS_Stop_Mode_IO_Set(void)
{  
    /* set pins not used to AIN */  
    GPIO_InitTypeDef    GPIO_InitStructure;
  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH, ENABLE);  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOI, ENABLE);
   
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;  
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
   
   GPIO_Init(GPIOA, &GPIO_InitStructure);   
   GPIO_Init(GPIOC, &GPIO_InitStructure);   
   GPIO_Init(GPIOD, &GPIO_InitStructure);     
   GPIO_Init(GPIOF, &GPIO_InitStructure);    
   GPIO_Init(GPIOG, &GPIO_InitStructure);     
   GPIO_Init(GPIOH, &GPIO_InitStructure);    
   GPIO_Init(GPIOI, &GPIO_InitStructure);   
    
   GPIO_InitStructure.GPIO_Pin = (~GPIO_Pin_1);      
   GPIO_Init(GPIOB, &GPIO_InitStructure);  
}

由上述代码可知,我们将没用到的I/O全部设置为模拟输入,保留PB1作为按键,产生外部中断来将芯片从STOP模式唤醒。

3)芯片进入STOP模式

该部分代码一般是程序执行一段时间再调用,我们先使用PWR_FlashPowerDownCmd配置停止模式下FLASH使用掉电模式,随后调用PWR_EnterSTOPMode把调压器设置在低功耗模式,最后使用WFI指令进入停止状态。由上文可知,WFI进入停止模式可由任意的EXTI的中断唤醒,所以此处使用按键中断唤醒是可行的,代码如下:

void CKS_Set_Stop_Mode(void)
{  
    PWR_FlashPowerDownCmd (ENABLE);  
    PWR_EnterSTOPMode(PWR_Regulator_LowPower,PWR_STOPEntry_WFI);
}

4)主函数配置

本例程中主函数主要对上文所述函数调用,程序编译下载至开发板,先进行相关外设初始化后,直接进入STOP模式,此时按下按键,芯片可立即被唤醒,随即又进入STOP模式,循环往复,主函数代码如下:

int main(void)
{    
    GPIO_Configuration();  
    CKS_EXTI_Init();  
    CKS_Stop_Mode_IO_Set();
   while (1)  
   {          
       CKS_Stop_Mode_IO_Set();    
       CKS_Set_Stop_Mode();    
       SystemInit();        
       Delay(0xffffff);      
   }
}

来源:中科芯MCU

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

围观 24

上一章我们介绍了CKS32F4的通用定时器定时操作的使用方法,这一章我们将向大家介绍通用定时器作为定时器脉冲计数的使用。在本章中,我们将用TIM5的通道1(PA0)来做输入捕获,捕获PA0上的脉冲。

输入捕获简介

输入捕获模式可以用来测量脉冲宽度或者脉冲计数,我们简单说明脉冲计数的原理,测量方法如下:首先设置定时器通道x为上升沿捕获,在通道有脉冲触发时,定时器进入捕获中断,我们可以在中断中完成一次计数的累加,当一个计数周期结束后,得到的累加值就是脉冲计数值。

CKS32F4的定时器,除了TIM6和TIM7,其他定时器都有输入捕获功能。CKS32F4的输入捕获,简单的说就是通过检测TIMx_CHx上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置捕获时是否触发中断/DMA等。

本章我们用到TIM5_CH1来实现脉冲计数。

输入捕获操作

接下来,我们介绍我们本章需要用到的一些寄存器配置,需要用到的寄存器有:TIMx_ARR、TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1接下来我们介绍这几个寄存器的配置。

首先TIMx_ARR和TIMx_PSC,这两个寄存器用来设自动重装载值和TIMx的时钟分频,对于TIMx_AR,如图1所示:一定要注意当自动装载的值为空时,计数器不工作。

1.png

图1

对于TIMx_PSC,如图2所示,利用这个寄存器和RCC的预分频寄存器配合,我们可以得到几微妙到几毫秒的计数周期。

2.png

图2 

再来看看捕获/比较模式寄存器1:TIMx_CCMR1,这个寄存器在输入捕获的时候,非常有用,该寄存器的各位描述如图3所示:

3.png

图3 TIMx_CCMR1寄存器各位描

当在输入捕获模式下使用的时候,对应图3的第二行描述,从图中可以看出,TIMx_CCMR1明显是针对2个通道的配置,低八位[7:0]用于捕获/比较通道1的控制,而高八位[15:8]则用于捕获/比较通道2的控制,因为TIMx还有CCMR2这个寄存器,所以可以知道CCMR2是用来控制通道3和通道4,这里我们用到的是TIM5的捕获/比较通道1,我们重点介绍TIMx_CCMR1的[7:0]位(其高8位配置类似),TIMx_CCMR1的[7:0]位详细描述见图4所示:

4.png

图4 TIMx_CCMR1[7:0]位详细描述

其中CC1S[1:0],这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上,即CC1对应TIMx_CH1。输入捕获1预分频器IC1PSC[1:0],这个比较好理解。我们是1次边沿就触发1次捕获,所以选择00就是了。输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器的输入频率(TIMxCLK),一般为84Mhz/168Mhz,而fDTS则是根据TIMx_CR1的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT,N值就是滤波长度,举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到通道1上,且为上升沿触发,那么在捕获到上升沿的时候,再以fCK_INT的频率,连续采样到8次通道1的电平,如果都是高电平,则说明确是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。

这样可以滤除那些高电平脉宽低于8个采样周期的脉冲信号,从而达到滤波的效果。这里,我们不做滤波处理,所以设置IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。再来看看捕获/比较使能寄存器:TIMx_CCER,本章我们要用到这个寄存器的最低2位,CC1E和CC1P位。这两个位的描述如图5所示:

5.png

图5 TIMx_CCER最低2位描述

所以,要使能输入捕获,必须设置CC1E=1,而CC1P则根据自己的需要来配置。接下来我们再看看中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图6:

6.png

图6 TIMx_DIER寄存器各位描述

本章,我们需要用到中断来处理捕获数据,所以必须开启通道1的捕获比较中断,即CC1IE设置为1。控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的,这里前面两章都有介绍,请大家参考前面的章节。最后再来看看捕获/比较寄存器TIMx_CCR1,该寄存器用来存储捕获发生时,TIMx_CNT的值,我们从TIMx_CCR1就可以读出通道1捕获发生时刻的TIMx_CNT值,至此,我们把本章要用的几个相关寄存器都介绍完了,本章要实现通过输入捕获,来计量TIM5_CH1(PA0)上面的脉冲数量,下面我们介绍库函数配置上述功能输入捕获的步骤:

1)开启TIM5时钟,配置PA0为复用功能(AF2),并开启下拉电阻

要使用TIM5,我们必须先开启TIM5的时钟。同时我们要捕获TIM5_CH1上面的高电平脉宽,所以先配置PA0为带下拉的复用功能,同时,为了让PA0的复用功能选择连接到TIM5,所以设置PA0的复用功能为AF2,即连接到TIM5上面。开启TIM5时钟的方法为:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE);//>>TIM5时钟使能

当然,这里我们也要开启PA0对应的GPIO的时钟。配置PA0为复用功能,所以我们首先要设置PA0引脚映射AF2,方法为:

br

最后,我们还要初始化GPIO的模式为复用功能,同时这里我们还要设置为开启下拉。方法为:

typedefstructGPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;//GPIOA0
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度100MHz
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽复用输出
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_DOWN;//下拉
GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化PA0

跟上一讲PWM输出类似,这里我们使用的是定时器5的通道1,所以我们从CKS32F4对应的数据手册可以查看到对应的IO口为PA0:

2)初始化TIM5,设置TIM5的ARR和PSC

在开启了TIM5的时钟之后,我们要设置ARR和PSC两个寄存器的值来设置输入捕获的自动重装载值和计数频率。这在库函数中是通过TIM_TimeBaseInit函数实现的,在上面章节已经讲解过,这里不重复讲解。

TIM_TimeBaseStructure.TIM_Prescaler=psc;//定时器分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseStructure.TIM_Period=arr;//自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM5,&TIM_TimeBaseStructure);//初始化TIM5

3)设置TIM5的输入捕获参数,开启输入捕获

TIM5_CCMR1寄存器控制着输入捕获1和2的模式,包括映射关系,滤波和分频等。这里我们需要设置通道1为输入模式,且IC1映射到TI1(通道1)上面,并且不使用滤波(提高响应速度)器。库函数是通过TIM_ICInit函数来初始化输入比较参数的:

voidTIM_ICInit(TIM_TypeDef*TIMx,TIM_ICInitTypeDef*TIM_ICInitStruct)

同样,我们来看看参数设置结构体TIM_ICInitTypeDef的定义:

typedef struct
{
    uint16_t TIM_Channel; //>>通道
    uint16_t TIM_ICPolarity; //>>捕获极性
    uint16_t TIM_ICSelection;//>>映射
    uint16_t TIM_ICPrescaler;//>>分频系数
    uint16_t TIM_ICFilter; //>>滤波器长度
}TIM_ICInitTypeDef;

参数TIM_Channel很好理解,用来设置通道。我们设置为通道1,为TIM_Channel_1。参数TIM_ICPolarit是用来设置输入信号的有效捕获极性,这里我们设置为TIM_ICPolarity_Rising,上升沿捕获。同时库函数还提供了单独设置通道1捕获极性的函数为:

TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling);

这表示通道1为上升沿捕获,我们后面会用到,同时对于其他三个通道也有一个类似的函数,使用的时候一定要分清楚使用的是哪个通道该调用哪个函数,格式为TIM_OCxPolarityConfig()。参数TIM_ICSelection是用来设置映射关系,我们配置IC1直接映射在TI1上,选TIM_ICSelection_DirectTI。参数TIM_ICPrescaler用来设置输入捕获分频系数,我们这里不分频,所以选中TIM_ICPSC_DIV1,还有2,4,8分频可选。参数TIM_ICFilter设置滤波器长度,这里我们不使用滤波器,所以设置为0。这些参数的意义,在我们讲解寄存器的时候举例说明过,这里不做详细解释。我们的配置代码是:

TIM5_ICInitStructure.TIM_Channel=TIM_Channel_1;//>>选择输入端IC1映射到TI1上
TIM5_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;//>>上升沿捕获
TIM5_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;//>>映射到TI1上
TIM5_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;//>>配置输入分频,不分频
TIM5_ICInitStructure.TIM_ICFilter=0x00;//>>IC1F=0000配置输入滤波器不滤波
TIM_ICInit(TIM5,&TIM5_ICInitStructure);

4)使能捕获和更新中断(设置TIM5的DIER寄存器)

因为我们要捕获的是高电平信号,所以,第一次捕获是上升沿,这两件事,我们都在中断里面做,所以必须开启捕获中断和更新中断。这里我们使用定时器的开中断函数TIM_ITConfig即可使能捕获和更新中断:

TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//>>允许更新中断和捕获中断

5)设置中断优先级,编写中断服务函数

因为我们要使用到中断,所以我们在系统初始化之后,需要先设置中断优先级分组,这里方法跟我们前面讲解一致,调用NVIC_PriorityGroupConfig()函数即可,我们系统默认设置都是分组2。设置中断优先级的方法前面多次提到这里我们不做讲解,主要是通过函数NVIC_Init()来完成。设置优先级完成后,我们还需要在中断函数里面完成数据处理和捕获设置等关键操作,从而实现高电平计数统计。在中断服务函数里面,跟以前的外部中断和定时器中断实验中一样,我们在中断开始的时候要进行中断类型判断,在中断结束的时候要清除中断标志位。使用到的函数在上面的实验已经讲解过,分别为TIM_GetITStatus()函数和TIM_ClearITPendingBit()函数。

if(TIM_GetITStatus(TIM5,TIM_IT_Update)!=RESET){}//>>判断是否为更新中断
if(TIM_GetITStatus(TIM5,TIM_IT_CC1)!=RESET){}//>>判断是否发生捕获事件
TIM_ClearITPendingBit(TIM5,TIM_IT_CC1|TIM_IT_Update);//>>清除中断和捕获标志位

6)使能定时器(设置TIM5的CR1寄存器)

最后,必须打开定时器的计数器开关,启动TIM5的计数器,开始输入捕获。

TIM_Cmd(TIM5,ENABLE);//>>使能定时器5

通过以上6步设置,定时器5的通道1就可以开始输入捕获了。

代码示例

这里我们主要是添加了输入捕获初始化函数TIM5_CH1_Cap_Init以及中断服务函数TIM5_IRQHandler。对于输入捕获,我们也是使用的定时器相关的操作,接下来我们来看看两个函数的内容:

TIM_ICInitTypeDef TIM5_ICInitStructure;

//>>定时器5通道1输入捕获配置

//>>arr:自动重装值(TIM2,TIM5是32位的!!)psc:时钟预分频数

void TIM5_CH1_Cap_Init(u32 arr,u16 psc) 
{ 
    GPIO_InitTypeDef GPIO_InitStructure; 
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 
    NVIC_InitTypeDef NVIC_InitStructure; 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE); //>>TIM5 时钟使能 
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //>>使能 PORTA 时钟 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //>>GPIOA0 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//>>复用功能 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 
    //>>速度 100MHz 
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //>>推挽复用输出 
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //>>下拉 
    GPIO_Init(GPIOA,&GPIO_InitStructure); //>>初始化 PA0 
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource0,GPIO_AF_TIM5); //>>PA0 复用位定时器 5 
    TIM_TimeBaseStructure.TIM_Prescaler=psc; //>>定时器分频 
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //>>向上计数模式 
    TIM_TimeBaseStructure.TIM_Period=arr; //>>自动重装载值 
    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
    TIM_TimeBaseInit(TIM5,&TIM_TimeBaseStructure); 
    TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //>>选择输入端 IC1 映射到 TI1 上 
    TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; 
    //>>上升沿捕获 
    TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //>>映射到 TI1 上 
    TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //>>配置输入分频,不分频 
    TIM5_ICInitStructure.TIM_ICFilter = 0x00;//>>IC1F=0000 配置输入滤波器 不滤波 
    TIM_ICInit(TIM5, &TIM5_ICInitStructure); //>>初始化 TIM5 输入捕获参数 
    TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//>>允许更新和捕获中断 
    TIM_Cmd(TIM5,ENABLE ); 
    //>>使能定时器 5 
    NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; 
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//>>抢占优先级 2 
    NVIC_InitStructure.NVIC_IRQChannelSubPriority =0;//>>响应优先级 0 
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //>>IRQ 通道使能 
    NVIC_Init(&NVIC_InitStructure); //>>根据指定的参数初始化 VIC 寄存器、 
} 
//>>捕获状态 
//>>[7]:0,没有成功的捕获;1,成功捕获到一次. 
//>>[6]:0,还没捕获到低电平;1,已经捕获到低电平了. 
//>>[5:0]:捕获低电平后溢出的次数(对于 32 位定时器来说,1us 计数器加 1,溢出时间:4294 秒) 
u8 TIM5CH1_CAPTURE_STA=0; //>>输入捕获状态
u32 TIM5CH1_CAPTURE_VAL;//>>输入捕获值(TIM2/TIM5 是 32 位) 
//>>定时器 5 中断服务程序 
void TIM5_IRQHandler(void) 
{        
    if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//>>捕获 1 发生捕获事件         
    {                 
        TIM5CH1_CAPTURE_VAL  ++;     }        
    }    
    TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //>>清除中断标志位 
}

此部分代码包含两个函数,其中TIM5_CH1_Cap_Init函数用于TIM5通道1的输入捕获设置,其设置和我们上面讲的步骤是一样的,这里就不多说,特别注意:TIM5是32位定时器,所以arr是u32类型的。接下来,重点来看看第二个函数。TIM5_IRQHandler是TIM5的中断服务函数,变量TIM5CH1_CAPTURE_VAL,则用来记录捕获到上升沿的时候,对脉冲进行计数,timer.h头文件内容比较简单,主要是函数申明,这里我们不做过多讲解。接下来,我们看看main函数内容:

extern u8 TIM5CH1_CAPTURE_STA; 
//>>输入捕获状态
extern u32 
TIM5CH1_CAPTURE_VAL;//输入捕获值 
int main(void) 
{ 
    long long temp = 0; 
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//>>设置系统中断优先级分组 2 
    delay_init(168); //初始化延时函数 
    uart_init(115200);//初始化串口波特率为 115200
    TIM14_PWM_Init(500-1,84-1); 
    //>>84M/84=1Mhz 的计数频率计数到 500,频率为 1M/500=2Khz 
    TIM5_CH1_Cap_Init(0XFFFFFFFF,84-1);//>>以 84M/84=1Mhz 的频率计数 
    while(1) 
    { 
        delay_ms(100);
        //>>得到脉冲计数 
        printf("PWM CNT:%d \r\n", TIM5CH1_CAPTURE_VAL);//>>打印脉冲计数
    } 
}

该main函数是在PWM实验的基础上修改来的,我们保留了PWM输出,同时通过设置TIM5_Cap_Init(0XFFFFFFFF,84-1),将TIM5_CH1的捕获计数器设计为1us计数一次,并设置重装载值为最大以达到不让定时器溢出的作用(溢出时间为2^32-1us),所以我们的捕获时间精度为1us。每隔100ms打印一次脉冲计数值。至此,我们的软件设计就完成了。

来源:中科芯MCU

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

围观 33

比较输出简介

比较输出可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形,简称脉宽调制,这也是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制,PWM原理如下图所示:

1.jpg

图中,我们假定定时器工作在向上计数PWM模式,且当CNT<ccrx <="" font="">时,输出0,当CNT>=CCRx时输出1。那么就可以得到如上的 PWM,示意图:当CNT值小于CCRx的时候,IO输出低电平(0),当CNT值大于等于CCRx的时候,IO输出高电平(1),当CNT达到ARR值的时候,重新归零,然后重新向上计数,依次循环。改变CCRx的值,就可以改变PWM输出的占空比,改变ARR的值,就可以改变PWM输出的频率,这就是 PWM输出的原理。

CKS32F4的定时器除了TIM6和7。其他的定时器都可以用来产生PWM输出。其中高级定时器TIM1和TIM8可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出。这里仅使用TIM14的CH1产生一路PWM输出。要使CKS32F4的通用定时器TIMx 产生PWM输出,除了上一章介绍的寄存器外,我们还会用到3个寄存器,来控制PWM的。这三个寄存器分别是:

捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。对于捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2个:TIMx _CCMR1和TIMx _CCMR2,不过TIM14只有一个。TIMx_CCMR1控制CH1和2,而TIMx_CCMR2控制CH3和4。以下我们将以TIM14为例进行介绍。TIM14_CCMR1寄存器各位描述如下图所示:

2.png

该寄存器的有些位在不同模式下,功能不一样,所以在图中,我们把寄存器分了2层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《CKS32F4xx

中文参考手册》这里我们需要说明的是模式设置位 OC1M,此部分由3位组成。总共可以配置成7种模式,我们使用的是PWM模式,所以这3位必须设置为110/111。

这两种PWM模式的区别就是输出电平的极性相反。另外CC1S用于设置通道的方向(输入/输 出)默认设置为0,就是设置通道作为输出使用。注意:这里是因为我们的TIM14只有1个通道,所以才只有第八位有效,高八位无效,其他有多个通道的定时器,高八位也是有效的,具体请参考《CKS32F4xvx 中文参考手册》对应定时器的寄存器描述。

接下来,我们介绍TIM14的捕获/比较使能寄存器(TIM14_CCER),该寄存器控制着各个输入输出通道的开关。该寄存器的各位描述如下图所示:

3.png

该寄存器比较简单,我们这里只用到了CC1E位,该位是输入/捕获1输出使能位,要想PWM从IO口输出,这个位必须设置为1,所以我们需要设置该位为1。因为TIM14只有1个通道,所以才只有低四位有效,如果是其他定时器,该寄存器的其他位也可能有效。

最后,我们介绍一下捕获/比较寄存器(TIMx_CCR1~4),该寄存器总共有4个,对应4 个通道CH1~4。不过TIM14只有一个,即:TIM14_CCR1,该寄存器的各位描述如下图所示:

4.png

在输出模式下,该寄存器的值与CNT的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。

如果是通用定时器,则配置以上三个寄存器就够了,但是如果是高级定时器,则还需要配置:刹车和死区寄存器(TIMx_BDTR),该寄存器各位描述如下图所示:

5.png

该寄存器,我们只需要关注最高位:MOE位,要想高级定时器的PWM正常输出,则必须设置MOE位为1,否则不会有输出。注意:通用定时器不需要配置这个。其他位我们这里就不详细介绍了。

PWM实际跟上一章节一样使用的是定时器的功能,所以相关的函数设置同样在库函数文件 CKS32f4xx_tim.h和CKS32f4xx_tim.c文件中。

1)开启TIM14和GPIO时钟,配置PF9选择复用功能AF9(TIM14)输出

要使用TIM14,我们必须先开启TIM14的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置PF9为复用(AF9)输出,才可以实现TIM14_CH1的PWM经过PF9输出。库函数使能 TIM14时钟的方法是:

6.png

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14,ENABLE); //>>TIM14 时钟使能

这在前面章节已经提到过。当然,这里我们还要使能GPIOF的时钟。然后我们要配置PF9引脚映射至AF9,复用为定时器14,调用的函数为:

GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); //>>GPIOF9 复用为定时器 14

这个方法跟我们串口实验讲解一样,调用的同一个函数,最后设置PF9为复用功能输出这里我们只列出GPIO初始化为复用功能的一行代码:

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //>>复用功能

这里还需要说明一下,对于定时器通道的引脚关系,大家可以查看CKS32F4对应的数据手册,比如我们PWM实验,我们使用的是定时器14的通道1,对应的引脚PF9可以从数据手册表中查看:

2)初始化TIM14,设置TIM14的ARR和PSC等参数

在开启了TIM14的时钟之后,我们要设置ARR和 PSC两个寄存器的值来控制输出PWM的周期。这在库函数是通过TIM_TimeBaseInit函数实现的,在上一节定时器中断章节我们已经有讲解,这里就不详细讲解,调用的格式为:

TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx

3)设置TIM14_CH1的PWM模式,使能TIM14的 CH1输出

设置TIM14_CH1为PWM模式(默认是冻结的)通过配置TIM14_CCMR1的相关位来控制TIM14_CH1的模式。在库函数中,PWM通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我们使用的是通道1,所以使用的函数是TIM_OC1Init()。

void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

这种初始化格式大家学到这里应该也熟悉了,所以我们直接来看看结构体TIM_OCInitTypeDef的定义:

TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 
typedef struct 
{ 
    uint16_t TIM_OCMode; 
    uint16_t TIM_OutputState; 
    uint16_t TIM_OutputNState; */ 
    uint16_t TIM_Pulse; 
    uint16_t TIM_OCPolarity; 
    uint16_t TIM_OCNPolarity; 
    uint16_t TIM_OCIdleState; 
    uint16_t TIM_OCNIdleState; 
} TIM_OCInitTypeDef;

这里我们讲解一下与我们要求相关的几个成员变量:参数TIM_OCMode设置模式是PWM还是输出比较,这里我们是PWM模式。

参数TIM_OutputState用来设置比较输出使能,也就是使能PWM输出到端口。

参数TIM_OCPolarity用来设置极性是高还是低。其他的参数TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState和 TIM_OCNIdleState是高级定时器才用到的。要实现我们上面提到的场景,方法是:

TIM_OCInitTypeDef TIM_OCInitStructure; 
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //>>选择模式 PWM 
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //>>比较输出使能 
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //>>输出极性低 
TIM_OC1Init(TIM14, &TIM_OCInitStructure); //>>根据指定的参数初始化外设TIM1 4OC1

4)使能TIM14

在完成以上设置了之后,我们需要使能TIM14。使能TIM14的方法前面已经讲解过:

TIM_Cmd(TIM14, ENABLE); //>>使能 TIM14

5)修改TIM14_CCR1来控制占空比

最后,在经过以上设置之后,PWM其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改TIM14_CCR1则可以控制 CH1的输出占空比。在库函数中,修改TIM14_CCR1占空比的函数是:

void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare2);

理所当然,对于其他通道,分别有一个函数名字,函数格式为TIM_SetComparex(x=1,2,3,4)。

通过以上5个步骤,我们就可以控制TIM14的CH1输出PWM波了。这里特别提醒一下大家,高级定时器虽然和通用定时器类似,但是高级定时器要想输出PWM,必须还要设置一个MOE位(TIMx_BDTR 的第15位),以使能主输出,否则不会输出PWM。库函数设置的函数为:

void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState)

代码示例

TIM3时钟来自于APB1域,我们通过APB1总线下的时钟使能函数来使能TIM3的时钟。调用的函数是:

//>>TIM14 PWM 部分初始化 
//>>PWM 输出初始化 
//>>arr:自动重装值 psc:时钟预分频数 
void TIM14_PWM_Init(u32 arr,u32 psc) 
{ 
    GPIO_InitTypeDef GPIO_InitStructure; 
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 
    TIM_OCInitTypeDef TIM_OCInitStructure; 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14,ENABLE);//TIM14 时钟使能 
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能  PORTF 时钟 
    GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); //GF9 复用为 TIM14 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //GPIOF9 
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 
    //速度 50MHz 
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出 
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 
    GPIO_Init(GPIOF,&GPIO_InitStructure); //初始化 PF9 
    TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频 
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式 
    TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值 
    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
    TIM_TimeBaseInit(TIM14,&TIM_TimeBaseStructure);
    //初始化定时器 14 //初始化   TIM14 Channel1 PWM 模式 
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM 调制模式 1 
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性低 
    TIM_OC1Init(TIM14, &TIM_OCInitStructure); //初始化外设 TIM1 4OC1 
    TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable); //使能预装载寄存器 
    TIM_ARRPreloadConfig(TIM14,ENABLE);//ARPE 使能 
    TIM_Cmd(TIM14, ENABLE); //使能 TIM14 
    TIM_SetCompare1(TIM14,300); //>>设置pwm的占空比为300/500 = 60%
}

此部分代码包含了上面介绍的PWM输出设置的前5个步骤。这里我们关于TIM14的设置就不再说了。接下来,我们看看主程序里面的main函数如下:

int main(void) 
{ 
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//>>设置系统中断优先级分组 2 
    delay_init(168); //>>初始化延时函数 
    TIM14_PWM_Init(500-1,84-1); //>>定时器时钟为 84M,分频系数为 84,所以计数频率 
    //>>为 84M/84=1Mhz,重装载值 500,所以 PWM 频率为 1M/500=2Khz. 
    while(1) 
    { 
    } 
}

这里,我们先设置好了NVIC终端优先级,然后初始化延时函数和timer,在timer的初始化参数中我们把PWM的频率设置成2K,将占空比设置成60%,完成PWM输出设置。

来源:中科芯MCU

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

围观 15

这一课我们将介绍CKS32F4XX系列产品的定时器使用,CKS32F4XX的定时器功能十分强大,包含2个高级控制定时器,8个普通定时器,2个基础定时器,以及两个看门狗定时器和一个系统定时器,总共15个定时器之多。关于定时器部分内容的讲解我们将分3个部分展开,本节将介绍定时器的基本特征和定时操作。

1.jpg

1、计数器分辨率:指定时器一个计数周期,例如同样是84Mhz的工作时钟,

对于TIM2,其分辨率的范围为:1*(1000ns/ 84)~(2^32)*(1000ns/84);

对于TIM3,其分辨率的范围为:1*(1000ns/84)~(2^16)*(1000ns/84)。

2、计数器类型:这个参数按照计数的方向来划分:

向上计数指的是从0开始到1,2...直到自己设置的计数上限值N,达到后再次从0开始计数,周而复始;

向下计数指的是从设置的计数上限值N开始到N-1,N-2,...直到0,达到后再次从N开始计数,周而复始;向上向下计数指的是从0,1,2...N,然后再从N,N-1,N-2...0,周而复始。

3、预分频系数:可以通过设置该系数来配置时基,如定时器工作在84Mhz下,配置不分频则一个计数时基为11.9ns,配置成2分频则一个计数时基为23.8ns。

4、产生DMA请求:定时器的更新会发出DMA请求,这是因为在DMA通道中为Timer预留了一个通道。

5、捕获比较通道:捕获就是定时器可以捕捉到通道的上升沿或者下降沿信号,比较就是定时器可以将计数器的值和装载值做比较,关于这部分将会在下后续章节展开。

6、互补输出:互补输出指的是输出的两个通道两个波形完全相反,通常运用在桥式电路中的互补PWM输出,这一部分将在后续章节展开。

7、最大接口时钟和最大工作时钟:定时器的时钟来源是APB,通过APB预分频器的配置,最大工作时钟可以是PCLKx的两倍。

CKS32F4XX定时器的定时操作

定时器的定时操作原理其实很简单,就像家里用的微波炉一样,需要加热食物时,先设定一个加热时间,然后按下开关,开始计时,当达到我们设置的定时时间以后,微波炉就会停止工作,并会有一个声音提示我们,定时时间到了。当然,在完成定时操作之前,必须要对Timer进行一些配置,下面我们以timer3为例,为大家演示。

1、Timer3时钟使能

TIM3时钟来自于APB1域,我们通过APB1总线下的时钟使能函数来使能TIM3的时钟。调用的函数是:

//>>使能 TIM3 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);typedefstruct

2、定时器参数初始化

在库函数中定时器的初始化通过TIM_TimeBaseInit实现的:

void TIM_TimeBaseInit(TIM_TypeDef  *TIMx, 
TIM_TimeBaseInitTypeDef  * TIM_TimeBaseInitStruct);

参数结构体指针,结构体类型为TIM_TimeBaseInitTypeDef,下面是结构体的定义:

typedef struct
{ 
    uint16_t TIM_Prescaler; 
    uint16_t TIM_CounterMode; 
    uint16_t TIM_Period; 
    uint16_t TIM_ClockDivision; 
    uint8_t TIM_RepetitionCounter; 
} TIM_TimeBaseInitTypeDef;typedefstruct

这个结构体一共有5个成员变量,要说明的是,对于通用定时器只有前面四个参数有用,最后参数TIM_RepetitionCounter是高级定时器才有用的,后续章节会详解,在此不赘述。

第一个参数TIM_Prescaler是用来设置分频系数的,对应上表中的预分频系数。

第二个参数TIM_CounterMode是用来设置计数方式,如上表所述,可以设置为向上计数,向下计数方式还有向上\向下计数(中央对齐计数)方式,比较常用的是向上计数TIM_CounterMode_Up和向下计数 TIM_CounterMode_Down。

第三个参数是设置自动重载计数周期值,可以通俗的理解成要定时的次数,这个是根据定时时间和时基做除法换算得到的,比如定时器现在计数1次,时间经过了250ns,要定时100us,那自动重载计数周期值为400。

第四个参数是用来设置时钟分频因子,这个参数与定时器的其他功能有密切,本节操作先按照TIM_CKD_DIV1来配置(不分频)。

针对TIM3初始化范例代码格式:

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 
TIM_TimeBaseStructure.TIM_Period=5000; 
TIM_TimeBaseStructure.TIM_Prescaler=7199; 
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; 
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

3、设置TIM3_DIER允许更新中断

因为我们要达到定时时间到后有一个到时提醒的效果,这就需要用到TIM3的更新中断,在库函数里面定时器中断使能是通过TIM_ITConfig函数来实现的:

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

第一个参数是选择定时器号,取值为 TIM1~TIM17。

第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很多种,包括更新中断TIM_IT_Update,触发中断TIM_IT_Trigger,以及输入捕获中断等等。

第三个参数就很简单了,就是失能还是使能。

例如我们要使能TIM3的更新中断,格式为:

TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE );

4、TIM3中断优先级设置

在定时器中断使能之后,因为要产生中断,必不可少的要设置NVIC(向量中断控制器)来设置中断优先级。关于NVIC_Init函数实现中断优先级的设置请到NVIC章节参考,这里就不重复讲解。

5、使能TIM3

配置好定时器后,不要忘记开启定时器,就像按下微波炉的开关一样,定时器才会进入工作状态,在固件库里面使能定时器的函数是通过TIM_Cmd函数来实现的:

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)

这个函数非常简单,比如我们要使能定时器3,方法为:

//>>使能 TIMx 外设 
TIM_Cmd(TIM3, ENABLE);

6、编写中断服务函数

最后,要编写定时器中断服务函数,类似于听到微波炉结束工作的声音后,我们需要进行把加热的食物取出或者继续加热等操作,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,在处理完中断之后应,该向TIM3_SR的最低位写0,来清除该中断标志,在固件库函数里面,用来读取中断状态寄存器的值判断中断类型的函数是:ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t)

该函数的作用是,判断定时器TIMx的中断类型 TIM_IT是否发生中断。比如,我们要判断定时器3 是否发生更新(溢出)中断,方法为:

if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){} IT_Update) != RESET){}

固件库中清除中断标志位的函数是:

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)

该函数的作用是,清除定时器TIMx的中断TIM_IT 标志位。使用起来非常简单,比如我们在TIM3的溢出中断发生后,我们要清除中断标志位,方法是:

TIM_ClearITPendingBit(TIM3, TIM_IT_Update );

这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标

志位的函数TIM_GetFlagStatus 和TIM_ClearFlag,他们的作用和前面两个函数的作用类似。只 是在TIM_GetITStatus函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而TIM_GetFlagStatus 直接用来判断状态标志位。通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来实现定时并产生定时中断信号。

代码实例

/**通用定时器3中断初始化

>>arr:自动重装值。psc:时钟预分频数

>>定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.

>>Ft=定时器工作频率,单位:Mhz

这里使用的是定时器3**/

void TIM3_Int_Init(u16 arr,u16 psc) 
{ 
    TIM_TimeBaseInitTypeDef   TIM_TimeBaseInitStructure; 
    NVIC_InitTypeDef NVIC_InitStructure; 
    //>> ①使能 TIM3 时钟 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); 
    //>>自动重装载值 
    TIM_TimeBaseInitStructure.TIM_Period = arr;
    //>>定时器分频 
    TIM_TimeBaseInitStructure.TIM_Prescaler=psc; 
    //>>向上计数模式
    TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;  
    TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
    // ②初始化定时器 TIM3
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
    //③允许定时器 3 更新中断 
    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); 
    //定时器 3 中断  
    NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; 
    //抢占优先级 1
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;  
    //响应优先级 3 
    NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; 
    // ④初始化 NVIC 
    NVIC_Init(&NVIC_InitStructure);
    //⑤使能定时器 3
    TIM_Cmd(TIM3,ENABLE);  
    } 
    //⑥定时器 3 中断服务函数 
    void TIM3_IRQHandler(void) 
    { 
        //>>溢出中断 
        if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) 
        { 
        } 
        //>>清除中断标志位 
        TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
    }

这里列出了一个中断服务函数和一个定时器3中断初始化函数,中断服务函数中,在每次中断后,判断 TIM3的中断类型,如果中断类型正确,则执行自己需要执行的操作并清除中断标志,TIM3_Int_Init 函数就是执行我们上面介绍的那5个步骤,使得 TIM3开始工作,并开启中断。这里我们分别用标号①~⑤来标注定时器初始化的五个步骤。该函数的2 个参数用来设置TIM3的溢出时间。假设系统初始化 SystemInit函数里面已经初始化APB1的时钟为 4分频,所以APB1的时钟为42M,这也是timer3的最大接口时钟,而从CKS32F4的内部时钟树图得知:当APB1的时钟分频数为1的时候,TIM2~7以及TIM12~14的时钟为APB1的时钟,而如果APB1的时钟分频数不为1,那么TIM2~7以及TIM12~14的时钟频率将为APB1时钟的两倍。因此,TIM3的时钟为84M,再根据我们设计的arr和psc的值,就可以计算中断时间了。计算公式如下:

Tout=((arr+1)*(psc+1))/Tclk;

其中:

Tclk:TIM3的输入时钟频率(单位为Mhz)。

Tout:TIM3溢出时间(单位为us)。

本节我们介绍了CKS32F4XX各定时器概况,以及如何设置最基础的定时功能,包括开启定时器的时钟,配置定时器的时基,定时次数以及计数的方向等,此外定时器其他的功能如输入捕获,比较输出和PWM输出等功能将会在后续章节展开。

来源:中科芯MCU

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

围观 38

EXTI 简介

EXTI(External interrupt/event controller)—外部中断/事件控制器,管理了控制器的 23个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。EXTI可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。

EXTI 功能框图

EXTI的功能框图包含了 EXTI最核心内容,EXTI可分为两大部分功能,一个是产生中断,另一个是产生事件。EXTI功能框图如下:

1.png

中断/事件线

EXTI有23个中断/事件线,每个GPIO都可以被设置为输入线,占用EXTI0至EXTI15,还有另外七根用于特定的外设事件。

2.png

EXTI0至EXTI15用于GPIO,通过编程控制可以实现任意一个GPIO作为 EXTI的输入源。由上图可知,EXTI0可以通过AFIO的外部中断配置寄存器 1(AFIO_EXTICR1)的EXTI0[3:0]位选择配置为PA0、PB0、PC0、PD0、PE0、PF0、PG0、PH0 或者PI0,其他EXTI线(EXTI中断/事件线)使用配置都是类似的。

外设事件连接模式:

EXTI line 16 连接PVD输出

EXTI line 17 连接RTC闹钟事件

EXTI line 18 连接USB OTG FS唤醒事件

EXTI line 19 连接以太网唤醒事件

EXTI line 20 连接USB OTG HS唤醒事件

EXTI line 21 连接RTC 篡改和时标事件

EXTI line 22 RTC唤醒事件

EXTI 初始化结构体

标准库函数对每个外设都建立了一个初始化结构体,比如 EXTI_InitTypeDef,结构体成员用于设置外设工作参数,并由外设初始化配置函数,比如 EXTI_Init()调用,这些设定参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。

初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 cks32f4xx_exti.h 文件中,初始化库函数定义在 cks32f4xx_exti.c 文件中,编程时我们可以结合这两个文件内注释使用。

EXTI操作流程

a)首先配置GPIO引脚模式:

RCC_APB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); 
/* 配置GPIOA.0 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
/* 配置为输入模式 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
/* 配置速率为高速模式 */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
/* 使能下拉模式 */
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;
/* 调用初始化函数 */
GPIO_Init(GPIOA, &GPIO_InitStructure);typedefstruct

b)配置EXTI line0中断线连接GPIOA.0:

typedefstrucSYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, GPIO_PinSource0);

c)配置中断线为中断模式,上升沿触发模式并使能:

EXTI_InitTypeDef  EXTI_Structure;
EXTI_StructInit(&EXTI_Structure); 
/*  配置中断线0 */
EXTI_Structure.EXTI_Line = EXTI_Line0;      
/* 使用中断模式 */
EXTI_Structure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 电压低于阀值时产生中断 */     
EXTI_Structure.EXTI_Trigger = EXTI_Trigger_Raising;  
/* 使能中断线  */
EXTI_Structure.EXTI_LineCmd = ENABLE;   
/* 初始化中断  */
EXTI_Init(&EXTI_Structure);

d)配置中断管理器NVIC,设置EXTI0_IRQn中断源优先级并初始化:

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

e)当产生 EXTI0中断,在中断程序中进行相应的处理:

typvoid EXTI0_IRQHandler(void)
{  
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)  
    {            
        EXTI_ClearITPendingBit(EXTI_Line0);
        /* 添加用户处理代码 */  
     }
}  edefstruct

来源:中科芯MCU

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

围观 15

SPI协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB在布局上节省了空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,它被广泛地使用在ADC、LCD、FLASH等设备与MCU之间的通信。

CKS32F4xx系列产品SPI介绍

CKS32F4xx系列的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟频率为fpclk/2(CKS32F407型号的芯片默认fpclk为142MHz,fpclk2为84MHz),完全支持SPI协议的4种模式。SPI协议根据CPOL及CPHA的不同状态分成的四种工作模式如下表所示:

1.jpg

CKS32F4xx系列的SPI架构如下图所示:

2.png

图中的1处是SPI的引脚MOSI、MISO、SCK、NSS。CKS32F4xx芯片有多个 SPI外设,它们的SPI通讯信号引出到不同GPIO引脚上,使用时必须配置到这些指定的引脚。关于GPIO引脚的复用功能可以查阅芯片数据手册。各个引脚的作用介绍如下:

(1)NSS:从设备选择信号线,常称为片选信号线。当有多个SPI从设备与 SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI 总线上,即无论有多少个从设备,都共同只使用这3条总线;而每个从设备都有独立的一条NSS信号线,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。

(2)SCK:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。

(3)MOSI:主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

(4)MISO:主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

图中的2处是SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率。

图中的3处是SPI的数据控制逻辑。SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及MISO、MOSI线。当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写SPI 的“数据寄存器DR”把数据填充到发送缓冲区中,通过“数据寄存器DR”,可以获取接收缓冲区中的内容。其中数据帧的长度可以通过“控制寄存器CR1”的“DFF位”配置成8位及16位模式;配置“LSBFIRST位”可选择MSB先行还是 LSB先行。

图中的4处是SPI的整体控制逻辑。整体控制逻辑负责协调整个SPI外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括SPI模式、波特率、LSB先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解SPI的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。实际应用中,我们一般不使用CKS32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

CKS32F4xx系列的SPI作为通讯主机端时收发数据的过程如下:

(1) 控制NSS信号线,产生起始信号;

(2) 把要发送的数据写入到“数据寄存器DR”中,该数据会被存储到发送缓冲区;

(3) 通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中;

(4) 当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;

(5) 等待到“TXE标志位”为1时,若还要继续发送数据,则再次往“数据寄存器DR”写入数据即可;等待到“RXNE标志位”为1时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。

假如我们使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发“数据寄存器 DR”中的数据。

CKS32F4xx系列产品SPI的配置

接下来我们讲解如何利用CKS32F4xx系列固件库来完成对SPI的配置使用。跟其它外设一样,CKS32标准库提供了SPI初始化结构体及初始化函数来配置 SPI外设。了解初始化结构体后我们就能对SPI外设运用自如了,代码如下:

typedef struct
{  
    uint16_t SPI_Direction;            
    uint16_t SPI_Mode;                
    uint16_t SPI_DataSize;            
    uint16_t SPI_CPOL;               
    uint16_t SPI_CPHA;                 
    uint16_t SPI_NSS;                 
    uint16_t SPI_BaudRatePrescaler;    
    uint16_t SPI_FirstBit;             
    uint16_t SPI_CRCPolynomial;       
}SPI_InitTypeDef;

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

1) SPI_Direction:本成员设置SPI的通讯方向,可设置为双线全双工 (SPI_Direction_2Lines_FullDuplex),双线只接收 (SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。

2) SPI_Mode:本成员设置SPI工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这两个模式的最大区别为SPI的SCK信号线的时序,SCK的时序是由通讯中的主机产生的。若被配置为从机模式,CKS32的SPI外设将接受外来的SCK信号:

3) SPI_DataSize: 本成员可以选择SPI通讯的数据帧大小是为8位 (SPI_DataSize_8b)还是16位(SPI_DataSize_16b)。

4) SPI_CPOL和SPI_CPHA: 这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,前面讲过这两个配置影响到SPI的通讯模式。时钟极性CPOL成员可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。时钟相位CPHA则 可以设置为SPI_CPHA_1Edge(在SCK的奇数边沿采集数据)或 SPI_CPHA_2Edge(在SCK的偶数边沿采集数据)。

5) SPI_NSS: 本成员配置NSS引脚的使用模式,可以选择为硬件模式 (SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的SPI片选信号由 SPI硬件自动产生,而软件模式则需要我们自己把相应的GPIO端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。

6) SPI_BaudRatePrescaler: 本成员设置波特率分频因子,分频后的时钟即为SPI的SCK信号线的时钟频率。这个成员参数可设置为fpclk的2、4、6、8、16、32、64、128、256分频。可选的值如下所示:

SPI_BaudRatePrescaler_2    //2分频
SPI_BaudRatePrescaler_4    //4分频
SPI_BaudRatePrescaler_6    //6分频
SPI_BaudRatePrescaler_8    //8分频
SPI_BaudRatePrescaler_16   //16分频
SPI_BaudRatePrescaler_32   //32分频
SPI_BaudRatePrescaler_64   //64分频
SPI_BaudRatePrescaler_128  //128分频
SPI_BaudRatePrescaler_256  //256分频

7) SPI_FirstBit: 所有串行的通讯协议都会有MSB先行(高位数据在前)还是 LSB先行(低位数据在前)的问题,而CKS32F4xx系列的SPI模块可以通过这个结构体成员,对这个特性编程控制。

SPI_FirstBit_MSB    //高位数据在前
SPI_FirstBit_LSB     //低位数据在前

8) SPI_CRCPolynomial: 这是SPI的CRC校验中的多项式,若我们使用CRC 校验时,就使用这个成员的参数(多项式),来计算CRC的值。

配置完这些结构体成员的值,调用库函数SPI_Init即可把结构体的配置写入到寄存器中。

CKS32F4xx读写SPI FLASH实验

串口的DMA接发通信实验是存储器到外设和外设到存储器的数据传输。在第24

本小节以一种使用SPI通讯的串行FLASH存储芯片的读写实验为大家讲解 CKS32F4xx系列的SPI使用方法。实验中的FLASH芯片(型号:W25Q32)是一种使用SPI通讯协议的NORFLASH存储器,它的CS/CLK/DIO/DO引脚分别连接到了CKS32F4xx对应的SPI引脚NSS/SCK/MOSI/MISO上,其中CKS32F4xx的NSS引脚是一个普通的GPIO,不是SPI的专用NSS引脚,所以程序中我们要使用软件控制的方式。

1.编程要点

(1) 初始化通讯使用的目标引脚及端口时钟;  

(2) 使能SPI外设的时钟;

(3) 配置SPI外设的模式、地址、速率等参数并使能SPI外设;

(4) 编写基本SPI按字节收发的函数; 

(5) 编写对FLASH擦除及读写操作的的函数; 

(6) 编写测试程序,对读写数据进行校验。 

2.代码分析

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

void W25QXX_Init(void)
{   
    GPIO_InitTypeDef  GPIO_InitStructure;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);   
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);   
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;  
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;   
    GPIO_Init(GPIOD, &GPIO_InitStructure); 
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;  
    GPIO_Init(GPIOG, &GPIO_InitStructure);
    GPIO_SetBits(GPIOG,GPIO_Pin_3);   
    W25QXX_CS=1;                        //SPI FLASH不选中  
    SPI1_Init();                 //初始化SPI  
    SPI1_SetSpeed(SPI_BaudRatePrescaler_4);    //设置为21M时钟  
    W25QXX_TYPE=W25QXX_ReadID();         //读取FLASH ID.
}

上面的代码主要是完成对W25Q32片选引脚的初始化,SPI初始化。SPI通信速率设置和读取W25Q32的ID。

代码清单2:SPI初始化函数

void SPI1_Init(void)
{     
    GPIO_InitTypeDef  GPIO_InitStructure;  
    SPI_InitTypeDef  SPI_InitStructure;  
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;  
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;  
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;   
    GPIO_Init(GPIOB, &GPIO_InitStructure); 
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1);
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); 
    GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1);   
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE);  
    RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE);  
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;   
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;      
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;    
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;      
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;   
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;      
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;    
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    
    SPI_InitStructure.SPI_CRCPolynomial = 7;    
    SPI_Init(SPI1, &SPI_InitStructure);    
    SPI_Cmd(SPI1, ENABLE);   
    SPI1_ReadWriteByte(0xff);   
}

上面这段代码主要是完成对SPI1的初始化,首先是配置了SPI1使用的引脚SPI1_SCK、SPI1_MOSI和SPI1_MISO。然后是根据第2小节的内容完成对SPI1外设模式的配置。根据FLASH芯片W25Q32的说明,它支持SPI模式0及模式3,支持双线全双工,使用MSB先行模式,支持最高通讯时钟为104MHz,数据帧长度为8位。我们要把CKS32F4的SPI外设中的这些参数配置一致。

代码清单3:SPI1单字节收发函数

u8 SPI1_ReadWriteByte(u8 TxData)
{              
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){}   
    SPI_I2S_SendData(SPI1, TxData);   
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){}   
    return SPI_I2S_ReceiveData(SPI1); 
}

本函数中不包含SPI起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作。通过检测TXE标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;等待至发送缓冲区为空后,调用库函数SPI_I2S_SendData把要发送的数据“TxData”写入到SPI的数据寄存器DR,写入SPI数据寄存器的数据会存储到发送缓冲区,由SPI外设发送出去;写入完毕后等待RXNE事件,即接收缓冲区非空事件。由于SPI双线全双工模式下MOSI与MISO数据传输是同步的,当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;等待至接收缓冲区非空时,通过调用库函数SPI_I2S_ReceiveData读取SPI的数据寄存器DR,就可以获取接收缓冲区中的新数据了。代码中使用关键字“return”把接收到的这个数据作为SPI1_ReadWriteByte函数的返回值。

搞定了SPI的基本收发单元后,还需要了解如何对FLASH芯片进行读写。FLASH 芯片自定义了很多指令,我们通过控制CKS32F4利用SPI总线向FLASH 芯片发送指令,FLASH芯片收到后就会执行相应的操作。具体的指令代码可以查看W25Q32芯片的数据手册。

代码清单4:读取FLASH芯片ID函数

u16 W25QXX_ReadID(void)
{  
    u16 Temp = 0;      
    W25QXX_CS=0;              
    SPI1_ReadWriteByte(0x90);        
    SPI1_ReadWriteByte(0x00);         
    SPI1_ReadWriteByte(0x00);         
    SPI1_ReadWriteByte(0x00);               
    Temp|=SPI1_ReadWriteByte(0xFF)<<8;    
    Temp|=SPI1_ReadWriteByte(0xFF);     
    W25QXX_CS=1;              
    return Temp;
}

这段代码利用控制CS引脚电平的宏“W25QXX_CS”以及前面编写的单字节收发函数SPI1_ReadWriteByte,很清晰地实现了读ID指令的时序,最后把读 取到的这3个数据合并到一个变量Temp中,然后作为函数返回值,把该返回值与我们定义的芯片ID对比,即可知道FLASH芯片是否正常。

代码清单5:W25Q32写使能和写禁止函数

void W25QXX_Write_Enable(void)   
{  
    W25QXX_CS=0;                                
    SPI1_ReadWriteByte(W25X_WriteEnable);      
    W25QXX_CS=1;                                    
}
void W25QXX_Write_Disable(void)   
{    
    W25QXX_CS=0;                              
    SPI1_ReadWriteByte(W25X_WriteDisable);          
    W25QXX_CS=1;                                      
}

由于FLASH存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”, 那就不修改存储矩阵,在要存储数据“0”时,需要更改该位。W25Q32支持“扇区擦除”、“块擦除”以及“整片擦除”。 扇区擦除指令的第一个字节为指令编码,紧接着发送的3个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。

代码清单6:W25Q32扇区擦除函数

void W25QXX_Erase_Sector(u32 Dst_Addr)   
{         
    Dst_Addr*=4096;    
    W25QXX_Write_Enable();                        
    W25QXX_Wait_Busy();       
    W25QXX_CS=0;                                  
    SPI1_ReadWriteByte(W25X_SectorErase);          
    SPI1_ReadWriteByte((u8)((Dst_Addr)>>16));         
    SPI1_ReadWriteByte((u8)((Dst_Addr)>>8));       
    SPI1_ReadWriteByte((u8)Dst_Addr);    
    W25QXX_CS=1;                                       
    W25QXX_Wait_Busy();             
}

目标扇区被擦除完毕后,就可以向它写入数据了。与EEPROM类似,FLASH芯片也有页写入命令,使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位称为页大小。在进行页写入时第1个字节为“页写入指令”编码,2-4字节为要写入的“地址A”,接着的是要写入的内容,最多可以发送 256字节数据,这些数据将会从“地址A”开始,按顺序写入到FLASH的存储矩阵。若发送的数据超出256个,则会覆盖前面发送的数据。

代码清单7:W25Q32页写入函数

void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{   
    u16 i;      
    W25QXX_Write_Enable();                   
    W25QXX_CS=0;                                   
    SPI1_ReadWriteByte(W25X_PageProgram);             
    SPI1_ReadWriteByte((u8)((WriteAddr)>>16));       
    SPI1_ReadWriteByte((u8)((WriteAddr)>>8));       
    SPI1_ReadWriteByte((u8)WriteAddr);       
    for(i=0;i<NumByteToWrite;i++)SPI1_ReadWriteByte(pBuffer[i]);   
    W25QXX_CS=1;                              
    W25QXX_Wait_Busy();            
}

应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便,所以我们页写入函数的基础上编写了“不定量数据写入”的函数。在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除状态

代码清单8:W25Q32不定量数据写入函数

void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{   
    u32 secpos;  
    u16 secoff;  
    u16 secremain;        
    u16 i;      
    u8 * W25QXX_BUF;         
    W25QXX_BUF=W25QXX_BUFFER;          
    secpos=WriteAddr/4096;   
    secoff=WriteAddr%4096;   
    secremain=4096-secoff;     
    if(NumByteToWrite<=secremain)secremain=NumByteToWrite;  
    while(1)   
    {      
        W25QXX_Read(W25QXX_BUF,secpos*4096,4096);    
        for(i=0;i<secremain;i++)     
        {      
            if(W25QXX_BUF[secoff+i]!=0XFF)break;         
        }    
        if(i<secremain)     
        {      
            W25QXX_Erase_Sector(secpos);       
            for(i=0;i<secremain;i++)         
            {    
                W25QXX_BUF[i+secoff]=pBuffer[i];          
            }      
            W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);     
        }else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);     
        if(NumByteToWrite==secremain)break;     
        else     
        {      
            secpos++;      
            secoff=0;            
            pBuffer+=secremain;        
            WriteAddr+=secremain;              
            NumByteToWrite-=secremain;              
            if(NumByteToWrite>4096)secremain=4096;        
            else secremain=NumByteToWrite;          
        }     
    };   
}

函数的入口参数pBuffer是数据存储区、WriteAd是开始写入的地址(24bit)、NumByteToWrite是要写入的字节数(最大65535)gaojp。

相对于写入,FLASH芯片W25Q32的数据读取要简单的多,发送了指令编码及要读的起始地址和要读取的字节数之后,FLASH 芯片W25Q32就会按地址递增的方式返回存储矩阵中一定字节数量的数据。

代码清单9:W25Q32读取数据函数

void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   
{    
    u16 i;                             
    W25QXX_CS=0;                                  
    SPI1_ReadWriteByte(W25X_ReadData);              
    SPI1_ReadWriteByte((u8)((ReadAddr)>>16));         
    SPI1_ReadWriteByte((u8)((ReadAddr)>>8));       
    SPI1_ReadWriteByte((u8)ReadAddr);       
    for(i=0;i<NumByteToRead;i++)  
    {         
        pBuffer[i]=SPI1_ReadWriteByte(0XFF);   //循环读数      
    }  
    W25QXX_CS=1;                      
}

函数的入口参数pBuffer是数据存储区、ReadAddr是开始读取的地址(24bit)、NumByteToRead是要读取的字节数(最大65535)。

完成基本的读写函数后,接下来我们编写一个读写测试函数来检验驱动程。

代码清单10:W25Q32读写测试函数

uint8_t w25q32_Test(void)
{  
    u16 i;   
    printf("写入的数据:\r\n");  
    for ( i=0; i<=10; i++ )   
    {       
        spi_Buf_Write[i] = i;    
        printf("0x%02X ", spi_Buf_Write[i]);   
    }   
    W25QXX_Write((u8*)spi_Buf_Write,FLASH_SIZE-100,11);          
    printf("写成功,");     
    printf("读出的数据:\r\n");    
    W25QXX_Read(datatemp,FLASH_SIZE-100,11);            
    for (i=0; i<11; i++)  
    {      
        if(datatemp[i] != spi_Buf_Write[i])    
        {      
            printf("0x%02X ", datatemp[i]);      
            printf("错误:I2C EEPROM写入与读出的数据不一致");     
            return 0;    
        }    
        printf("0x%02X ", datatemp[i]);  
    }  
    printf("\r\n");    
    printf("spi(w25q32)读写测试成功");  
    return 1;
}

代码中先填充一个数组,数组的内容为0,1至10,接着把这个数组的内容写入到SPI FLASH中,并将写入的数据打印输出到串口调试助手。写入完毕后再从SPI FLASH的地址中读取数据,把读取到的数据与写入的数据进行校验,若一致说明读写正常,否则读写过程有问题或者SPI FLASH芯片不正常,然后再将读取到的数据打印输出到串口调试助手。

代码清单11:主函数

int main(void)
{     
    u16 id = 0;    
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  
    delay_init(168);       
    USART_Configuration();  
    W25QXX_Init();        
    while(1)  
    {    
        id = W25QXX_ReadID();    
        if (id == W25Q32 || id == NM25Q32)      
            break;     
            printf("W25Q32 init failed\r\n");    
        delay_ms(500);    
        delay_ms(500);  
    }   
    printf("W25Q32 init success\r\n");   
    w25q32_Test(); 
     while(1)  
     {   
     }       
 }

主函数代码比较简单,主要是完成串口初始化和W25Q32的初始化,初始化完成之后会执行W25QXX_ReadID函数,读取W25Q32的ID,同时对ID进行判断,并将结果通过串口调试助手打印输出。然后会执行一次W25Q32测试函数,并将一些测试结果通过串口调试助手打印输出。

来源:中科芯MCU

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

围观 29

页面

订阅 RSS - CKS32F4xx