代码均来自正点原子官网,函数结构清晰和注释写的非常清楚,大家可以自行下载。
正点原子资料下载中心 — 正点原子资料下载中心 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]); } //遍历求占比