STM32

STM32F1xx官方资料:
《STM32中文参考手册V10》-第8章通用和复用功能IO(GPIO和AFIO )
芯片数据手册(datasheet)

STM32的GPIO介绍

STM32引脚说明

GPIO是通用输入/输出端口的简称,是STM32可控制的引脚。GPIO的引脚与外部硬件设备连接,可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。

STM32F103ZET6芯片为144脚芯片,包括7个通用目的的输入/输出口(GPIO)组,分别为GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF、GPIOG,同时每组GPIO口组有16个GPIO口。通常简略称为PAx、PBx、PCx、PDx、PEx、PFx、PGx,其中x为0-15。

STM32的大部分引脚除了当GPIO使用之外,还可以复用位外设功能引脚(比如串口),这部分在【STM32】STM32端口复用和重映射(AFIO辅助功能时钟) 中有详细的介绍。

GPIO基本结构

每个GPIO内部都有这样的一个电路结构,这个结构在本文下面会具体介绍。


这边的电路图稍微提一下:

保护二极管:IO引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入。当引脚电压高于VDD时,上方的二极管导通;当引脚电压低于VSS时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。但是尽管如此,还是不能直接外接大功率器件,须加大功率及隔离电路驱动,防止烧坏芯片或者外接器件无法正常工作。

P-MOS管和N-MOS管:由P-MOS管和N-MOS管组成的单元电路使得GPIO具有“推挽输出”和“开漏输出”的模式。这里的电路会在下面很详细地分析到。
TTL肖特基触发器:信号经过触发器后,模拟信号转化为0和1的数字信号。但是,当GPIO引脚作为ADC采集电压的输入通道时,用其“模拟输入”功能,此时信号不再经过触发器进行TTL电平转换。ADC外设要采集到的原始的模拟信号。

这里需要注意的是,在查看《STM32中文参考手册V10》中的GPIO的表格时,会看到有“FT”一列,这代表着这个GPIO口时兼容3.3V和5V的;如果没有标注“FT”,就代表着不兼容5V。

STM32的GPIO工作方式

GPIO支持4种输入模式(浮空输入、上拉输入、下拉输入、模拟输入)和4种输出模式(开漏输出、开漏复用输出、推挽输出、推挽复用输出)。同时,GPIO还支持三种最大翻转速度(2MHz、10MHz、50MHz)。

每个I/O口可以自由编程,但I/O口寄存器必须按32位字被访问。

1. GPIO_Mode_AIN 模拟输入

2. GPIO_Mode_IN_FLOATING 浮空输入

3. GPIO_Mode_IPD 下拉输入

4. GPIO_Mode_IPU 上拉输入

5. GPIO_Mode_Out_OD 开漏输出

6. GPIO_Mode_Out_PP 推挽输出

7. GPIO_Mode_AF_OD 复用开漏输出

8. GPIO_Mode_AF_PP 复用推挽输出

下面将具体介绍GPIO的这八种工作方式:

浮空输入模式


浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。

上拉输入模式


上拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在高电平;并且在I/O端口输入为低电平的时候,输入端的电平也还是低电平。

下拉输入模式


下拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在低电平;并且在I/O端口输入为高电平的时候,输入端的电平也还是高电平。

模拟输入模式


模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等。

开漏输出模式


开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经N-MOS管,最终输出到I/O端口。这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,I/O端口的电平不一定是输出的电平。

开漏复用输出模式


开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。

推挽输出模式


推挽输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经P-MOS管和N-MOS管,最终输出到I/O端口。这里要注意P-MOS管和N-MOS管,当设置输出的值为高电平的时候,P-MOS管处于开启状态,N-MOS管处于关闭状态,此时I/O端口的电平就由P-MOS管决定:高电平;当设置输出的值为低电平的时候,P-MOS管处于关闭状态,N-MOS管处于开启状态,此时I/O端口的电平就由N-MOS管决定:低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平。

推挽复用输出模式


推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。

总结与分析

1、什么是推挽结构和推挽电路?

推挽结构一般是指两个参数相同的三极管或MOS管分别受两互补信号的控制,总是在一个三极管或MOS管导通的时候另一个截止。高低电平由输出电平决定。

推挽电路是两个参数相同的三极管或MOSFET,以推挽方式存在于电路中,各负责正负半周的波形放大任务。电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载抽取电流。推拉式输出级既提高电路的负载能力,又提高开关速度。

2、开漏输出和推挽输出的区别?

开漏输出:只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极。适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内);

推挽输出:可以输出强高、低电平,连接数字器件。

关于推挽输出和开漏输出,最后用一幅最简单的图形来概括:


该图中左边的便是推挽输出模式,其中比较器输出高电平时下面的PNP三极管截止,而上面NPN三极管导通,输出电平VS+;当比较器输出低电平时则恰恰相反,PNP三极管导通,输出和地相连,为低电平。右边的则可以理解为开漏输出形式,需要接上拉。

3、在STM32中选用怎样选择I/O模式?

浮空输入_IN_FLOATING ——浮空输入,可以做KEY识别,RX1

带上拉输入_IPU——IO内部上拉电阻输入

带下拉输入_IPD—— IO内部下拉电阻输入

模拟输入_AIN ——应用ADC模拟输入,或者低功耗下省电

开漏输出_OUT_OD ——IO输出0接GND,IO输出1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为1时,IO口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样IO口也就可以由外部电路改变为低电平或不变。可以读IO输入电平变化,实现C51的IO双向功能

推挽输出_OUT_PP ——IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的

复用功能的推挽输出_AF_PP ——片内外设功能(I2C的SCL、SDA)

复用功能的开漏输出_AF_OD——片内外设功能(TX1、MOSI、MISO.SCK.SS)

来源:CSDN,作者:Yngz_Miao
原文:https://blog.csdn.net/qq_38410730/article/details/79858906
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 2

STM32F1xx官方资料:《Cortex-M3权威指南-中文》-第8章最后一个小节:Systick定时器

SysTick定时器

Systick定时器,是一个简单的定时器,对于CM3、CM4内核芯片,都有Systick定时器。Systick定时器常用来做延时,或者实时系统的心跳时钟。这样可以节省MCU资源,不用浪费一个定时器。比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟。

Systick定时器就是系统滴答定时器,一个24 位的倒计数定时器,计到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。

SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15)。Systick中断的优先级也可以设置。

实际上,Systick就是一个定时器而已,只是它放在了NVIC中,主要的目的是为了给操作系统提供一个硬件上的中断,称之为滴答中断操作系统进行运转的时候,也会有时间节拍。它会根据节拍来工作,把整个时间段分成很多小小的时间片,而每个任务每次只能运行一个时间片的时间长度,超时就退出给别的任务运行,这样可以确保任何一个任务都不会霸占操作系统提供的各种定时功能,都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统的节拍。只要不把它在SysTick控制及状态寄存器中的使能位清除,就一直执行。 

SysTick相关寄存器

SysTick有四个寄存器,分别为CTRL(控制与状态寄存器)、LOAD(自动重装载值寄存器)、VAL(当前值寄存器)、CALIB(校准值寄存器)。

在MDK的core_m3.h文件中定义了一个结构体SysTick_Type,里面也包括了这四个寄存器。

typedef struct
{
  __IO uint32_t CTRL;                         /*!< Offset: 0x00  SysTick Control and Status Register */
  __IO uint32_t LOAD;                         /*!< Offset: 0x04  SysTick Reload Value Register       */
  __IO uint32_t VAL;                          /*!< Offset: 0x08  SysTick Current Value Register      */
  __I  uint32_t CALIB;                        /*!< Offset: 0x0C  SysTick Calibration Register        */
} SysTick_Type;

它们的各位描述如下面的表格所述:


对于STM32,外部时钟源(STCLK)是HCLK(AHB总线时钟)的1/8,内核时钟(FCLK)是HCLK(AHB总线时钟)。



SysTick相关库函数

SysTick_CLKSourceConfig(),这是Systick的时钟源选择,直接配置CTRL寄存器的值。假设HCLK为72MHz,选用外部时钟源,那么SysTick的时钟即9MHz。这就意味着,SysTick的计数器VAL每减1代表时间过去了1/9us。

具体的定义在misc.c文件中。

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;
  }
}

SysTick_Config(uint32_t ticks) ,这是SysTick的初始化函数,时钟为HCLK,并开启SysTick中断。其中函数的参数表示两次中断之间时间间隔期间的SysTick周期,即两次中断之间有多少个SysTick周期。

具体的定义在core_cm3.h文件中。

static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                               
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}

void SysTick_Handler(void),这是SysTick的中断服务函数。我们举一个例子,利用中断的方式实现delay延时函数,见下面的程序:

static __IO uint32_t TimingDelay;
void Delay(__IO uint32_t nTime)
{ 
   TimingDelay = nTime;
   while(TimingDelay != 0);
}
void SysTick_Handler(void)
{
    if (TimingDelay != 0x00) 
     { 
       TimingDelay--;
     }
}
 int main(void)
 {  …
    if (SysTick_Config(SystemCoreClock / 1000)) //systick时钟为HCLK,中断时间间隔1ms
     {
     while (1);
     }
    while(1)
     { Delay(200);//200ms
     … 
     }
}

Delay延时函数讲解

之前利用中断实现延时函数,但是一直使用中断会造成资源的浪费,不建议这样实现,我们利用查询的方式实现delay延时。下面主要介绍Delay延时函数的实现:

delay_init()

首先是delay_init(),延时初始化函数。利用Syst_CLKSourceConfig()函数选择SysTick时钟源,选择外部时钟(HCLK的1/8);同时初始化fac_us和fac_ms两个变量。

void delay_init()
{
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);	//选择外部时钟  HCLK/8
	fac_us=SystemCoreClock/8000000;	      //为系统时钟的1/8,实际上也就是在计算1usSysTick的VAL减的数目
	fac_ms=(u16)fac_us*1000;		//代表每个ms需要的systick时钟数,即每毫秒SysTick的VAL减的数目   
}

delay_ms()

其次,delay_ms(),此函数用来延时指定的ms。

此时要注意nms的范围,SysTick->LOAD为24位寄存器,所以最大延时为:nms<=0xffffff*8*1000/SYSCLK;SYSCLK单位为Hz,nms单位为ms。对72M条件下,nms<=1864。如果超出这个值,建议多次调用此函数来实现。

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)));	//等待时间到达,看CTRL的第16位(COUNTFLAG)是否为1,看STRL的第0位(ENABLE)是否为1   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;       //关闭计数器
	SysTick->VAL =0X00;       //清空计数器	  	    
} 

这段代码其实就是先把延时的时间换算成SysTick的时钟周期数,然后写入LOAD寄存器。然后清空当前寄存器VAL的内容,再开启倒数功能。等倒数结束即延时了nms、最后关闭SysTick,清空VAL的值,实现一次延时的操作。

这里特别说一下,temp&0x01,这一句用来判断SysTick定时器是否还处在开启的状态,可以防止SysTick被意外关闭导致的死循环。

delay_us()

最后,delay_us(),此函数用来延时指定的us。具体的逻辑和上面一个函数类似,就不介绍了。

void delay_us(u32 nus)
{		
	u32 temp;	    	 
	SysTick->LOAD=nus*fac_us; 				//时间加载	  		 
	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;       //清空计数器	 
}

delay延时的相关函数在SYSTEM文件夹下的delay子文件夹,在使用delay_ms()或者delay_us()函数之前一定不要忘记先初始化delay_init()。

来源:CSDN,作者:Yngz_Miao,转载此文目的在于传递更多信息,版权归原作者所有。
原文:
https://blog.csdn.net/qq_38410730/article/details/79843806
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 16

Cortex-M3内核支持256个中断,其中包含了16个内核中断(异常)和240个外部中断,并且具有256级的可编程中断设置。但是,STM32并没有使用CM3内核的全部东西,而是只用了它的一部分。STM32有84个中断,包括16个内核中断(异常)和68个可屏蔽中断,具有16级可编程的中断优先级。而STM32F103系列上面,16个内核中断(异常)不变,而可屏蔽中断只有60个(在107系列才有68个)。

注意一下:CM3的外部中断和STM32的外部中断不是一个概念。CM3:除了内核异常之外的都是外部中断;STM32:外部中断EXTI只有6个

其实,内核中断也叫内核异常。

  • 中断是指系统停止当前正在运行的程序转而其他服务,可能是程序接收了比自身高优先级的请求,或者是人为设置中断,中断是属于正常现象。
  •  

  • 异常是指由于cpu本身故障、程序故障或者请求服务等引起的错误,异常属于不正常现象。

因此,下面就直接介绍一下STM32F103系列的16个内核中断(异常)、60个中断:



在中断向量表中从优先级7-66(中断号从0-59)代表着STM32F103的60个中断。优先级号越小,优先级越高。当表中的某处异常或中断被触发,程序计数器指针(PC)将跳转到该异常或中断的地址处执行,该地址处存放这一条跳转指令,跳转到该异常或中断的服务函数处执行相应的功能。因此,异常和中断向量表只能用汇编语言编写。

在MDK中,有标准的异常和中断向量表文件可以使用(startup_stm32f10x_hd.s),在其中标明了中断处理函数的名称,不能随意定义。而中断通道NVIC_IRQChannel(即IRQn_Type类型)是在stm32f10x.h文件中进行了宏定义。

什么是NVIC?即嵌套向量中断控制器(Nested Vectored Interrupt Controller)。CM3的中有一个强大而方便的NVIC,它是属于Cortex内核的器件,中断向量表中60个中断都由它来处理。NVIC是Cortex-M3核心的一部分,关于它的资料不在《STM32的技术参考手册》中,应查阅ARM公司的《Cortex-M3技术参考手册》。Cortex-M3的向量中断统一由NVIC管理。

NVIC的核心功能是中断优先级分组、中断优先级的配置、读中断请求标志、清除中断请求标志、使能中断、清除中断等,它控制着STM32中断向量表中中断号为0-59的60个中断!!外部中断信号从核外发出,信号最终要传递到NVIC(嵌套向量中断控制器)。NVIC跟内核紧密耦合,它控制着整个芯片中断的相关功能。

STM32中断优先级分组

中断优先级分组寄存器

这60个中断,怎么管理呢?这就涉及到STM32的中断分组。STM32可以将中断分成5个组,分别为组0-4;同时,对每个中断设置一个抢占优先级和响应优先级。分组配置是由SCB->AIRCR寄存器的bit10-8来定义的。SCB->AIRCR是在哪里的呢?由于这是CM3内核定义的,在文档《Cortex-M3权威指南(中文)》中能查找到。

具体的分配关系如下所示:


其中AIRCR寄存器来确定是用哪种分组,IP寄存器是相对应于那种分组抢占优先级和响应优先级的分配比例。例如组设置成3,那么此时所有的60个中断优先寄存器高4位中的最高3位是抢占优先级,低1位为响应优先级。CM3中定义了8个Bit用于设置中断源的优先级,而STM32只选用其中的4个Bit。

抢占优先级的级别高于响应优先级,而数值越小所代表的的优先级越高。 

介绍一下抢占优先级、响应优先级的区别:

① 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的;

② 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断;

③ 抢占优先级相同的中断,当两个中断同时发生的情况下,哪个响应优先级高,哪个先执行;

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

除此之外有两点需要注意:

① 打断的情况只会与抢占优先级有关, 和响应优先级无关!

② 一般情况下,系统代码执行过程中,只设置一次中断优先级分组,比如分组2,设置好分组之后一般不会再改变分组。随意改变分组会导致中断管理混乱,程序出现意想不到的执行结果。

中断优先级分组库函数

CM3核的优先级分组方式,使用的设置函数NVIC_SetPriorityGrouping()。

接下来介绍STM32的中断优先级分组函数NVIC_PriorityGroupConfig(),用来进行中断分组设置的,此函数是在固件库下misc.c文件中(文件目录是:

STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src\misc.c):

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;
}

函数的参数的取值,是在同文件中进行宏定义的:

#define NVIC_PriorityGroup_0         ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
                                                            4 bits for subpriority */
#define NVIC_PriorityGroup_1         ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
                                                            3 bits for subpriority */
#define NVIC_PriorityGroup_2         ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
                                                            2 bits for subpriority */
#define NVIC_PriorityGroup_3         ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
                                                            1 bits for subpriority */
#define NVIC_PriorityGroup_4         ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
                                                            0 bits for subpriority */

STM32中断优先级管理

中断优先级设置寄存器

分组设置好了之后,怎么设置单个中断的抢占优先级和响应优先级?

MDK为与NVIC相关的寄存器定义了如下的结构体,控制着中断向量表中60个中断(由于与中断内核有关,定义在core_cm3.h文件中):

typedef struct
{
  __IO uint32_t ISER[8];                      /*!< Offset: 0x000  Interrupt Set Enable Register           */
       uint32_t RESERVED0[24];                                   
  __IO uint32_t ICER[8];                      /*!< Offset: 0x080  Interrupt Clear Enable Register         */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];                      /*!< Offset: 0x100  Interrupt Set Pending Register          */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];                      /*!< Offset: 0x180  Interrupt Clear Pending Register        */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];                      /*!< Offset: 0x200  Interrupt Active bit Register           */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];                      /*!< Offset: 0x300  Interrupt Priority Register (8Bit wide) */
       uint32_t RESERVED5[644];                                  
  __O  uint32_t STIR;                         /*!< Offset: 0xE00  Software Trigger Interrupt Register     */
}  NVIC_Type;     

我们依次介绍一下这些寄存器:

先介绍几个寄存器组长度为8,这些寄存器是32位寄存器。由于STM32只有60个可屏蔽中断,8个32位寄存器中只需要2个就有64位了,每1位控制一个中断。

  • ISER[8](Interrupt Set-Enable Registers):中断使能寄存器。其中只使用到了ISER[0]和ISER[1],ISER[0]的bit0~bit31分别对应中断0~31。ISER[1]的bit0~27对应中断32~59。要使能某个中断,就必须设置相应的ISER位为1,使该中断被使能(这仅仅是使能,还要配合中断分组、屏蔽、I/O口映射等设置才算完整)。具体每一位对应哪个中断参考stm32f103x.h里面第140行。
  • ICER[8](Interrupt Clear-Enable Registers):中断移除寄存器。该寄存器的作用于ISER相反。这里专门设置一个ICER来清除中断位,而不是向ISER位写0,是因为NVIC的寄存器写1有效,写0无效。
  • ISPR[8](Interrupt Set-Pending Registers):中断挂起控制寄存器。通过置1可以将正在进行的中断挂起,执行同级或者更高级别的中断。写0无效。
  • ICPR[8](Interrupt Clear-Pending Registers):中断解挂控制寄存器。通过置1可以将正在挂起的中断解挂。写0无效。
  • IABR[8](Interrupt Active-Bit Registers):中断激活标志位寄存器。这是一个只读寄存器,可以知道当前在执行的中断是哪一个(为1),在中断执行完后硬件自动清零。

最后,介绍一个寄存器组长度为240,这个寄存器为8位寄存器。240个8位寄存器,每个中断使用一个寄存器来确定优先级。由于CM3由240个外部中断,所以这个寄存器组的数目就是240(注意与上面寄存器的区别,一个是一个寄存器控制一个,一个是一位控制一个)。

  • IP[240](Interrupt Priority Registers):中断优先级控制的寄存器。这是用来控制每个中断的优先级。由于STM32F10x系列一共60个可屏蔽中断,故使用IP[59]~IP[0]。其中每个IP寄存器的高4位[7:4]用来设置抢占和响应优先级(根据分组),低4位没有用到。而两个优先级各占几个位又要由上面讲到的中断优先级分组决定。

中断优先级设置库函数

接下来介绍如何使用库函数实现中断优先级管理,这里使用NVIC_Init()函数来进行对每个中断优先级的设置(misc.c文件中):

void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
{
  uint32_t tmppriority = 0x00, tmppre = 0x00, tmpsub = 0x0F;
  
  /* Check the parameters */
  assert_param(IS_FUNCTIONAL_STATE(NVIC_InitStruct->NVIC_IRQChannelCmd));
  assert_param(IS_NVIC_PREEMPTION_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority));  
  assert_param(IS_NVIC_SUB_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelSubPriority));
    
  if (NVIC_InitStruct->NVIC_IRQChannelCmd != DISABLE)
  {
    /* Compute the Corresponding IRQ Priority --------------------------------*/    
    tmppriority = (0x700 - ((SCB->AIRCR) & (uint32_t)0x700))> 0x08;
    tmppre = (0x4 - tmppriority);
    tmpsub = tmpsub >> tmppriority;
 
    tmppriority = (uint32_t)NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority << tmppre;
    tmppriority |=  NVIC_InitStruct->NVIC_IRQChannelSubPriority & tmpsub;
    tmppriority = tmppriority << 0x04;
        
    NVIC->IP[NVIC_InitStruct->NVIC_IRQChannel] = tmppriority;
    
    /* Enable the Selected IRQ Channels --------------------------------------*/
    NVIC->ISER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
      (uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
  }
  else
  {
    /* Disable the Selected IRQ Channels -------------------------------------*/
    NVIC->ICER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
      (uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
  }
}

其中,NVIC_InitTypeDef为一个结构体,它的成员变量为:

typedef struct
{
  uint8_t NVIC_IRQChannel;                    /*!< Specifies the IRQ channel to be enabled or disabled.
                                                   This parameter can be a value of @ref IRQn_Type 
                                                   (For the complete STM32 Devices IRQ Channels list, please
                                                    refer to stm32f10x.h file) */
 
  uint8_t NVIC_IRQChannelPreemptionPriority;  /*!< Specifies the pre-emption priority for the IRQ channel
                                                   specified in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */
 
  uint8_t NVIC_IRQChannelSubPriority;         /*!< Specifies the subpriority level for the IRQ channel specified
                                                   in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */
 
  FunctionalState NVIC_IRQChannelCmd;         /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
                                                   will be enabled or disabled. 
                                                   This parameter can be set either to ENABLE or DISABLE */   
} NVIC_InitTypeDef;

NVIC_InitTypeDef结构体有4个成员变量:

① NVIC_IRQChannel:定义初始化的是哪一个中断,这个可以在stm32f10x.h文件中查到每个中断对应的名字,如USART1_IRQn;

② NVIC_IRQChannelPreemptionPriority:定义此中断的抢占优先级别;

③ NVIC_IRQChannelSubPriority:定义此中断的响应优先级别;

④ NVIC_IRQChannelCmd:该中断是否使能。

其实我们看NVIC_Init()函数内部使能中断,也是通过ISER寄存器配置的。这与我么之前的内容并不矛盾。函数内部使用NVIC->ISER,而NVIC是一个宏定义,

#define NVIC     ((NVIC_Type *) NVIC_BASE)     /*!< NVIC configuration struct         */

也就是直接操作结构体来实现操作ISER寄存器。

比如,使能串口1中断,抢占优先级为1,响应优先级为2,初始化的方法为:

NVIC_InitTypeDef   NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//IRQ通道使能
NVIC_Init(&NVIC_InitStructure);	//根据上面指定的参数初始化NVIC寄存器

总结与分析

最后总结一下中断优先级设置的步骤:

1、系统运行后先设置中断优先级分组。调用函数:

void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
整个系统执行过程中,只设置一次中断分组;

2、针对每个中断,设置对应的抢占优先级和响应优先级:

void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

来源:CSDN,作者:Yngz_Miao
原文:
https://blog.csdn.net/qq_38410730/article/details/79829983
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 100

简介

本文描述了如何使用在搭载了 RT-Thread 系统的 STM32 平台上使用 C++,包括 C++ 的配置和应用等,并给出了在STM32F411 NUCLEO开发板上验证的代码示例。

硬件平台简介

本文基于意法半导体 STM32F411 NUCLEO开发板,给出了 C++ 的具体应用示例代码,由于RT-Thread上层应用API的通用性,因此这些代码不局限于具体的硬件平台,用户可以轻松将它移植到其它平台上。

STM32F411 NUCLEO是意法半导体推出的一款基于ARM Cortex-M4内核的开发板,最高主频为100Mhz,该开发板具有丰富的板载资源,可以充分发挥STM32F411RE 的芯片性能。

STM32F411RE从属于销量名列前茅的STM32F4系列,众所周知,F4是STM32主打高性能和数字信号处理的“轻奢”系列。

“奢侈”在F4作为内核为Cortex-M4 (DSP+FPU)的MCU,可选180MHz 主频、2M Flash/384KB RAM、Chrom-ART加速器、MPI-DSI接口、延伸到125度的工作温度、DFSDM数字滤波器以及各种常见的音频(SAI)、连接(Ethernet、Camera、USB)、控制(CAN、UART、I2C)、存储(FMC、2/4/8 bits SPI、SDMMC)外设。

“轻”在价格让人“轻松”、尺寸“轻巧”(不到3mm*3mm的封装)、功耗“轻微”。


准备工作

1、下载 RT-Thread 源码

2、下载 ENV 工具

3、进入rt-thread\bsp\stm32f411-st-nucleo 目录,检查 BSP rtconfig.py 文件和 SConstruct 文件是否支持 C++ 配置,如下图所示

检查 rtconfig.py 文件中对 C++ 的支持


检查 SConstruct 文件中对 C++ 的支持


打开 C++ 支持:

打开 Env 工具,在 Env 命令行中输入 menuconfig,进入配置界面,使用 menuconfig 工具(学习如何使用)配置工程。在 menuconfig 配置界面依次选择 RT-Thread Components ---> C++ features ---> Support C++ features,如图所示:


编译工程: scons --target=mdk5 1. 生成 mdk5 工程,将示例代码附带的 main.cpp 替换掉 BSP 中的 main.c 并重新加入到工程中,如图所示:


编译,下载程序,在终端输入 help 命令可以看到 test_cpp 已经添加成功了。


运行 C++ 程序:

在终端输入 test_cpp 运行结果如下图所示。


C++ 全局对象构造函数的调用

RT-Thread 中对全局对象构造函数的实现中,以 GNUC 为例,在 rt-thread\components\cplusplus 目录下的 crt_init.c 文件中对 C++ 进行了系统初始化, 在特定的 BSP 目录下,连接脚本文件 link.lds 为 C++ 全局构造函数的代码分配了段,使 C++ 全局对象构造函数链接后能够存放在指定的段中。如下图所示:


crt_init.c 文件完成了 C++ 系统的初始化工作

C++ 系统初始化部分:


在 cplusplus_system_init 函数中,将全局对象的构造函数依次链接到了链接脚本文件中为其分配的段中,并且调用了 RT-Thread 组件自动初始化的宏 INIT_COMPONENT_EXPORT,所以在链接的时候,C++全局对象构造函数所产生的目标文件就被链接到了__ctors_start__和__ctors_end__组成的段中。

链接脚本中为 C++ 全局构造函数分配的段部分:


__ctors_start__ 分配了 C++ 全局构造函数段的起始地址, __ctors_end__ 分配了 C++ 全局构造函数段的结束地址,所以全局构造函数在系统初始化的时候,就会被链接到这里分配的段地址中。

RT-Thread C++ 异常说明

同样,在链接脚本文件 link.lds 中,也为 C++ 异常分配了段地址:


__exidx_start 分配了 C++ 异常的起始地址, __exidx_end 分配了 C++ 异常的结束地址,当异常产生的时候,就会被分配到指定的段地址中。

这里以一个 C++ 除零异常的抛出和捕获为例:


当除零异常发生的时候 div_func 函数会抛出一个异常,在 throw_exceptions 函数中会去捕获这个异常。

下载代码,并在终端输入 throw_exceptions 运行结果如下图所示。


到这一步为止,如何在搭载了 RT-Thread 系统的 STM32 平台上如何使用 C++ 的介绍就结束了。

参考资料

1、ENV 用户手册
https://www.rt-thread.org/document/site/programming-manual/env/env/

2、STM32F411-ST-NUCLEO BSP 源码
https://github.com/RT-Thread/rt-thread/tree/master/bsp/stm32/stm32f411-s...

本文转自网络,转载此文目的在于传递更多信息,版权归原作者所有。

围观 29

STM32F1xx官方资料:《STM32中文参考手册V10》-第8章通用和复用功能IO(GPIO和AFIO)

端口复用功能

端口复用的定义

STM32有许多的内置外设(如串口、ADC、DCA等等),这些外设的外部引脚都是和GPIO复用的。也就是说,一个GPIO如果可以复用为内置外设的功能引脚,那么当这个GPIO作为内置外设使用的时候,就叫复用。详细的可以参考《STM32F103ZET6数据手册》p30的内容,表格的倒数第二栏就表示端口复用功能。

比如说,STM32的串口1的引脚对应的I/O位PA9、PA10。而PA9、PA10默认的功能都是GPIO,所以说当PA9、PA10引脚作为串口1使用的时候就是端口复用。


那么,什么时候端口是默认功能,什么时候端口是复用功能呢?

STM32时钟系统的配置除了初始化的时候在system_stm32f10x.c中的SystemInit函数中外,其他的配置主要在stm32f10x_rcc.c文件中, 所以GPIO等等外设的时钟使能函数都是在此文件中。同时我们通过函数名可以得到规律:GPIOA-GPIOC是挂载在APB2下面,TIM2-TIM4是挂载在APB1下面,DMA是挂载在AHB下面。所以调用函数的名称是需要根据这个来确定的。

端口复用初始化过程

接下来看一下端口复用初始化过程的步骤,拿串口1为例:

1、GPIO端口时钟使能。要使用到端口复用,首先是要使能端口的时钟了;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

2、复用的外设时钟使能。比如要将PA9、PA10引脚复用成串口,必须也要使能串口时钟;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

3、端口模式配置。在I/O复用位内置外设功能引脚的时候,必须设置GPIO端口的模式。至于在复用功能下,GPIO的模式怎么设置,可以查看手册《STM32中文参考手册》p110的内容。这里拿USART1为例,进行配置,要配置全双工的串口1,TX引脚需要推挽复用输出,RX引脚需要浮空输入或者上拉输入;


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);
  
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10 PA.10 浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);  

总而言之,使用复用功能的时候至少要使能2时钟:GPIO时钟使能、复用的外设时钟使能。同时还要初始化GPIO以及复用外设功能(端口模式配置)。

端口重映射

端口重映射的定义

为了使不同的器件封装的外设I/O功能数量达到最优,可以把一些复用功能重新映射到其他的引脚上。STM32中有许多的内置外设的输入、输出引脚都具有重映射(Remap)的功能。

我们知道,每个内置外设都有若干个输入、输出引脚,一般这些引脚的输出端口都是固定不变的,为了更好地安排引脚的走向和功能,在STM32中引入了外设引脚重映射的概念,即一个外设的引脚除了具有默认的端口之外,还可以通过设定重映射寄存器的方式把这个外设的引脚映射到其他的端口。

简单讲,就是把引脚的外设功能映射到其他的引脚上,但不是可以随便映射的,具体的对应关系参考《STM32F103ZET6数据手册》p30的内容,表格的最后一栏就表示端口重映射功能。

这里同样用串口1为例来说明。


可以看出,我们可以将串口1重映射到PB6、PB7引脚上。

端口重映射初始化过程

接下来看一下端口重映射初始化过程的步骤,拿串口1为例,除了之前使能复用功能的2个时钟之外,还需要使能AFIO功能时钟,然后调用重映射函数:

1、GPIO端口时钟使能。要使用到端口复用,首先是要使能端口的时钟了;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

2、复用的外设时钟使能。比如要将PB6、PB7引脚复用成串口,必须也要使能串口时钟;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

3、使能AFIO时钟。重映射必须使能AFIO时钟;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

4、开启重映射;

GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);

这样,就将串口1的TX和RX引脚映射到PB6、PB7引脚上面了。至于哪些功能可以重映射,除了查看中文参考手册之外,还可以从GPIO_PinRemapConfig函数入手查看第一个入口参数的取值范围的值。stm32f10x_gpio.h中定义了一些宏定义的标识符:

#define GPIO_Remap_SPI1             ((uint32_t)0x00000001)  /*!< SPI1 Alternate Function mapping */
#define GPIO_Remap_I2C1             ((uint32_t)0x00000002)  /*!< I2C1 Alternate Function mapping */
#define GPIO_Remap_USART1           ((uint32_t)0x00000004)  /*!< USART1 Alternate Function mapping */
#define GPIO_Remap_USART2           ((uint32_t)0x00000008)  /*!< USART2 Alternate Function mapping */
#define GPIO_PartialRemap_USART3    ((uint32_t)0x00140010)  /*!< USART3 Partial Alternate Function mapping */
#define GPIO_FullRemap_USART3       ((uint32_t)0x00140030)  /*!< USART3 Full Alternate Function mapping */
#define GPIO_PartialRemap_TIM1      ((uint32_t)0x00160040)  /*!< TIM1 Partial Alternate Function mapping */
#define GPIO_FullRemap_TIM1         ((uint32_t)0x001600C0)  /*!< TIM1 Full Alternate Function mapping */
#define GPIO_PartialRemap1_TIM2     ((uint32_t)0x00180100)  /*!< TIM2 Partial1 Alternate Function mapping */
#define GPIO_PartialRemap2_TIM2     ((uint32_t)0x00180200)  /*!< TIM2 Partial2 Alternate Function mapping */
#define GPIO_FullRemap_TIM2         ((uint32_t)0x00180300)  /*!< TIM2 Full Alternate Function mapping */
#define GPIO_PartialRemap_TIM3      ((uint32_t)0x001A0800)  /*!< TIM3 Partial Alternate Function mapping */
#define GPIO_FullRemap_TIM3         ((uint32_t)0x001A0C00)  /*!< TIM3 Full Alternate Function mapping */

可以看出,USART1只有一种重映射,而USART3存在部分重映射和完全重映射。所谓部分重映射就是部分引脚和默认的是一样的,完全重映射就是所有引脚都映射到了新的引脚。可以查看《STM32中文参考手册》p119的内容查看部分重映射和完全重映射的内容。而在之前最后开启重映射的函数中,根据第一个参数,来确定是部分重映射还是全部重映射。

AFIO辅助功能时钟

之前在端口重映射的时候,讲到要使能AFIO辅助功能时钟。那么什么时候需要开启(使能)呢?

对寄存器AFIO_MAPR、AFIO_EXTICRx和AFIO_EVCR进行读写操作前,应当首先开启AFIO时钟。

来源:CSDN,作者:Yngz_Miao
原文:
https://blog.csdn.net/qq_38410730/article/details/79828852
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 249

时钟系统就是CPU的脉搏,像人的心跳一样,重要性不言而喻。由于STM32本身十分复杂,外设非常多,但并不是所有的外设都需要系统时钟那么高的频率,比如看门狗以及RTC只需要几十k的时钟即可。并且,同一个电路,时钟越快功耗越快,同时抗电磁干扰能力也就越弱,所以较为复杂的MCU都是采用多时钟源的方法来解决这些问题。

STM32F1xx官方资料:
《STM32中文参考手册V10》-第六章 复位和时钟控制 RCC

STM32的时钟系统

STM32的时钟系统图

上图是STM32的时钟系统图。STM32 有5个时钟源:HSI、HSE、LSI、LSE、PLL,在图中有红色方框标记的位置。从时钟频率来分可以分成高速时钟源和低速时钟源,在这5个中HSI、HSE和PLL是高速时钟源,LSI和LSE是低速时钟源。从时钟来源来分可以分成外部时钟源和内部时钟源。外部时钟源就是从外部通过接晶振的方式获取时钟源。其中,HSE和ISE是外部时钟源,其他的是内部时钟源。

下面来看看STM32的5个时钟源:

① HSI(High Speed Internal)是高速内部时钟,RC振荡器,频率为8MHz,精度不高;

② HSE(High Speed External)是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz;

③ LSI(Low Speed Internal)是低速内部时钟,RC振荡器,频率为40kHz,提供低功耗时钟。独立看门狗的时钟源只能是LSI,同时LSI还可以做PTC的时钟源;

④ LSE(Low Speed External)是低速外部时钟,接频率为32.768kHz的石英晶体。这个主要是RTC的时钟源。

⑤ PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。

下面分析一下,图中A-E标识的五个地方:

A:STM32可以选择一个时钟信号输出到MCO脚(PA8,时钟输出引脚)上,可以选择为PLL输出的2分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源;

B:这里是RTC的时钟源,可以选择LSI、LSE以及HSE的128分频;

C:此处的USB时钟源来自于PLL时钟源。STM32有一个全速功能的USB模块,其串行接口引擎需要一个48MHz的时钟源。该时钟源只能由PLL输出端获取,若PLL输出72MHz,则1.5分频;若PLL输出48MHz,则1分频。也就是说,当需要使用USB模块的时候,PLL必须使能;

D:STM32的系统时钟SYSCLK,提供STM32的绝大多数部件工作的时钟源。它的来源可以是三个时钟源:HSI振荡器时钟、HSE振荡器时钟和PLL时钟。系统时钟的最大频率为72MHz。

E:这里指的就是其他的所有外设了,这些外设的时钟来源都是SYSCLK。SYSCLK通过AHB分频器分频后送给各模块使用。

这些模块包括:
① AHB总线、内核、内存和DMA使用的HCLK时钟(最大72MHz);
② 通过8分频后送给Cortex的系统定时器时钟,也就是systick了;
③ 直接送给Cortex的空闲运行时钟FCLK;
④ 送给APB1分频器。APB1分频器输出一路给APB1外设使用(PCLK1,最大频率36MHz),另一路给通用定时器使用;
⑤ 送给APB2分频器。APB2分频器输出一路给APB2外设使用(PCLK2,最大频率72MHz),另一路给定时器使用;

APB1和APB2的区别

这里需要理解一下APB1和APB2的区别:


APB1上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、USART2、USART3、UART4、UART5、SPI2、SP3等;

而APB2上面连接的是高速外设,包括UART1、SPI1、Timer1、ADC1、ADC2、ADC3、所有的普通I/O口(PA-PE)、第二功能I/O(AFIO)口等。

在上面的时钟输出中,有很多是带使能控制的,例如AHB总线时钟、内核时钟、各种APB1外设、APB2外设等等。当使用某模块时,记得一定要先使能其相应的时钟。

SystemInit函数

STM32时钟系统的配置除了初始化的时候在system_stm32f10x.c中的SystemInit函数中外,其他的配置主要在stm32f10x_rcc.c文件中,需要对这个文件好好研究一下。

本文主要看一下初始化时的SystemInit函数:

SystemInit函数主要就是完成系统初始化时的默认状态的设置,同时系统时钟默认是在SetSysClock()函数中来进行判断的,而判断的依据则是通过宏定义设置的。先看看SetSysClocks()函数:

static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
  SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
  SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
  SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
  SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
  SetSysClockTo56();  
#elif defined SYSCLK_FREQ_72MHz
  SetSysClockTo72();
#endif
 
 /* If none of the define above is enabled, the HSI is used as System clock
    source (default after reset) */ 
}

这段代码很简单,就是判断系统宏定义的时钟是多少,然后设置相应值。系统默认的宏定义是72HMz:

#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

接下来就选择进入SetSysClockTo72()函数,我们看一下这个函数的内容:

这里主要的内容就是下面这一段:

    /* HCLK = SYSCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
      
    /* PCLK2 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
    
    /* PCLK1 = HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
 
    /*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);

设置SYSCLK为72MHz,HCLK=SYSCLK,PCLK1=SYSCLK/2,PCLK2=SYSCLK。总而言之,即通过HSE(8MHz)倍频9倍成SYSCLK(72MHz),然后直接输出为HCLK(72MHz)、PCLK2(72HMz)、PCLK1(36HMz)。

相对应的,如果说宏定义不是SYSCLK_FREQ_72MHz ,而是其他的宏定义。那么就会进入各自的SetSysClockToxx()函数,比如说SetSysClockTo54()、SetSysClockTo36()等等。然后在相应的程序中,会对SYSCLK等进行赋值。

同时,在设置好系统时钟之后,可以通过变量SystemCoreClock来获取系统时钟值。这也是在stm32f10x.c文件中设置的:

总结与分析

这里总结一下SystemInit函数默认设置的系统时钟的大小:


来源:CSDN,作者:Yngz_Miao
原文:https://blog.csdn.net/qq_38410730/article/details/79826397
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 218

对于MCU,一切底层配置,最终都是在配置寄存器。

STM32F1xx官方资料:《STM32中文参考手册V10》-第8章通用和复用功能IO(GPIO和AFIO)

51单片机访问地址

51单片机经常会引用一个reg51.h的头文件。

下面看看它是怎么把名字和寄存器联系在一起的:

sfr p0=0x80;
p0=0x00;

sfr是一种扩充数据类型,点用一个内存单位,值域为0-255.利用它可以访问51单片机内部所有的特殊功能寄存器。前一句“sfr p0=0x80”就是将P0映射到地址0x80。后一句“p0=0x00”就是往p0地址(0x80)代表的寄存器写值。

STM32访问地址

寄存器地址名称映射

STM32肯定也是可以这样来设置寄存器的。但是由于STM32的寄存器数目太多了,如果以这样的方式列出来,需要很大的篇幅,而且也不方便开发。所以,MDK采用的方式是通过结构体来将寄存器组织在一起。

下面就介绍MDK如何把结构体和地址对应起来的,为什么修改结构体成员变量的值就可以达到操作寄存器的值?这些事情都是在stm32f10x.h文件中完成的。

注:stm32f10x.h文件在STM32固件库下的目录是:

STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x文件夹下。

GPIOA的寄存器地址名称映射

我们通过GPIOA的寄存器为例来进行介绍。

GPIOA->ODR=0x00000000;

首先,我们需要看一下GPIOA是个什么东西?通过宏定义我们可以看到:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)

GPIOA是一个将GPIOA_BASE强制转换成GPIO_TypeDef的指针。这句话的意思就是,GPIOA指向地址GPIOA_BASE,而GPIOA_BASE存放的数据类型是GPIO_TypeDef。再看一下结构体GPIO_TypeDef的定义:

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

结构体里面声明了7个变量,这个时候就明白了“GPIOA->ODR”就是指:GPIOA结构体下的ODR变量。

其实结构体的7个变量就是GPIOA的7个寄存器。我们需要知道GPIOA下的ODR寄存器的地址,首先需要知道的是GPIOA的基地址是怎么计算的呢?

#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE            (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE            (APB2PERIPH_BASE + 0x2000)

因为GPIO都是挂载在APB2总线之上的,所以它的基地址是由APB2总线的基地址+GPIO在APB2总线上的偏移地址决定的。那么APB2总线的基地址是怎么计算的呢?

#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

原理都是一样的,APB2总线的基地址也是从其他地址进行地址偏移得到的。

所以到这个时候,就可以算出GPIOA的基地址位了:

GPIOA_BASE=0x40000000+0x10000+0x0800=0x40010800

这上面就已经知道了GPIOA的基地址,那么那些GPIOA的7个寄存器的地址又是怎么计算出来的呢?


GPIOA的寄存器的地址=GPIOA基地址+寄存器相对GPIOA基地址的偏移值

寄存器相对于GPIOA基地址的偏移值可以在上面的寄存器地址映射表中查到。稍微解释一下:GPIO的每个寄存器都是32位的,所以每个寄存器是占用4个地址,也就是说一共占用28个地址。地址偏移范围为(000h-01Bh)。这个地址偏移是相对于GPIOA的基地址而言的。

那么你可能又有一个疑问:结构体里面的寄存器又是怎么与地址一一对应的呢?这就涉及到结构体的一个特征,那就是结构体存储的成员的地址是连续的。上面讲到GPIOA是指向GPIO_TypeDef类型的指针,又由于GPIO_TypeDef是结构体,所以自然而然我们就可以算出GPIOA指向的结构体成员变量对应地址了。

总结与分析

对于STM32而言,使用“GPIOA->ODR=0x00000000;”来对寄存器赋值的原理,也就是将GPIO下的所有寄存器放在一个结构体内,通过基地址和在基地址上的偏移地址不断转化,最终找到准确的寄存器实际地址来进行赋值。也就是说,和51单片机最大的不同就是:由于STM32的寄存器数目太多,就将其中控制同一外设的寄存器设置成一个结构体(如GPIO、DMA等),通过对结构体的地址和寄存器相对于结构体的偏移地址,来确定某个特定的寄存器。

来源:CSDN,作者:Yngz_Miao
原文:
https://blog.csdn.net/qq_38410730/article/details/79816270
版权声明:本文为博主原创文章,转载请附上博文链接!

围观 115

最近在试用uFUN开发板,下载配套的Demo程序,串口数据输出正常,当使用另一个模板工程,调用串口printf调试功能时,输出的却是乱码,最后发现是外部晶振频率不一样。很多STM32开发板都是使用的8M晶振,这个也是ST官方推荐的晶振频率,而且固件库默认是8M频率,倍频系数9。而uFUN开发板的晶振是和CH340共用一个12M晶振。如果固件库的参数不和硬件实际连接的晶振频率一致,那么不仅是串口会出现乱码,而且定时器这些也是不准确的,因为基本的工作时钟被打乱了。其实之前也遇到过这个问题,这次就算是记录一下吧!


1. 修改stm32f10x.h文件中的晶振频率

打开工程中的stm32f10x.h文件,Ctrl+G快捷键定位到119行,把宏定义

#define HSE_VALUE ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
外部晶振频率8M修改为12M

#define HSE_VALUE ((uint32_t)12000000) /*!< Value of the External oscillator in Hz */
如下图所示:


2. 修改system_stm32f10x.c文件中的倍频系数

打开工程中的system_stm32f10x.c文件,Ctrl+G快捷键定位到1056行,把宏定义

RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
倍频系数由9修改为6,因为STM32F103主频最高到72MHz,外部晶振改为12M后,所以倍频系数改为6

RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL6);
如下图所示:


总结

当然串口乱码可能不止这一个原因,这个解决方法只针对于外部晶振的改变,导致的串口数据乱码。其他问题还要仔细分析,总得来说,两个问题:程序问题,硬件问题。

参考资料:

STM32串口通信乱码详细处理方法

转自:王超的独立博客
原文链接: http://www.wangchaochao.top/2019/03/17/uFun-2/

围观 148

问题描述

某STM32用户反馈,当使用STM32L4芯片的时候,程序运行一段时间后,会忽然复位。复位后程序继续运行,但是还会继续复位,原因不详。

问题解析

01:初步确定复位的原因,是硬件复位,如外部NRST被拉低,还是软件复位,包括软件直接调用复位,或者看门狗复位,还是低功耗模式如standby模式被唤醒时产生中断。

02:查看复位状态寄存器了解复位大方向,然后做进一步得拆解分析。

03

目前客户项目的复位原因是因为看门狗复位,即客户使用了IWDG,但由于某种原因没有及时喂狗,导致IWDG超时复位。初步怀疑由于客户软件的问题,程序跑飞,进入异常处理。

因为客户的异常处理函数中并没有做任何动作,导致独立看门狗IWDG复位。基于此,我们先关闭IWDG,然后在所有的异常处理中,先加入死循环并打上断点,对异常原因进行捕捉。



04

正如我们所猜测,的确是由于程序跑飞导致。程序停在了voidHardFault_Handler(void) 。

通过查看SP以及回溯栈里面的内容,找到了对应的LR,具体方法如下:

当中断产生时,按照上图所示的顺序进行压栈,同时栈指针SP--,即: R0, R1, R2, R3, R12, LR, PC, xPSR。

如上图所示,当产生异常时,如果call stack窗口显示不出来的话,只能根据core的寄存器手动回溯栈,以找到出错时的指针。根据ARM core的说明,SP+6,即红框的部分,为中断处理后LR和PC,据此可以追溯函数异常时的位置。

05

根据出错时的PC和LR,发现是浮点运算的函数,初步判断是因为浮点运算导致,比如没有对齐导致的Hardfault,但实际检查发现,并不是浮点运算的问题。

06

问题一时陷入了僵局。但有一点是确定的,是因为栈的区域被异常覆盖或者改写导致产生hardfault。

07

由于问题可以稳定复现,采取逐个排除法最终发现了问题的所在:

当把一个局部数组变量改为全局数组时,问题消失!

由于局部数组变量是保存在栈当中,所以怀疑是对这个局部数组变量使用不当导致了栈被覆盖或者改写。

追查这个局部变量数组:



经检查发现,这个原先是8bit的局部变量的数组,在最后被强制转换成了uint32_t*类型的指针,由于是指针,在对其进行++或--操作时,都是按照4字节宽带操作的,这就相当于扩大了4倍,覆盖了后面的栈的内容,导致了程序跑飞。

小结

当芯片异常复位或者进入异常处理 (如Hard fault, Mem Manage, Bus fault等)时,首先考虑的是,如何快速的复现这个问题,当问题被稳定复现的时候,可以通过调试工具在异常处理的地方打上断点停留,这样就可以获取到栈指针SP,通过SP去看栈里面的内容去回溯栈。当然,如果栈的内容被无端改写时,栈里面的内容,如保存的LR就没有太大的参考意义。不过,可以通过观察栈里面的内容,去估测是哪个模块或者函数异常修改了栈的内容,进而定位最终的问题源。

来源: STM32单片机

围观 174

页面

订阅 RSS - STM32