如何更好地学习STM32?——掌握正点原子入门篇例程的半日学习经验分享

本文代码均来正点原子标准例程
声明:本文不是教学文章,可能也不适合初学者阅读

不知为什么,最近总蹦出有很多想法(可能是工作太闲了)一会想学这,一会想学那,这不,突然想复习一下STM32了。

我好久以前就学过正点原子的课程,还买过一些开发板,但现在手上只有一个核心板了,就暂且凑合着用吧。

我是个喜欢制定计划的人,既然有了想法,那就得制定一个学习计划,估摸了一下,明天要上班,现在已经中午了,所以我只有一个下午加一个晚上的时间。哎😢,工作之后发现学习的时间太少了,所以,既然是复习,那就不搞那么多弯弯绕绕了,直接针对正点原子的代码,通过代码学习STM32,那些啥原理的,通通给我抛到九霄云外去,以后有机会慢慢整。

文章目录

  • 开发平台
  • 实验1 跑马灯实验
  • main()
  • delay_init() 函数
  • SysTick_CLKSourceConfig(…)
  • LED_Init() 函数
  • RCC_APB2PeriphClockCmd(…)
  • GPIO_InitTypeDef
  • delay_ms(…)函数
  • 位操作
  • 实验2 按键输入
  • main()
  • KEY_Init()函数
  • BEEP_Init()函数
  • KEY_Scan(…)函数
  • 实验3 串口实验
  • mian()
  • NVIC_PriorityGroupConfig
  • uart_init(…)函数
  • 串口处理全局变量
  • 重写printf(fputc)
  • USART1_IRQHandler串口中断函数
  • 实验4 外部中断实验
  • main()
  • EXTIX_Init()函数
  • EXTIx_IRQHandler外部中断函数
  • 实验5 独立看门狗实验
  • main()
  • IWDG_Init(…)函数
  • IWDG_Feed()函数
  • 实验6 窗口看门狗实验[待学习]
  • main()
  • WWDG_Init(…)函数
  • 实验7 定时器中断实验
  • main()
  • TIM3_Int_Init(…)函数
  • TIMx_IRQHandler()定时器中断函数
  • 实验8 PWM输出实验
  • main()
  • TIM3_PWM_Init(…)函数
  • 实验9 输入捕获实验[待学习]
  • main()
  • TIM5_Cap_Init(…)函数
  • TIM5_IRQHandler() 处理输入捕获
  • 实验10 TFTLCD显示实验[放弃]
  • 开发平台

    话不多说,开始整活,先准备一下硬件:

    就一个核心板,太寒酸了,还好有个屏幕撑撑场面。核心板的MCU型号为STM32F103ZET6。

    有了硬件,就差代码了。
    下图是正点原子的入门篇视频,我就按照这个顺序来学一遍(没有硬件支持的话,就只能跳过了,如OLED),寄存器版的就不考虑了,太麻烦。

    虽然从教学视频的目录上看感觉实验多得有些吓人,但打开工程文件夹一看,嘿嘿,舒服了。😁,这么一点,一下午就能搞完。

    就在我窃喜的时候,看了一眼时间,时间不多了,抓紧了🚐。。。

    实验1 跑马灯实验

    main()

    光看主函数,觉得他和51一样简单,就是初始化和设置GPIO的高低,但实际上它们有本质区别,毕竟一个是8位,一个是32位。下面我们来一行行地分析吧。

    
    int main(void)
    { 
     
    	delay_init();		  //初始化延时函数
    	LED_Init();		        //初始化LED端口
    	while(1)
    	{
    			GPIO_ResetBits(GPIOB,GPIO_Pin_5);  //LED0对应引脚GPIOB.5拉低,亮  等同LED0=0;
    			GPIO_SetBits(GPIOE,GPIO_Pin_5);   //LED1对应引脚GPIOE.5拉高,灭 等同LED1=1;
    			delay_ms(300);  		   //延时300ms
    			GPIO_SetBits(GPIOB,GPIO_Pin_5);	   //LED0对应引脚GPIOB.5拉高,灭  等同LED0=1;
    			GPIO_ResetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉低,亮 等同LED1=0;
    			delay_ms(300);                     //延时300ms
    	}
    } 
    

    delay_init() 函数

    //初始化延迟函数
    //SYSTICK的时钟固定为HCLK时钟的1/8
    //SYSCLK:系统时钟
    void delay_init()
    {
    	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);	//选择外部时钟  HCLK/8
    	fac_us=SystemCoreClock/8000000;				//为系统时钟的1/8  
    	fac_ms=(u16)fac_us*1000;					//非OS下,代表每个ms需要的systick时钟数   
    }								    
    

    第一个函数delay_init(),不像51里直接用一个while实现延时,这里的延时由滴答定时器实现。Systick定时器就是系统滴答定时器,一个24 位的倒计数定时器,计到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。
    SysTick_CLKSourceConfig是一个库函数,作用是配置滴答定时器的时钟源。

    STM32 有5个时钟源:HSI、HSE、LSI、LSE、PLL。
    ①、HSI是高速内部时钟,RC振荡器,频率为8MHz,精度不高。
    ②、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
    ③、LSI是低速内部时钟,RC振荡器,频率为40kHz,提供低功耗时钟。WDG
    ④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。RTC
    ⑤、PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。
    倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。

    STM32时钟源的知识还是挺多的,我自己现在也不是很清楚(得专门抽空学学),但我知道如果没有做配置,系统默认时钟频率是最高频率——本平台为72MHz
    system_stm32f10x.c里有以下内容,先记录一下,以后再分析。

    #if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
    /* #define SYSCLK_FREQ_HSE    HSE_VALUE */
     #define SYSCLK_FREQ_24MHz  24000000
    #else
    /* #define SYSCLK_FREQ_HSE    HSE_VALUE */
    /* #define SYSCLK_FREQ_24MHz  24000000 */ 
    /* #define SYSCLK_FREQ_36MHz  36000000 */
    /* #define SYSCLK_FREQ_48MHz  48000000 */
    /* #define SYSCLK_FREQ_56MHz  56000000 */
    #define SYSCLK_FREQ_72MHz  72000000
    #endif
    

    SysTick_CLKSourceConfig(…)

    下面看看滴答定时器时钟源配置的库函数源码,可以看出它的时钟源只能为SysTick_CLKSource_HCLK_Div8SysTick_CLKSource_HCLK,那么问题来了,什么是HCLK:

    HCLK :AHB总线时钟,由系统时钟SYSCLK 分频得到,一般不分频,等于系统时钟

    刚刚提到系统时钟为72M,所以SysTick_CLKSource_HCLK_Div8 就是72/8=9M。

    /**
      * @brief  Configures the SysTick clock source.
      * @param  SysTick_CLKSource: specifies the SysTick clock source.
      *   This parameter can be one of the following values:
      *     @arg SysTick_CLKSource_HCLK_Div8: AHB clock divided by 8 selected as SysTick clock source.
      *     @arg SysTick_CLKSource_HCLK: AHB clock selected as SysTick clock source.
      * @retval None
      */
    void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
    {
      /* Check the parameters */
      assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
      if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
      {
        SysTick->CTRL |= SysTick_CLKSource_HCLK;
      }
      else
      {
        SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
      }
    }
    

    配置完了滴答定时器的时钟,delay_init函数内还有两行:

    fac_us=SystemCoreClock/8000000;	//为系统时钟的1/8  
    fac_ms=(u16)fac_us*1000;		//非OS下,代表每个ms需要的systick时钟数   		
    

    fac_us表示微秒的计时因子,即滴答计时器重载值为1*fac_us时,计时时间为1us(可以看后面的delay_us函数),fac_ms为fac_us的1000倍,自然就是1ms了。

  • 那么问题来了,为什么fac_us代表1us呢?
  • 之前我们提到
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8
    即滴答定时器定时器频率为9M(72/8),9M意味着定时器1秒计数9000000,那么1毫秒计数就为9000,1微秒为9。这代表什么?计9次数为1us,这个9就是1微秒的计数因子(fac_us),即fac_us(72000000/8000000=9)代表1us。n微秒则为n * fac_us。

    LED_Init() 函数

    终于到了本实验的主角——LED(GPIO)

    //初始化PB5和PE5为输出口.并使能这两个口的时钟		    
    //LED IO初始化
    void LED_Init(void)
    {
     
     GPIO_InitTypeDef  GPIO_InitStructure;
     	
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);	 //使能PB,PE端口时钟
    	
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;				 //LED0-->PB.5 端口配置
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
     GPIO_Init(GPIOB, &GPIO_InitStructure);					 //根据设定参数初始化GPIOB.5
     GPIO_SetBits(GPIOB,GPIO_Pin_5);						 //PB.5 输出高
    
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	    		 //LED1-->PE.5 端口配置, 推挽输出
     GPIO_Init(GPIOE, &GPIO_InitStructure);	  				 //推挽输出 ,IO口速度为50MHz
     GPIO_SetBits(GPIOE,GPIO_Pin_5); 						 //PE.5 输出高 
    }
    

    概括一下配置GPIO的步骤:

    1. 定义一个GPIO_InitTypeDef 成员
    2. 使能GPIO对应的端口时钟RCC_APB2PeriphClockCmd(...)
    3. 配置引脚GPIO_Pin
    4. 配置模式(输入、输出、推挽、开漏、浮空)GPIO_Mode
    5. 配置IO速度(我练习时一般不太在意这一项)GPIO_Speed
    6. 初始化GPIO_InitTypeDef 成员GPIO_Init(..)
    7. 设置引脚高低状态GPIO_SetBits(..)GPIO_ResetBits(...)

    RCC_APB2PeriphClockCmd(…)

    库函数注释中标明了时钟总线上的外设,GPIOB和GPIOE都在APB2总线上

    /**
      * @brief  Enables or disables the High Speed APB (APB2) peripheral clock.
      * @param  RCC_APB2Periph: specifies the APB2 peripheral to gates its clock.
      *   This parameter can be any combination of the following values:
      *     @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
      *          RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
      *          RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
      *          RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
      *          RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
      *          RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
      *          RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11     
      * @param  NewState: new state of the specified peripheral clock.
      *   This parameter can be: ENABLE or DISABLE.
      * @retval None
      */
    void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
    {
      /* Check the parameters */
      assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
      assert_param(IS_FUNCTIONAL_STATE(NewState));
      if (NewState != DISABLE)
      {
        RCC->APB2ENR |= RCC_APB2Periph;
      }
      else
      {
        RCC->APB2ENR &= ~RCC_APB2Periph;
      }
    }
    

    如果想快速查到某外设的时钟总线,可以参考《STM32中文参考手册》存储器和总线架构章节:

    GPIO_InitTypeDef

    下面是GPIO_InitTypeDef结构体定义

    /** 
      * @brief  GPIO Init structure definition  
      */
    
    typedef struct
    {
      uint16_t GPIO_Pin;             /*!< Specifies the GPIO pins to be configured.
                                          This parameter can be any value of @ref GPIO_pins_define */
    
      GPIOSpeed_TypeDef GPIO_Speed;  /*!< Specifies the speed for the selected pins.
                                          This parameter can be a value of @ref GPIOSpeed_TypeDef */
    
      GPIOMode_TypeDef GPIO_Mode;    /*!< Specifies the operating mode for the selected pins.
                                          This parameter can be a value of @ref GPIOMode_TypeDef */
    }GPIO_InitTypeDef;
    
    

    LED_init中,LED0GPIO_Pin为GPIOB5,LED1为GPIOE5;
    模式都选择了推挽输出

    推挽输出的最大特点是可以真正能真正的输出高电平和低电平,在两种电平下都具有驱动能力。

    由LED的原理图可以知道它们为共阳极,所以默认要将IO拉高。


    其他细节感兴趣的可以自己去研究😗。

    delay_ms(…)函数

    fac_ms刚刚在延时函数初始化中已经介绍,滴答定时器SysTick每计时fac_ms次,则表示1ms,所以nms*fac_ms表示计时nms毫秒。SysTick->LOAD为定时器的重载值,SysTick->VAL表示计数值,还要注意:滴答定时器是倒数计数的。SysTick->CTRL为控制寄存器,第16位可以用来检测是否倒数到0。

    void delay_ms(u16 nms)
    {	 		  	  
    	u32 temp;		   
    	SysTick->LOAD=(u32)nms*fac_ms;				//时间加载(SysTick->LOAD为24bit)
    	SysTick->VAL =0x00;							//清空计数器
    	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//开始倒数  
    	do
    	{
    		temp=SysTick->CTRL;
    	}while((temp&0x01)&&!(temp&(1<<16)));		//等待时间到达   
    	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
    	SysTick->VAL =0X00;       					//清空计数器	  	    
    } 
    

    位操作

    对于操作寄存器,经常要用到位操作,如SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk中,SysTick_CTRL_ENABLE_Msk表示1,SysTick->CTRL|=1的作用是将CTRL寄存器的最低位置1,而不影响其他高19位(0或任何二进制数,都会是它自己);
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;的作用是将CTRL最低位置0,0x00000001按位取反后为0xfffffffe,该数与任何32位数按位与(&),都不会影响高31位,因为1和任何二进制数进行与运算都等于它自己。


    本来想写位带操作的,但看了看时间,就放弃了


    GPIO引脚控制函数就不提了,之前在LED_Init()函数里已经见过。
    实验效果——红绿灯交替闪烁。

    实验2 按键输入

    main()

    主函数中LED0、LED1和BEEP代表的是GPIO的位段(本文忽略这个概念),把它当做51里对GPIO的位操作就行了。
    与上一个实验相比,本实验多了按键模块和蜂鸣器模块。

    
    int main(void)
     {
     	vu8 key=0;	
    	delay_init();	    	 //延时函数初始化	  
     	LED_Init();			     //LED端口初始化
    	KEY_Init();          	//初始化与按键连接的硬件接口
    	BEEP_Init();         	//初始化蜂鸣器端口
    	LED0=0;					//先点亮红灯
    	while(1)
    	{
     		key=KEY_Scan(0);	//得到键值
    	   	if(key)
    		{						   
    			switch(key)
    			{				 
    				case WKUP_PRES:	//控制LED1翻转	
    					LED1=!LED1;
    					BEEP = !BEEP;
    					break;
    				case KEY0_PRES:	//同时控制LED0翻转 
    					LED0=!LED0;
    					BEEP = !BEEP;
    					break;
    			}
    		}else delay_ms(10); 
    	}	 
    }
    
    

    KEY_Init()函数

    与LED_Init()类似,配置步骤相同(配置步骤见LED_Init()介绍部分)。

    void KEY_Init(void) //IO初始化
    { 
     	GPIO_InitTypeDef GPIO_InitStructure;
     
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE时钟
    
    	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4;//KEY0
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //设置成下拉输入
     	GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
    
    	//初始化 WK_UP-->GPIOA.0	  下拉输入
    	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉	  
    	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
    }
    

    开发板上有两个按键,KEY_UPKEY0,都是一端接高电平,一端接IO,所以模式设置为下拉输入,KEY_UP对应的GPIO引脚为GPIOA0,KEY0对应的引脚为GPIOE4。IO时钟都挂载在APB2上。


    BEEP_Init()函数

    开发板上并没有蜂鸣器,我选择了外接一个蜂鸣器,同样接在PB8引脚上。初始化配置步骤和LED与KEY相同,模式为推挽输出,由于我的蜂鸣器低电平有效,所以初始化中还需把IO电平拉高。

    //初始化PB8为输出口.并使能这个口的时钟		    
    //蜂鸣器初始化
    void BEEP_Init(void)
    {
     
     GPIO_InitTypeDef  GPIO_InitStructure;
     	
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 //使能GPIOB端口时钟
     
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;				 //BEEP-->PB.8 端口配置
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	 //速度为50MHz
     GPIO_Init(GPIOB, &GPIO_InitStructure);	 //根据参数初始化GPIOB.8
     
     GPIO_SetBits(GPIOB,GPIO_Pin_8);//输出0,关闭蜂鸣器输出
    
    }
    

    KEY_Scan(…)函数

    该函数中,mode表示模式,为0表示短按,为1表示长按。局部静态变量key_up默认为1,表示按键处于空闲状态(松开)。
    如果选择短按,在按键处于空闲状态时,检测到KEY0WK_UP中任意一个按键被按下,则将key_up置0,在此期间不处理其他按键判断,函数返回值为按键值或0(无按键);当按键松开,程序再次运行到按键扫描函数中时,key_up置为1,按键再次回到空闲状态。
    如果选择长按,则key_up恒为1,无论是否有按键正处于按下状态,每次进入KEY_Scan函数都进行按键判断,这样就实现了按键的长按检测。

    u8 KEY_Scan(u8 mode)
    {	 
    	static u8 key_up=1;//按键按松开标志
    	if(mode)key_up=1;  //支持连按		  
    	if(key_up&&(KEY0==1||WK_UP==1))
    	{
    		delay_ms(10);//去抖动 
    		key_up=0;
    		if(KEY0==1)return KEY0_PRES;
    		else if(WK_UP==1)return WKUP_PRES;
    	}else if(KEY0==0&&WK_UP==0)key_up=1; 	    
     	return 0;// 无按键按下
    }
    

    实验3 串口实验

    mian()

    与前两个实验相比,串口实验增加了NVIC中断配置、串口初始化配置。main函数实现的功能为:单片机不停地向串口发送提示性数据,如果有外部设备通过串口向单片机发送数据(以“\r\n“作为结束符),单片机接收数据并返回给外部设备。

    
     int main(void)
     {		
     	u16 t;  
    	u16 len;	
    	u16 times=0;
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();			     //LED端口初始化
    	KEY_Init();          //初始化与按键连接的硬件接口
     	while(1)
    	{
    		if(USART_RX_STA&0x8000)
    		{					   
    			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
    			printf("\r\n您发送的消息为:\r\n\r\n");
    			for(t=0;t<len;t++)
    			{
    				USART_SendData(USART1, USART_RX_BUF[t]);//向串口1发送数据
    				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
    			}
    			printf("\r\n\r\n");//插入换行
    			USART_RX_STA=0;
    		}else
    		{
    			times++;
    			if(times%5000==0)
    			{
    				printf("\r\n战舰STM32开发板 串口实验\r\n");
    				printf("正点原子@ALIENTEK\r\n\r\n");
    			}
    			if(times%200==0)printf("请输入数据,以回车键结束\n");  
    			if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
    			delay_ms(10);   
    		}
    	}	 
     }
    

    NVIC_PriorityGroupConfig

    NVIC:嵌套向量中断控制器,NVIC_PriorityGroupConfig函数是中断优先级的分组配置函数。

    
    /**
      * @brief  Configures the priority grouping: pre-emption priority and subpriority.
      * @param  NVIC_PriorityGroup: specifies the priority grouping bits length. 
      *   This parameter can be one of the following values:
      *     @arg NVIC_PriorityGroup_0: 0 bits for pre-emption priority
      *                                4 bits for subpriority
      *     @arg NVIC_PriorityGroup_1: 1 bits for pre-emption priority
      *                                3 bits for subpriority
      *     @arg NVIC_PriorityGroup_2: 2 bits for pre-emption priority
      *                                2 bits for subpriority
      *     @arg NVIC_PriorityGroup_3: 3 bits for pre-emption priority
      *                                1 bits for subpriority
      *     @arg NVIC_PriorityGroup_4: 4 bits for pre-emption priority
      *                                0 bits for subpriority
      * @retval None
      */
    void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
    {
      /* Check the parameters */
      assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
      
      /* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
      SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
    }
    

    中断优先级分为抢占式优先级响应优先级,抢占优先级越高的先处理,当两个中断向量的抢占优先级相同时,如果两个中断同时到达, 则先处理响应优先级高的中断。

    如果对两种优先级的位数分配进行分组,可以分为5组(0~4),分组配置是在寄存器SCB->AIRCR中配置:

    main函数中,分组为:

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    

    注:这个分组只是设置STM32中断的两种优先级可选范围,比如0组中,没有抢占优先级,一般情况(学习过程中)该配置设置为2组就行了。另外,这个分组是全局的,所以一个程序中只需要配置一次,多次配置可能会导致未知错误。

    uart_init(…)函数

    串口初始化函数里不仅有GPIO初始化,还有UART初始化和NVIC初始化。

    void uart_init(u32 bound){
      //GPIO端口设置
      GPIO_InitTypeDef GPIO_InitStructure;
    	USART_InitTypeDef USART_InitStructure;
    	NVIC_InitTypeDef NVIC_InitStructure;
    	 
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
      
    	//USART1_TX   GPIOA.9
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
      GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
       
      //USART1_RX	  GPIOA.10初始化
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
      GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  
    
      //Usart1 NVIC 配置
      NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
    	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
      
       //USART 初始化设置
    
    	USART_InitStructure.USART_BaudRate = bound;//串口波特率
    	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
    	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(USART1, &USART_InitStructure); //初始化串口1
      USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
      USART_Cmd(USART1, ENABLE);                    //使能串口1 
    
    }
    
    

    从原理图可知串口的发送IO为GPIOA9,接收IO为GPIOA10,TX(PA9)设置为复用推挽输出(PA9为复用引脚,可以通过设置复用推挽输出完成USART_TX功能的配置,另外还可以通过配合复用寄存器方式实现复用,如PWM实验),RX设置为浮空输入。

    串口配置一般可以分为以下几步:

    1. 定义USART_InitTypeDef结构体成员
    2. 使能USART外设时钟RCC_APB2PeriphClockCmd(...),根据手册查看对应时钟总线(LED_init中有介绍)
    3. 设置波特率USART_BaudRate,可以通过外部传参设置
    4. 设置字长USART_WordLength,一般为8字节
    5. 设置停止位USART_StopBits,一般为1个停止位
    6. 设置校验方式USART_Parity
    7. 设置流控制USART_HardwareFlowControl,一般无控制
    8. 设置收发模式USART_Mode,设置为收发共用
    9. 初始化USART_InitTypeDef成员USART_Init(...)
    10. 如果要使用串口中断,还需要配置串口中断USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
    11. 使能串口USART_Cmd(USARTx, ENABLE)

    NVIC中断配置一般步骤:

    1. 定义一个NVIC_InitTypeDef结构体成员
    2. 设置中断通道NVIC_IRQChannel
    3. 设置抢占优先级NVIC_IRQChannelPreemptionPriority,大小范围由分组决定
    4. 设置响应优先级NVIC_IRQChannelSubPriority
    5. 使能中断NVIC_IRQChannelCmd,ENABLE为使能
    6. 初始化NVIC_InitTypeDef成员,NVIC_Init(...)

    串口处理全局变量

    //串口1中断服务程序
    //注意,读取USARTx->SR能避免莫名其妙的错误   	
    u8 USART_RX_BUF[USART_REC_LEN];     //接收缓冲,最大USART_REC_LEN个字节.
    //接收状态
    //bit15,	接收完成标志
    //bit14,	接收到0x0d
    //bit13~0,	接收到的有效字节数目
    u16 USART_RX_STA=0;       //接收状态标记
    

    USART_RX_BUF为程序定义的全局接收缓冲, USART_REC_LEN为缓冲最大字节数。
    USART_RX_STA为程序定义的全局状态值,表述串口1的接收状态,16位,0-13位为接收到的字节数,14位为1表示接收到0x0d(’\r’),15位数据为1表示接收完成。

    重写printf(fputc)

    USART_SendData(...)是串口发送字节数据的库函数,如果我们想发送字符串,用这个函数就不太方便,如果我们重写fputc函数,就能直接用printf来打印字符串到串口,方法如下。
    USART_GetFlagStatus函数的作用是获取串口状态,USART_FLAG_TC参数表示接收完成状态。
    【注意】使用该方式重写fputc,一定要打开Keil的Use MicroLIB,否则程序可能无法运行。

    /*使用microLib的方法*/
     
    int fputc(int ch, FILE *f)
    {
    	USART_SendData(USART1, (uint8_t) ch);
    
    	while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}	
       
        return ch;
    }
    

    USART1_IRQHandler串口中断函数

    STM32的中断服务函数名称是固定,是通过函数名与底层绑定。

    void USART1_IRQHandler(void)                	//串口1中断服务程序
    	{
    	u8 Res;
    	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
    		{
    		Res =USART_ReceiveData(USART1);	//读取接收到的数据
    		
    		if((USART_RX_STA&0x8000)==0)//接收未完成
    			{
    				if(USART_RX_STA&0x4000)//接收到了0x0d
    				{
    					if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
    					else USART_RX_STA|=0x8000;	//接收完成了 
    				}
    				else //还没收到0X0D
    				{	
    					if(Res==0x0d)USART_RX_STA|=0x4000;
    					else
    					{
    						USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
    						USART_RX_STA++;
    						if(USART_RX_STA>(USART_REC_LEN-1))
    							USART_RX_STA=0;//接收数据错误,重新开始接收	  
    					}		 
    				}
    			}   		 
         } 
    } 
    

    当串口接收到数据,中断服务函数自动执行,先使用USART_GetITStatus(...)判断USART1的中断接受状态USART_IT_RXNE,如果接收完成,使用USART_ReceiveData(...)接收串口数据,串口数据接收以1个字节为单位,当接收到回车时(’\r‘和\‘n’),完成接收,此时主函数开始处理接收到的数据。

    实验结果


    这。。。。。。😵,才完成3个实验啊。


    实验4 外部中断实验

    STM32的中断控制器支持19个外部中断/事件请求:
    线0~15:对应外部IO口的输入中断。
    线16:连接到PVD输出。
    线17:连接到RTC闹钟事件。
    线18:连接到USB唤醒事件。
    下图为IO口对应的中断线:

    main()

    该实验仅仅比实验3多了一个EXTIX_Init(),主函数没有什么代码,这就说明功能全搬到了外部中断服务函数中。

     int main(void)
     {		
     
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();		  		//初始化与LED连接的硬件接口
    	BEEP_Init();         	//初始化蜂鸣器端口
    	KEY_Init();         	//初始化与按键连接的硬件接口
    	EXTIX_Init();		 	//外部中断初始化
    	LED0=0;					//点亮LED0
    	while(1)
    	{	    
    		printf("OK\r\n");	
    		delay_ms(1000);	  
    	}
     }
    

    EXTIX_Init()函数

    初始化函数中,配置了两个外部中断(EXTI0和EXIT4),对应开发板上的两个按键。

    //外部中断0服务程序
    void EXTIX_Init(void)
    {
     
     	EXTI_InitTypeDef EXTI_InitStructure;
     	NVIC_InitTypeDef NVIC_InitStructure;
    
        KEY_Init();	 //	按键端口初始化
    
      	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	//使能复用功能时钟
    
       //GPIOE.4	  中断线以及中断初始化配置  上升沿触发	//KEY0
      	GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);
    	EXTI_InitStructure.EXTI_Line=EXTI_Line4;	//KEY0
      	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
      	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
      	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
      	EXTI_Init(&EXTI_InitStructure);	 	//根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
    
    	//GPIOA.0	  中断线以及中断初始化配置 上升沿触发 PA0  WK_UP
    	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0); 
      	EXTI_InitStructure.EXTI_Line=EXTI_Line0;
      	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
      	EXTI_Init(&EXTI_InitStructure);		//根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
    
    
      	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//使能按键WK_UP所在的外部中断通道
      	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;	//抢占优先级2, 
      	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;					//子优先级3
      	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;								//使能外部中断通道
      	NVIC_Init(&NVIC_InitStructure); 
    
    	NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;			//使能按键KEY0所在的外部中断通道
      	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;	//抢占优先级2 
      	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;					//子优先级0 
      	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;								//使能外部中断通道
      	NVIC_Init(&NVIC_InitStructure);  	  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
     
    }
    

    外部中断初始化配置一般步骤:

    1. 定义一个EXTI_InitTypeDef结构体成员
    2. 外部中断(复用IO,AFIO)时钟使能,RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    3. IO中断线配置,GPIO_EXTILineConfig(...)
    4. 设置中断线EXTI_Line
    5. 设置中断模式EXTI_Mode
    6. 设置触发方式EXTI_Trigger
    7. 使能中断线EXTI_LineCmd
    8. 初始化EXTI_InitTypeDef成员,

    EXTIx_IRQHandler外部中断函数

    当按键按下,IO检测到上升沿,触发中断,执行对应中断服务函数,函数中完成了LED1LED0的亮灭控制,每次进入中断后,必须手动清除中断标志EXTI_ClearITPendingBit(...),不然中断函数会一直执行。

    //外部中断0服务程序 
    void EXTI0_IRQHandler(void)
    {
    	delay_ms(10);//消抖
    	if(WK_UP==1)	 	 //WK_UP按键
    	{				 
    		LED1=!LED1;	
    	}
    	EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位  
    }
     
    //外部中断4服务程序
    void EXTI4_IRQHandler(void)
    {
    	delay_ms(10);//消抖
    	if(KEY0==1)	 //按键KEY0
    	{
    		LED0=!LED0;
    	}		 
    	EXTI_ClearITPendingBit(EXTI_Line4);  //清除LINE4上的中断标志位  
    }
    

    实验5 独立看门狗实验

    STM32看门狗
    在由单片机构成的微型计算机系统中,由于单片机的工作常常会受到来自外界电磁场的干扰,造成程序的跑飞,而陷入死循环,程序的正常运行被打断,由单片机控制的系统无法继续工作,会造成整个系统的陷入停滞状态,发生不可预料的后果,所以出于对单片机运行状态进行实时监测的考虑,便产生了一种专门用于监测单片机程序运行状态的模块或者芯片,俗称“看门狗”(watchdog) 。
    STM32内置两个看门狗,提供了更高的安全性,时间的精确性和使用的灵活性。两个看门狗设备(独立看门狗/窗口看门狗)可以用来检测和解决由软件错误引起的故障。当计数器达到给定的超时值时,触发一个中断(仅适用窗口看门狗)或者产生系统复位。
    独立看门狗(IWDG)由专用的低速时钟(LSI)驱动,即使主时钟发生故障它仍有效。
    独立看门狗适合应用于需要看门狗作为一个在主程序之外 能够完全独立工作,并且对时间精度要求低的场合。
    窗口看门狗由从APB1时钟分频后得到时钟驱动。通过可配置的时间窗口来检测应用程序非正常的过迟或过早操作。
    窗口看门狗最适合那些要求看门狗在精确计时窗口起作用的程序。
    ——正点原子课程PPT

    IWDG(Independent watchdog)独立看门狗,可以用来检测并解决由于软件错误导致的故障,当计数器到达给定的超时值时,会触发一个中断或产生系统复位。

    main()

    新增两个函数:IWDG_Init(...)IWDG_Feed(),本实验通过按键WKUP喂狗,如果超过1s没有喂狗,单片机就复位(LED一直亮说明没复位)。

     int main(void)
     {		
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();		  	 //初始化与LED连接的硬件接口
    	KEY_Init();          //按键初始化	 
    	delay_ms(500);   	 //让人看得到灭
    	IWDG_Init(4,625);    //与分频数为64,重载值为625,溢出时间为1s	   
    	LED0=0;				 //点亮LED0
    	while(1)
    	{
    		if(KEY_Scan(0)==WKUP_PRES)
    		{
    			IWDG_Feed();//如果WK_UP按下,则喂狗
    		}
    		delay_ms(10);
    	};	 
    }
    

    IWDG_Init(…)函数

    初始化独立看门狗,需要先用IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);使能相关寄存器的写权限,然后用IWDG_SetPrescaler(prer)IWDG_SetReload(rlr)设置预分频值prer和重载值rlr,接着用IWDG_ReloadCounter()重装载计数器(喂狗),最后使能看门狗——IWDG_Enable()

    //初始化独立看门狗
    //prer:分频数:0~7(只有低3位有效!)
    //分频因子=4*2^prer.但最大值只能是256!
    //rlr:重装载寄存器值:低11位有效.
    //时间计算(大概):Tout=((4*2^prer)*rlr)/40 (ms).
    void IWDG_Init(u8 prer,u16 rlr) 
    {	
     	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  //使能对寄存器IWDG_PR和IWDG_RLR的写操作
    	
    	IWDG_SetPrescaler(prer);  //设置IWDG预分频值:设置IWDG预分频值为64
    	
    	IWDG_SetReload(rlr);  //设置IWDG重装载值
    	
    	IWDG_ReloadCounter();  //按照IWDG重装载寄存器的值重装载IWDG计数器
    	
    	IWDG_Enable();  //使能IWDG
    }
    

    从库函数里的宏定义我们可以了解到看门狗在喂狗使能时向IWDG_KR发送的数据。

    /* KR register bit mask */
    #define KR_KEY_Reload    ((uint16_t)0xAAAA)
    #define KR_KEY_Enable    ((uint16_t)0xCCCC)
    

    IWDG_Feed()函数

    重载计数器即为喂狗

    //喂独立看门狗
    void IWDG_Feed(void)
    {   
     	IWDG_ReloadCounter();//reload										   
    }
    

    实验6 窗口看门狗实验[待学习]

    窗口看门狗(Window watchdog)通常被用来监测由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。
    之所以称为窗口就是因为其喂狗时间是一个有上下限的范围(窗口),你可以通过设定相关寄存器,设定其上限时间(下限固定)。喂狗的时间不能过早也不能过晚。

    为什么要窗口看门狗?
    对于一般的看门狗,程序可以在它产生复位前的任意时刻刷新看门狗,但这有一个隐患,有可能程序跑乱了又跑回到正常的地方,或跑乱的程序正好执行了刷新看门狗操作,这样的情况下一般的看门狗就检测不出来了;
    如果使用窗口看门狗,程序员可以根据程序正常执行的时间设置刷新看门狗的一个时间窗口,保证不会提前刷新看门狗也不会滞后刷新看门狗,这样可以检测出程序没有按照正常的路径运行非正常地跳过了某些程序段的情况。
    ——正点原子课程PPT

    窗口看门狗以前没认真学,现在一点概念都没有了,先记录下,以后再完善吧。。。

    main()

     int main(void)
     {		
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();
    	KEY_Init();          //按键初始化	 
    	LED0=0;
    	delay_ms(300);	  
    	WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//计数器值为7f,窗口寄存器为5f,分频数为8	   
     	while(1)
    	{
    		LED0=1;			  	   
    	}   
    }
      
    

    WWDG_Init(…)函数

    //保存WWDG计数器的设置值,默认为最大. 
    u8 WWDG_CNT=0x7f; 
    //初始化窗口看门狗 	
    //tr   :T[6:0],计数器值 
    //wr   :W[6:0],窗口值 
    //fprer:分频系数(WDGTB),仅最低2位有效 
    //Fwwdg=PCLK1/(4096*2^fprer). 
    
    void WWDG_Init(u8 tr,u8 wr,u32 fprer)
    { 
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);  //   WWDG时钟使能
    
    	WWDG_CNT=tr&WWDG_CNT;   //初始化WWDG_CNT.   
    	WWDG_SetPrescaler(fprer);设置IWDG预分频值
    
    	WWDG_SetWindowValue(wr);//设置窗口值
    
    	WWDG_Enable(WWDG_CNT);	 //使能看门狗 ,	设置 counter .                  
    
    	WWDG_ClearFlag();//清除提前唤醒中断标志位 
    
    	WWDG_NVIC_Init();//初始化窗口看门狗 NVIC
    
    	WWDG_EnableIT(); //开启窗口看门狗中断
    } 
    
    //窗口看门狗中断服务程序
    void WWDG_NVIC_Init()
    {
    	NVIC_InitTypeDef NVIC_InitStructure;
    	NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn;    //WWDG中断
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;   //抢占2,子优先级3,组2	
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;	 //抢占2,子优先级3,组2	
      	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; 
    	NVIC_Init(&NVIC_InitStructure);//NVIC初始化
    }
    
    void WWDG_IRQHandler(void)
    {
    
    	WWDG_SetCounter(WWDG_CNT);	  //当禁掉此句后,窗口看门狗将产生复位
    
    	WWDG_ClearFlag();	  //清除提前唤醒中断标志位
    
    	LED1=!LED1;		 //LED状态翻转
    }
    

    实验7 定时器中断实验

    main()

    主函数仅仅多了定时器初始化函数TIM3_Int_Init(...)

     int main(void)
     {		
     
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();			     //LED端口初始化
    	TIM3_Int_Init(4999,7199);//10Khz的计数频率,计数到5000为500ms  
       	while(1)
    	{
    		LED0=!LED0;
    		delay_ms(200);		   
    	}	 
    }	
    

    TIM3_Int_Init(…)函数


    定时器3挂载在APB1时钟总线上。

    //通用定时器3中断初始化
    //这里时钟选择为APB1的2倍,而APB1为36M
    //arr:自动重装值。
    //psc:时钟预分频数
    //这里使用的是定时器3!
    void TIM3_Int_Init(u16 arr,u16 psc)
    {
        TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    	NVIC_InitTypeDef NVIC_InitStructure;
    
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
    	
    	//定时器TIM3初始化
    	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
    	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
    	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
     
    	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断
    
    	//中断优先级NVIC设置
    	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
    	NVIC_Init(&NVIC_InitStructure);  //初始化NVIC寄存器
    
    
    	TIM_Cmd(TIM3, ENABLE);  //使能TIMx					 
    }
    

    基本定时器初始化配置一般步骤

    1. 定义一个TIM_TimeBaseInitTypeDef结构体成员
    2. 使能外设时钟,TIM3:RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)
    3. 设置周期TIM_Period
    4. 设置预分频系数TIM_Prescaler
    5. 设置时钟分割TIM_ClockDivision
    6. 设置计数方向TIM_CounterMode
    7. 初始化TIM_TimeBaseInitTypeDef成员,TIM_TimeBaseInit(...)
    8. 如果开启中断,则需要使能对应中断TIM_ITConfig(TIMx,TIM_IT_Update,ENABLE );
    9. 使能定时器TIM_Cmd(TIMx, ENABLE)


    TIM3在APB1时钟总线上,APB1的时钟为36MHz,但TIM3的时钟是APB1的两倍,即72MHz。

    定时器配置完后,下一步就是了解如何设定计时的参数,main函数中定时器初始化:TIM3_Int_Init(4999,7199),TIM3的时钟频率为72MHz,预分频系数为7200 – 1(定时器计时溢出后的再计一次数才触发中断,所以要减1),72000000 / 7200 = 10000(Hz),1 / 10000 = 0.1ms,计数值从0开始,所以4999表示计时5000次,5000 * 0.1ms = 500ms。

    TIMx_IRQHandler()定时器中断函数

    当定时器中断更新,单片机执行定时器的中断服务函数,记得在函数中用TIM_ClearITPendingBit(...)来清除对应的中断位。

    //定时器3中断服务程序
    void TIM3_IRQHandler(void)   //TIM3中断
    {
    	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
    		{
    		TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
    		LED1=!LED1;
    		}
    }
    
    

    实验8 PWM输出实验

    PWM产生原理:在定时器一个计时周期内,计数值低于比较值时,IO输出低电平(或高电平),当计数值高于比较值,IO输出相反的电平,这样就产生了所谓的PWM波,比较值CCR决定PWM的占空比,重载值ARR决定PWM的周期(频率)。

    main()

    这个实验一看便知道是呼吸灯了,直接分析PWM配置吧。
    main函数里用到了TIM_SetCompare2(...)函数,它的作用是设置定时器比较值,即PWM占空比

     int main(void)
     {		
     	u16 led0pwmval=0;
    	u8 dir=1;	
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();			     //LED端口初始化
     	TIM3_PWM_Init(899,0);	 //不分频。PWM频率=72000000/900=80Khz
       	while(1)
    	{
     		delay_ms(10);	 
    		if(dir)led0pwmval++;
    		else led0pwmval--;
    
     		if(led0pwmval>300)dir=0;
    		if(led0pwmval==0)dir=1;										 
    		TIM_SetCompare2(TIM3,led0pwmval);		   
    	}	 
     }
    
    

    TIM3_PWM_Init(…)函数

    
    //TIM3 PWM部分初始化 
    //PWM输出初始化
    //arr:自动重装值
    //psc:时钟预分频数
    void TIM3_PWM_Init(u16 arr,u16 psc)
    {  
    	GPIO_InitTypeDef GPIO_InitStructure;
    	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    	TIM_OCInitTypeDef  TIM_OCInitStructure;
    	
    
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);	//使能定时器3时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB  | RCC_APB2Periph_AFIO, ENABLE);  //使能GPIO外设和AFIO复用功能模块时钟
    	
    	GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重映射  TIM3_CH2->PB5    
     
       //设置该引脚为复用输出功能,输出TIM3 CH2的PWM脉冲波形	GPIOB.5
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
     
       //初始化TIM3
    	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
    	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 
    	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
    	
    	//初始化TIM3 Channel2 PWM模式	 
    	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
     	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
    	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
    	TIM_OC2Init(TIM3, &TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM3 OC2
    
    	TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);  //使能TIM3在CCR2上的预装载寄存器
     
    	TIM_Cmd(TIM3, ENABLE);  //使能TIM3
    }
    


    开发板上PB5对应LED1,如果要将它作为TIM3_CH2来输出PWM波,必须先配置复用寄存器(选择部分重映像)。步骤:

    1. 使能复用时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)
    2. 使能TIM3部分重映像GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE)

    除此以外,PB5也需要重新配置,模式选择复用推挽输出

    定时器PWM配置一般步骤

    1. 定义一个TIM_OCInitTypeDef结构体成员
    2. 设置比较输出的模式TIM_OCModeTIM_OCMode_PWM2
    3. 设置输出状态TIM_OutputStateTIM_OutputState_Enable
    4. 设置输出极性TIM_OCPolarity
    5. 初始化TIM_OCInitTypeDef成员,TIM_OC2Init(...)
    6. 使能定时器输出比较预装载寄存器,TIM_OC2PreloadConfig(TIMx, TIM_OCPreload_Enable)

    此时,我内心已经绝望,原来我这么菜🥱,后面几个实验学起来都感觉很生疏了,当初偷的懒,现在得来偿还。


    实验9 输入捕获实验[待学习]

    说实话,我都忘了什么叫输入捕获。

    一句话总结工作过程:通过检测TIMx_CHx上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。
    ——正点原子课程PPT

    main()

    main函数分析了半天,开始以为本实验是用TIM5捕获TIM3的输出 ,后来发现PB5和PA0根本没有引出IO引脚(PB5为LED1,PA0为KEY_UP),所以本实验的功能为检测按键按下的时间并打印到串口。这么说输入捕获和逻辑分析仪有点类似。

    extern u8  TIM5CH1_CAPTURE_STA;		//输入捕获状态		    				
    extern u16	TIM5CH1_CAPTURE_VAL;	//输入捕获值	
     int main(void)
     {		
     	u32 temp=0; 
    	delay_init();	    	 //延时函数初始化	  
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	uart_init(115200);	 //串口初始化为115200
     	LED_Init();			     //LED端口初始化
     
     	TIM3_PWM_Init(899,0); 		//不分频。PWM频率=72000/(899+1)=80Khz
     	TIM5_Cap_Init(0XFFFF,72-1);	//以1Mhz的频率计数 
       	while(1)
    	{
     		delay_ms(10);
    		TIM_SetCompare2(TIM3,TIM_GetCapture2(TIM3)+1);
    
    		if(TIM_GetCapture2(TIM3)==300)TIM_SetCompare2(TIM3,0);	
    		 		 
     		if(TIM5CH1_CAPTURE_STA&0X80)//成功捕获到了一次上升沿
    		{
    			temp=TIM5CH1_CAPTURE_STA&0X3F;
    			temp*=65536;//溢出时间总和
    			temp+=TIM5CH1_CAPTURE_VAL;//得到总的高电平时间
    			printf("HIGH:%d us\r\n",temp);//打印总的高点平时间
    			TIM5CH1_CAPTURE_STA=0;//开启下一次捕获
    		}
    	}
     }
    
    

    TIM5_Cap_Init(…)函数

    //定时器5通道1输入捕获配置
    
    TIM_ICInitTypeDef  TIM5_ICInitStructure;
    
    void TIM5_Cap_Init(u16 arr,u16 psc)
    {	 
    	GPIO_InitTypeDef GPIO_InitStructure;
    	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
       	NVIC_InitTypeDef NVIC_InitStructure;
    
    	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);	//使能TIM5时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  //使能GPIOA时钟
    	
    	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;  //PA0 清除之前设置  
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 输入  
    	GPIO_Init(GPIOA, &GPIO_InitStructure);
    	GPIO_ResetBits(GPIOA,GPIO_Pin_0);						 //PA0 下拉
    	
    	//初始化定时器5 TIM5	 
    	TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 
    	TIM_TimeBaseStructure.TIM_Prescaler =psc; 	//预分频器   
    	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
    	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
      
    	//初始化TIM5输入捕获参数
    	TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 	选择输入端 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);
    	
    	//中断分组初始化
    	NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;  //TIM3中断
    	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  //先占优先级2级
    	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //从优先级0级
    	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
    	NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 
    	
    	TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断	
    	
       	TIM_Cmd(TIM5,ENABLE ); 	//使能定时器5
       
    }
    

    定时器输入捕获配置流程一般为

    1. 定义一个TIM_ICInitTypeDef结构体成员
    2. 设置通道TIM_Channel
    3. 设置输入捕获电平TIM_ICPolarity
    4. 设置输入捕获方向TIM_ICSelection
    5. 设置输入捕获预分频系数TIM_ICPrescaler
    6. 设置输入捕获滤波器TIM_ICFilter
    7. 初始化TIM_ICInitTypeDef成员,TIM_ICInit(...)
    8. 如果开启捕获中断:TIM_ITConfig(TIMx, TIM_TI_CC1, ENABLE)

    TIM5_IRQHandler() 处理输入捕获

    
    u8  TIM5CH1_CAPTURE_STA=0;	//输入捕获状态		    				
    u16	TIM5CH1_CAPTURE_VAL;	//输入捕获值
     
    //定时器5中断服务程序	 
    void TIM5_IRQHandler(void)
    { 
    
     	if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获	
    	{	  
    		if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
    		 
    		{	    
    			if(TIM5CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
    			{
    				if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
    				{
    					TIM5CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
    					TIM5CH1_CAPTURE_VAL=0XFFFF;
    				}else TIM5CH1_CAPTURE_STA++;
    			}	 
    		}
    	if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
    		{	
    			if(TIM5CH1_CAPTURE_STA&0X40)		//捕获到一个下降沿 		
    			{	  			
    				TIM5CH1_CAPTURE_STA|=0X80;		//标记成功捕获到一次高电平脉宽
    				TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5);
    		   		TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
    			}else  								//还未开始,第一次捕获上升沿
    			{
    				TIM5CH1_CAPTURE_STA=0;			//清空
    				TIM5CH1_CAPTURE_VAL=0;
    	 			TIM_SetCounter(TIM5,0);
    				TIM5CH1_CAPTURE_STA|=0X40;		//标记捕获到了上升沿
    		   		TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling);		//CC1P=1 设置为下降沿捕获
    			}		    
    		}			     	    					   
     	}
     
        TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
     
    }
    

    实验10 TFTLCD显示实验[放弃]

    LCD,额🥱,正点原子的LCD代码兼容了好几款屏幕,驱动代码3000+行,这个实验就算了。

    今天的学习就到此为止吧,路还很长!

    已经是另一天了,从13:56到0:21,差不多10个多小时,任务失败了,mission failed!


    物联沃分享整理
    物联沃-IOTWORD物联网 » 如何更好地学习STM32?——掌握正点原子入门篇例程的半日学习经验分享

    发表评论