STM32F407ZGT6寄存器版本代码学习笔记

代码均来自正点原子官网,函数结构清晰和注释写的非常清楚,大家可以自行下载。

正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档

1、跑马灯实验

Stm32_Clock_Init(336,8,2,7);//设置时钟,168Mhz

//这是时钟设置函数,具体配置如下
//Fvco=Fs*(plln/pllm);
//Fsys=Fvco/pllp=Fs*(plln/(pllm*pllp));
//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));

//Fvco:VCO频率
//Fsys:系统时钟频率
//Fusb:USB,SDIO,RNG等的时钟频率
//Fs:PLL输入时钟频率,可以是HSI,HSE等. 
//plln:主PLL倍频系数(PLL倍频),取值范围:64~432.
//pllm:主PLL和音频PLL分频系数(PLL之前的分频),取值范围:2~63.
//pllp:系统时钟的主PLL分频系数(PLL之后的分频),取值范围:2,4,6,8.(仅限这4个值!)
//pllq:USB/SDIO/随机数产生器等的主PLL分频系数(PLL之后的分频),取值范围:2~15.

//外部晶振为8M的时候,推荐值:plln=336,pllm=8,pllp=2,pllq=7.
//得到:Fvco=8*(336/8)=336Mhz
//     Fsys=336/2=168Mhz
//     Fusb=336/7=48Mhz
//返回值:0,成功;1,失败。
void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq)
{  
	RCC->CR|=0x00000001;		//设置HISON,开启内部高速RC振荡
	RCC->CFGR=0x00000000;		//CFGR清零 
	RCC->CR&=0xFEF6FFFF;		//HSEON,CSSON,PLLON清零 
	RCC->PLLCFGR=0x24003010;	//PLLCFGR恢复复位值 
	RCC->CR&=~(1<<18);			//HSEBYP清零,外部晶振不旁路
	RCC->CIR=0x00000000;		//禁止RCC时钟中断 
	Sys_Clock_Set(plln,pllm,pllp,pllq);//设置时钟 
	//配置向量表				  
#ifdef  VECT_TAB_RAM
	MY_NVIC_SetVectorTable(1<<29,0x0);
#else   
	MY_NVIC_SetVectorTable(0,0x0);
#endif 
}		    

配置时钟:

晶振8M的时候按照推荐值进行配置,配置完成后系统时钟是168MHZ;这里启用的是内部晶振,但是后续配置为了外部晶振,可能是因为复位后默认使用内部晶振,启用内部晶振之后转为外部晶振更稳定。

delay_init(168);		//初始化延时函数

函数具体内容如下
void delay_init(u8 SYSCLK)
{
#if SYSTEM_SUPPORT_OS 						//如果需要支持OS.
	u32 reload;
#endif
 	SysTick->CTRL&=~(1<<2);					//SYSTICK使用外部时钟源	 
	fac_us=SYSCLK/8;						//不论是否使用OS,fac_us都需要使用
#if SYSTEM_SUPPORT_OS 						//如果需要支持OS.
	reload=SYSCLK/8;						//每秒钟的计数次数 单位为M	   
	reload*=1000000/delay_ostickspersec;	//根据delay_ostickspersec设定溢出时间
											//reload为24位寄存器,最大值:16777216,在168M下,约合0.7989s左右	
	fac_ms=1000/delay_ostickspersec;		//代表OS可以延时的最少单位	   
	SysTick->CTRL|=1<<1;   					//开启SYSTICK中断
	SysTick->LOAD=reload; 					//每1/delay_ostickspersec秒中断一次	
	SysTick->CTRL|=1<<0;   					//开启SYSTICK    
#else
	fac_ms=(u16)fac_us*1000;				//非OS下,代表每个ms需要的systick时钟数   
#endif
}								    

延时函数初始化:

OS是指操作系统,暂时没有用到。SYSTICK的时钟固定为AHB时钟的1/8,也就是21MHZ,延时1ms要计数21000次,这个寄存器是24位的,最多可以计16777216个数完全够用的。具体怎么实现的可以跳到延时函数查看(代码均来自正点原子官网)。

LED_Init();				//初始化LED时钟  
函数具体内容如下
void LED_Init(void)
{    	 
	RCC->AHB1ENR|=1<<5;//使能PORTF时钟 
	GPIO_Set(GPIOF,PIN9|PIN10,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU); //PF9,PF10设置
	LED0=1;//LED0关闭
	LED1=1;//LED1关闭
}

LED初始化:

RCC->AHB1ENR |= 1 << 5;是使能 GPIOF 的时钟AHB1ENR 对应的是 AHB1 总线上的外设时钟使能,1 << 5 是将第五位设置为 1,表示启用 GPIOF。外设总线和位号需查看数据手册。

PF9和PF10设置为输出模式,GPIO_OTYPE_PP是指推挽输出(驱动能力较强),速度100M,PU是设置为上拉(悬空时为高电平)。

主循环很简单不做赘述。


2、按键中断实验

按键写在循环里没用中断,知识点和跑马灯差别不大不做赘述。

3、串口通信实验

	uart_init(84,115200);	//串口初始化为115200

串口初始化:

先说为什么是84,pclk2(APB2外设时钟)在分频时是2分频,所以168/2=84HZ。分频是因为数据手册规定APB2最大不能超过84M。

跳转到函数看一下具体内容

temp = (float)(pclk2 * 1000000) / (bound * 16);  // 计算USARTDIV,单位为赫兹(Hz)
mantissa = temp;  // 整数部分
fraction = (temp - mantissa) * 16;  // 小数部分
mantissa <<= 4;  // 左移4位,整数部分乘以16
mantissa += fraction;  // 波特率配置值:整数部分和小数部分合并

这些是计算实际波特率,一般使用标准波特率9600/115200等等就不用额外计算。

	RCC->AHB1ENR|=1<<0;   	//使能PORTA口时钟  
	RCC->APB2ENR|=1<<4;  	//使能串口1时钟 
	GPIO_Set(GPIOA,PIN9|PIN10,GPIO_MODE_AF,GPIO_OTYPE_PP,GPIO_SPEED_50M,GPIO_PUPD_PU);//PA9,PA10,复用功能,上拉输出
 	GPIO_AF_Set(GPIOA,9,7);	//PA9,AF7
	GPIO_AF_Set(GPIOA,10,7);//PA10,AF7  

使能和配置引脚不赘述,如需使用其它引脚按照需求进行修改

//波特率设置
 	USART1->BRR=mantissa; 	//波特率设置	 
	USART1->CR1&=~(1<<15); 	//设置OVER8=0 
	USART1->CR1|=1<<3;  	//串口发送使能 

这边配置的都是USART1,按照需求进行修改,波特率存到BRR寄存器。OVER8 = 0(标准 16 倍采样模式)每个波特周期使用 16 次采样;OVER8 = 1(8 倍采样模式)每个波特周期会进行 8 次采样。一般就用16倍。

	//使能接收中断 
	USART1->CR1|=1<<2;  	//串口接收使能
	USART1->CR1|=1<<5;    	//接收缓冲区非空中断使能	    	
	MY_NVIC_Init(3,3,USART1_IRQn,2);//组2,最低优先级 
#endif
	USART1->CR1|=1<<13;  	//串口使能
}

最后是串口中断使能和优先级配置。

主循环:

if(USART_RX_STA&0x8000)
		{					   
			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
			printf("\r\n您发送的消息为:\r\n");
			for(t=0;t<len;t++)
			{
				USART1->DR=USART_RX_BUF[t];
				while((USART1->SR&0X40)==0);//等待发送结束
			}
			printf("\r\n\r\n");//插入换行
			USART_RX_STA=0;
		}else
		{
			times++;
			if(times%5000==0)
			{
				printf("\r\nALIENTEK 探索者STM32F407开发板 串口实验\r\n");
				printf("正点原子@ALIENTEK\r\n\r\n\r\n");
			}
			if(times%200==0)printf("请输入数据,以回车键结束\r\n");  
			if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
			delay_ms(10);   
		}

if(USART_RX_STA&0x8000)是判断USART_RX_STA的最高一位是否为1,如果为1则表示接收到了一段完整的数据。低14位存的是接收到数据的长度,15位没有用到。具体定义在USART1_IRQHandler函数中,这里定义的是接收到回车符号就判断接收完成,根据需求自行修改函数功能。

4、外部中断实验

主循环只打印ok,不做赘述。

主要是外部中断,先看初始化。

void EXTIX_Init(void)
{
	KEY_Init(); 
	Ex_NVIC_Config(GPIO_E,4,RTIR); 		//下降沿触发
 	Ex_NVIC_Config(GPIO_A,0,RTIR); 	 	//上升沿触发    
	MY_NVIC_Init(1,2,EXTI4_IRQn,2);		//抢占1,子优先级2,组2	   
	MY_NVIC_Init(0,2,EXTI0_IRQn,2);		//抢占0,子优先级2,组2	   
}

这边上升沿触发和下降沿触发正点原子应该写错了,这边有点搞笑。为什么有点搞笑,看一下函数内部写了什么。

//外部中断配置函数
//只针对GPIOA~I;不包括PVD,RTC,USB_OTG,USB_HS,以太网唤醒等
//参数:
//GPIOx:0~8,代表GPIOA~I
//BITx:需要使能的位;
//TRIM:触发模式,1,下升沿;2,上降沿;3,任意电平触发
//该函数一次只能配置1个IO口,多个IO口,需多次调用
//该函数会自动开启对应中断,以及屏蔽线   	    
void Ex_NVIC_Config(u8 GPIOx,u8 BITx,u8 TRIM) 
{ 
	u8 EXTOFFSET=(BITx%4)*4;  
	RCC->APB2ENR|=1<<14;  						//使能SYSCFG时钟  
	SYSCFG->EXTICR[BITx/4]&=~(0x000F<<EXTOFFSET);//清除原来设置!!!
	SYSCFG->EXTICR[BITx/4]|=GPIOx<<EXTOFFSET;	//EXTI.BITx映射到GPIOx.BITx 
	//自动设置
	EXTI->IMR|=1<<BITx;					//开启line BITx上的中断(如果要禁止中断,则反操作即可)
	if(TRIM&0x01)EXTI->FTSR|=1<<BITx;	//line BITx上事件下降沿触发
	if(TRIM&0x02)EXTI->RTSR|=1<<BITx;	//line BITx上事件上升降沿触发

上面注释写1是下升沿,2是上降沿。。。。。回到代码注释,1是下降沿2是上升沿

#define RTIR   				2  		//上升沿触发

所以PE4和PA0都是上升沿触发,PA0的抢占优先级是0,同时按下时执行PA0的中断。 

中断内容就是进中断、消抖(按键按下会有多个上升沿,消除这个干扰),LED反转,不作赘述。

这里PA、PB、PC、PD、PE、PF引脚都可以配置为外部中断,但是注意PA0、PB0、……共享同一个外部中断exti0 

5、独立看门狗

独立看门狗(Independent Watchdog Timer, IWDG)是一种嵌入式系统中的硬件定时器,用于提高系统的可靠性。其主要作用是监控系统运行状态,在系统发生故障(如程序跑飞、陷入死循环等)时能够自动复位系统,确保系统能够自动恢复正常工作。
暂时没有计划使用该功能,先不写。

6、窗口看门狗

窗口看门狗(Window Watchdog Timer, WWDG)是一种特殊类型的看门狗定时器,用于监控系统的运行状态,确保系统在正常运行时能够及时重置看门狗计数器,并在系统发生故障(如程序跑飞、死循环等)时进行复位。与独立看门狗相比,窗口看门狗增加了一个“窗口”机制,用于限定喂狗操作的时间窗口,以提高系统的可靠性和安全性。

暂时没有计划使用该功能,先不写。

7、定时器中断

主循环什么都没写,直接看定时器中断的内容,先看初始化。

TIM3_Int_Init(5000-1,8400-1);//10Khz的计数频率,计数5K次为500ms   
void TIM3_Int_Init(u16 arr,u16 psc)
{
	RCC->APB1ENR|=1<<1;	//TIM3时钟使能    
 	TIM3->ARR=arr;  	//设定计数器自动重装值 
	TIM3->PSC=psc;  	//预分频器	  
	TIM3->DIER|=1<<0;   //允许更新中断	  
	TIM3->CR1|=0x01;    //使能定时器3
  	MY_NVIC_Init(1,3,TIM3_IRQn,2);	//抢占1,子优先级3,组2									 
}

计数上限值4999,也就是计5000个数。APB1是四分频,也就是168/4=42MHz,TIME3的时钟是APB1的2倍,84MHz分频8400是10kHz。为什么是两倍在芯片手册108页标注了:STM32F405xx/07xx 和 STM32F415xx/17xx 的定时器时钟频率由硬件自动设置。分为两种 情况:  如果 APB 预分频器为 1,定时器时钟频率等于 APB 域的频率。 否则,等于 APB 域的频率的两倍 (×2)。

进中断还是LED翻转,不做赘述。

8-15没用到先不做

16、ADC

主循环就是获取AD采集平均值在LCD显示,LCD没用到先不作解释。主要还是看AD初始化和采集。

void  Adc_Init(void)
{    
	//先初始化IO口
 	RCC->APB2ENR|=1<<8;    	//使能ADC1时钟 
	RCC->AHB1ENR|=1<<0;    	//使能PORTA时钟	  
	GPIO_Set(GPIOA,PIN5,GPIO_MODE_AIN,0,0,GPIO_PUPD_PU);	//PA5,模拟输入,下拉   

	RCC->APB2RSTR|=1<<8;   	//ADCs复位
	RCC->APB2RSTR&=~(1<<8);	//复位结束	 
	ADC->CCR=3<<16;			//ADCCLK=PCLK2/4=84/4=21Mhz,ADC时钟最好不要超过36Mhz
 	
	ADC1->CR1=0;   			//CR1设置清零
	ADC1->CR2=0;   			//CR2设置清零
	ADC1->CR1|=0<<24;      	//12位模式
	ADC1->CR1|=0<<8;    	//非扫描模式	
	
	ADC1->CR2&=~(1<<1);    	//单次转换模式
 	ADC1->CR2&=~(1<<11);   	//右对齐	
	ADC1->CR2|=0<<28;    	//软件触发
	
	ADC1->SQR1&=~(0XF<<20);
	ADC1->SQR1|=0<<20;     	//1个转换在规则序列中 也就是只转换规则序列1 			   
	//设置通道5的采样时间
	ADC1->SMPR2&=~(7<<(3*5));//通道5采样时间清空	  
 	ADC1->SMPR2|=7<<(3*5); 	//通道5  480个周期,提高采样时间可以提高精确度	 
 	ADC1->CR2|=1<<0;	   	//开启AD转换器	  
}		

 ADC1使能在手册144页,APB2的第8位;A时钟在150页;PU实际是上拉,这边注释是错的。这样的话就是直接采3.3V,根据需求自行配置,一般都是不设上下拉。分频在手册284页,这里因该是8分频10.5MHz。其他寄存器不做赘述自行查看数据手册。

在非扫描模式下,ADC 只会从一个通道进行采样和转换。这个模式下,ADC 每次转换的通道是固定的,转换结束后不会自动切换到其他通道。

单次转换模式:当 ADC 配置为单次转换模式时,ADC 在每次转换后会停止,不会自动开始下一次转换。每次进行 ADC 转换时,必须通过软件手动触发下一次转换。

右对齐模式:ADC 的转换结果将被右对齐,也就是说低位的数据会放在数据的低位(低地址),高位的数据会放在数据的高位(高地址)。

软件触发:ADC 的转换将由软件控制触发,而不是由外部硬件信号(如定时器溢出、中断等)触发。

然后是ADC转换配置,最后采样21MHz采样480个采样周期是0.02ms左右

u16 Get_Adc(u8 ch)
{
	//设置转换序列	  		 
	ADC1->SQR3&=0XFFFFFFE0;//规则序列1 通道ch
	ADC1->SQR3|=ch;		  			    
	ADC1->CR2|=1<<30;       //启动规则转换通道 
	while(!(ADC1->SR&1<<1));//等待转换结束	 	   
	return ADC1->DR;		//返回adc值	
}

 清除规则序列1的通道改为通道ch,下一个函数采20次计算一下平均值。

u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);
		delay_ms(5);
	}
	return temp_val/times;
}  

17、内部温度传感器

和上面AD采样程序差不多,主要区别就是温度传感器是ADC1的CH16

short Get_Temprate(void)
{
	u32 adcx;
	short result;
 	double temperate;
	adcx=Get_Adc_Average(ADC_CH_TEMP,20);	//读取通道16,20次取平均
	temperate=(float)adcx*(3.3/4096);		//电压值
	temperate=(temperate-0.76)/0.0025+25; 	//转换为温度值 
	result=temperate*=100;					//扩大100倍.
	return result;
}

采集转换取平均之后还有一些电压转换为温度的比例问题,按照示例改一下就行。

18-20没用到先不做

21、SPI实验

这边写的是跟外扩FLASH进行SPI通信,我没有外扩FLASH,就简单看一下SPI的部分

22-25没用到先不做

26、内存管理实验

USMART和外部SRAM没用到,就只看一下如何管理内部内存。先看初始化

void mymemset(void *s,u8 c,u32 count)  
{  
    u8 *xs = s;  
    while(count--)*xs++=c;  
}	   
  • void *s:指向内存区域的指针,可以是任何类型的指针。
  • u8 c:要设置的值,类型是 u8(一个字节的值,通常是一个 8 位数值)。
  • u32 count:要设置的字节数,表示多少个字节的数据需要被填充。
//内存管理初始化  
//memx:所属内存块
void my_mem_init(u8 memx)  
{  
    mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);//内存状态表数据清零  
	mymemset(mallco_dev.membase[memx], 0,memsize[memx]);	//内存池所有数据清零  
	mallco_dev.memrdy[memx]=1;								//内存管理初始化OK  
}
  • mallco_dev.memmap[memx]:指向内存块 memx 对应的内存状态表。
  • memtblsize[memx] * 2:内存状态表的大小,乘以 2 可能是因为每个状态项占 2 个字节(通常是 16 位)。
  • mallco_dev.membase[memx]:指向内存池基地址(内存池的起始地址)。
  • memsize[memx]:内存池的大小。

然后看主循环

while(1)
	{	
		key=KEY_Scan(0);//不支持连按	
		switch(key)
		{
			case 0://没有按键按下	
				break;
			case KEY0_PRES:	//KEY0按下
				p=mymalloc(sramx,2048);//申请2K字节
				if(p!=NULL)sprintf((char*)p,"Memory Malloc Test%03d",i);//向p写入一些内容
				break;
			case KEY1_PRES:	//KEY1按下	   
				if(p!=NULL)
				{
					sprintf((char*)p,"Memory Malloc Test%03d",i);//更新显示内容 	 
					LCD_ShowString(30,270,200,16,16,p);			 //显示P的内容
				}
				break;
			case WKUP_PRES:	//	  
				myfree(sramx,p);//释放内存
				p=0;			//指向空地址
				break;
			case KEY2_PRES:	// 
				sramx++; 
				if(sramx>2)sramx=0;
				if(sramx==0)LCD_ShowString(30,170,200,16,16,"SRAMIN ");
				else if(sramx==1)LCD_ShowString(30,170,200,16,16,"SRAMEX ");
				else LCD_ShowString(30,170,200,16,16,"SRAMCCM");
				break;
		}
		if(tp!=p)
		{
			tp=p;
			sprintf((char*)paddr,"P Addr:0X%08X",(u32)tp);
			LCD_ShowString(30,250,200,16,16,paddr);	//显示p的地址
			if(p)LCD_ShowString(30,270,200,16,16,p);//显示P的内容
		    else LCD_Fill(30,270,239,266,WHITE);	//p=0,清除显示
		}
		delay_ms(10);   
		i++;
		if((i%20)==0)//DS0闪烁.
		{
			LCD_ShowNum(30+104,190,my_mem_perused(SRAMIN),3,16);//显示内部内存使用率
			LCD_ShowNum(30+104,210,my_mem_perused(SRAMEX),3,16);//显示外部内存使用率
			LCD_ShowNum(30+104,230,my_mem_perused(SRAMCCM),3,16);//显示CCM内存使用率
 			LED0=!LED0;
 		}
	}	   
  • 通过 KEY_Scan(0) 函数扫描按键,返回按键值。0 表示不支持连按模式,即只有按下某个按键时才会有响应。
  • key 变量用来保存当前按下的按键的状态。
  • case 0::没有按键按下。此时不做任何操作,继续循环。
  • case KEY0_PRES::如果按下 KEY0(假设是内存分配按键),调用 mymalloc(sramx, 2048) 函数来申请 2KB 内存,并用 sprintf 向已分配的内存 p 中写入字符串 "Memory Malloc Testxxx"xxx 为当前的 i 值)。
  • void *mymalloc(u8 memx, u32 size)
    {
        u32 offset;   
        offset = my_mem_malloc(memx, size);  	   	 	   
        
        if (offset == 0XFFFFFFFF) return NULL;  
        else return (void *)((u32)mallco_dev.membase[memx] + offset);  
    }
    //偏移量(offset):返回值是从内存池的起始位置开始计算的内存地址的偏移量。它通常是一个整数,表示从内存池基址到分配内存位置的偏移量。0xFFFFFFFF 是一个常见的错误标志,表示没有足够的内存来满足分配请求
    mallco_dev.membase[memx]:这是一个指向内存池基地址的指针数组。memx 用来选择当前分配的内存池。
    (u32)mallco_dev.membase[memx]:将内存池基地址转换为 u32 类型(即地址)。
    offset:将返回的偏移量加到基地址上,得到实际的内存地址。
    (void *):将计算出的内存地址强制转换为 void * 类型,表示它是一个指向内存的通用指针。
  • case KEY1_PRES::如果按下 KEY1(假设是更新显示内容的按键),检查 p 是否为空,如果 p 不为空,则更新 p 中的数据并显示在 LCD 上。
  • case WKUP_PRES::如果按下 WKUP(假设是释放内存的按键),调用 myfree(sramx, p) 释放之前申请的内存,并将 p 设置为 0(指向空地址)。
  • case KEY2_PRES::如果按下 KEY2(假设是切换内存区域的按键),切换 sramx 的值(0、1、2),并根据当前值显示相应的内存区域标识。

if(tp != p)

  • 这部分代码用于检测 p 的值是否发生变化。如果 p 有变化(即分配了新内存或释放了内存),则更新 tp(临时保存 p 的值),并显示 p 的地址和内容。

if((i%20) == 0)

  • 每隔一段时间,显示内存使用情况。通过 my_mem_perused(SRAMIN) 等函数获取内存使用率,并显示在 LCD 上。my_mem_perused 应该是一个返回指定内存区域使用情况的函数。
  • u8 my_mem_perused(u8 memx)
    {
        u32 used = 0;  // 用于统计已使用的内存块数量
        u32 i;         // 用于遍历内存状态表的索引
    
        // 遍历内存池的状态表
        for(i = 0; i < memtblsize[memx]; i++)
        {
            // 如果内存状态表中的某个位置的值为 1,表示该内存块已被使用
            if(mallco_dev.memmap[memx][i]) used++; 
        }
    
        // 返回已使用内存块数占总内存块数的百分比
        return (used * 100) / (memtblsize[memx]);  
    }
    //遍历求占比

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值