完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
单片机作为一种微控制器,最基本的用途便是通过其引脚与外界进行交互,而在单片机编程界,有这么一个程序,堪称单片机中的HelloWorld,不仅可以熟悉单片机的引脚控制,更能对单片机的时钟进行深入了解,那就是几乎所有单片机教程中都会提到的——流水灯。
在上一篇中我们已经搭建好了STM32开发环境,点亮了第一个LED灯,这一篇将从电路原理分析开始,对流水灯的控制原理,电路参数设计,STM32F103引脚与时钟的设置进行介绍。 流水灯电路设计 顾名思义,流水灯就是像水流一样,依次点亮的一组灯。设计流水灯为间隔固定的时间每次点亮一个LED,因此在点亮下一个LED的同时,要关闭上一个LED,并且进行计时,以循环下去。 在上一篇中我们用单片机的GPIOB8引脚点亮了最小系统板上自带的一个LED,根据其电路原理图,可以设计流水灯的电路原理。 电路原理图 根据最小系统板的引脚分布,选择GPIOA2~GPIOA7、GPIOB8、GPIOB9共8个引脚来控制流水灯(这8个引脚在笔者的系统板同一侧且相邻,用杜邦线连接整齐点o( ̄︶ ̄)o),电路原理图和实物图如下: 电路参数设计分析 这里实物是自己焊的小板子,用的1K Ω Omega Ω的排阻作为限流电阻,选的白发绿LED: [tr][/tr]
VIN表示引脚输入电平的范围,有5V电平耐受能力的引脚可以达5.5V,这里PA2~PA7(PA-GPIOA)不具有5V电平耐受能力(也在手册中能找到,引脚定义表里标记FT即能耐受5V电平)。 IIO表示引脚输入输出电流的范围,均为25mA 时钟及GPIO分析 控制流水灯正常运行,需要对8个LED的控制引脚进行设置,并按固定时间对其输出进行控制。由于时钟是单片机运行的基础,首先对时钟进行分析。 STM32F103时钟分析 首先在STM32手册中找到时钟树,如下图,沿图中红色直线从左至右看: OSC_OUT和OSC_IN就是STM32的外部时钟输入,范围是4-16MHz,这里最小系统板用的是8MHz的晶振,下面的OSC32_OUT和OSC32_IN适用于32KHz的时钟输入,暂时不用考虑; 然后遇到分频器PLLXTPRE(HSE divider for PLL entry),可以对输入的时钟进行2分频(即频率除以2)或不分频; 然后进入PLLSRC(PLL entry clock source),这个是选择时钟来源 为HSI(内部高速时钟High Speed Internal clock)或者HSE(High Speed External clock); 然后进入倍频器PLLMUL(PLL multiplication factor),对时钟频率进行倍频(乘上一个因数); 然后进入SW(System clock switch),可以选择系统时钟来源; 然后进入预分频器AHB Prescaler(Advanced High performance Bus,即高级高性能总线时钟的预分频器);从AHB预分频器出来的时钟信号再分给其他外设; 比如进入APB1 Prescaler(Advanced Peripheral Bus,即高级外设总线时钟的预分频器),可以提供给挂载在APB1总线上的外设;进入APB2 Prescaler可以提供给挂载在APB2总线上的外设。 上述提到的PLLXTPRE、PLLSRC、PLLMUL、SW、AHB Prescaler、APB1 Prescaler、APB2 Prescaler等相关器件都有相应的寄存器进行控制,感兴趣的朋友可以在STM32手册中找到相关寄存器的说明,这里就略过啦啦啦(~ ̄▽ ̄)~。 GPIO分析 GPIO即General-purpose IOs,通用输入输出引脚。STM32F103C8T6共有48个引脚,其中一部分是电源、时钟输入、启动方式、复位等特殊功能的引脚,剩下的引脚又可分为通用输入输出引脚GPIOs和复用功能输入输出引脚AFIOs,这些引脚都可以: 通过Port configuration register(端口配置寄存器)进行功能配置; 通过Port input data register(端口输入数据寄存器),读取各个引脚输入数据(高低电平); 通过Port output data register(端口输出数据寄存器),向外部输出数据(高低电平); 通过Port bit set/reset register(端口位设置/清除寄存器),对端口的某个数据位(相当于某一引脚)进行清0或置1; 通过Port bit reset register(端口位清除寄存器),对端口的某个数据位进行清0。 当然,上面所有的寄存器都可以通过调用库函数来进行设置,实现GPIO的控制功能。 再看到下面STM32手册中的系统结构图,在右下部分可以看到所有GPIO都挂载在APB2总线上,因此在使用GPIO时,需要先初始化APB2总线也就是初始化APB2总线的时钟,当然,这也是调用库函数就可以实现的: 程序设计 新建一个LED流水灯文件夹,并复制一份工程模板到文件夹下,打开工程文件,进入Keil uVision5。 时钟设置分析 首先对启动文件startup_stm32f10x_hd.s中关于系统时钟设置的相关内容进行分析。打开startup_stm32f10x_hd.s文件: 第1-33行:文件说明注释; 第35-145行:堆栈以及中断向量地址的设置; 从147行为复位中断向量标号,即可以认为STM32在上电或复位后跳转到标号位置运行: ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP 148行:输出Reset_Handler标号,这样其它文件也可以使用这个标号来进行跳转; 149行:导入__main标号,可以认为是导入main函数所在地址的标号; 150行:导入SystemInit标号; 151-152行:设置寄存器R0的值为SystemInit标号代表的地址,然后跳转到这个地址运行,即调用SystemInit函数; 153行开始运行main函数。 对startup_stm32f10x_hd.s文件的分析可以参考:专家揭秘:STM32启动过程全解 通过分析,在上电或者复位后,首先调用SystemInit函数,然后再进入main函数继续运行,因此先分析SystemInit函数,在SystemInit标号上右键->Go To Definition of ‘SystemInit’,进入SystemInit函数(位于system_stm32f10x.c文件中)。 在SystemInit函数中,有详细的注释,简略说明: 第214-258行:将所有与时钟相关的寄存器复位为默认值; 第262行调用SetSysClock()函数,对系统时钟进行设置,同样右键->Go To Definition of ‘SetSysClock’,跳转到SetSYSClock函数。 /** * @brief Configures the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers. * @param None * @retval None */ 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) */ } 函数内容可以知道,这个函数根据宏定义,再调用具体的函数对系统时钟进行设置;而在system_stm32f10x.c文件的115行,有SYSCLK_FREQ_72MHz定义(在106行的if判断里,STM32F10X_LD_VL、STM32F10X_MD_VL、STM32F10X_HD_VL均未定义,故进入else分支): #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 综合上述分析,在系统上电或复位后,进入复位中断向量地址,调用SystemInit()函数->调用SetSysClock()函数->调用SetSysClockTo72()函数。 在SetSysClockTo72()函数中将系统时钟设置为72MHz,即SYSCLK时钟频率为72MHz,并设置AHB、APB1、APB2分频分别为1、2、1,即: 系统时钟SYSCLK频率为72MHz; AHB总线时钟HCLK频率为72MHz; APB1总线时钟PCLK1频率为36MHz; APB2总线时钟PCLK2频率为72MHZ。 (APB1分频系数设为2是因为PCLK1时钟频率不能大于36MHz) 程序 首先理清程序思路,开发库的启动文件已经完成了系统时钟的设置,但外设时钟还未开启,因此程序流程如下:
开启APB2总线上的外设时钟,需要调用RCC_APB2PeriphClockCmd()函数,具体说明可以在库函数手册中查找到: 开启GPIOA、GPIOB的时钟: RCC_APB2PeriphClockCmd(GPIOA, ENABLE); RCC_APB2PeriphClockCmd(GPIOB, ENABLE); 配置GPIO 对GPIO引脚进行设置,库提供了GPIO初始化结构体GPIO_InitTypeDef: 共有三个参数:
并调用GPIO_SetBits()函数将引脚置1,输出高电平,使LED在开始时处于关闭状态;而调用GPIO_ResetBits()函数可以将引脚清0,输出低电平,点亮LED。 具体函数用法请自行查询库函数手册。 配置GPIOA、GPIOB : GPIO_InitTypeDef GPIOInitStruct; GPIOInitStruct.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIOInitStruct); GPIO_SetBits(GPIOA, GPIOInitStruct.GPIO_Pin); GPIOInitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIOInitStruct); GPIO_SetBits(GPIOB, GPIOInitStruct.GPIO_Pin); 延时函数 在这里使用粗略的延时函数,所谓延时,即什么也不做,让处理器进行等待,对应的操作称为nop,表示等待一个机器周期: 因此,若执行72个__nop()函数,则可以延时1μ s mu sμs,但考虑到函数调用与返回需要两个机器周期,因此设计执行70个__nop()函数,延时函数如下: static void delay_1us() { __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); } (不用数了,就是70个__nop(),其实以后可以利用SysTick寄存器进行精确延时计算) 粗略延时函数: /** * @brief 粗略延时t us * @param t * @retval None */ void delay_u(int t) { while(t--) delay_1us(); } /** * @brief 粗略延时t ms * @param t * @retval None */ void delay_m(int t) { while(t--) delay_u(1000); } 流水灯控制 部分程序如下,思路就是关闭前一个LED然后点亮下一个LED,并延时一段时间: ... GPIO_SetBits(GPIOB, GPIO_Pin_9); GPIO_ResetBits(GPIOA, GPIO_Pin_2); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_2); GPIO_ResetBits(GPIOA, GPIO_Pin_3); delay_m(200); ... 完整程序 /* Includes ------------------------------------------------------------------*/ #include "stm32f10x.h" /* Private functions Declaration ---------------------------------------------*/ void GPIOConfig(void); void delay_m(int t); void delay_u(int t); /** * @brief Main program. * @param None * @retval None */ int main(void) { GPIOConfig(); while(1) { GPIO_SetBits(GPIOB, GPIO_Pin_9); GPIO_ResetBits(GPIOA, GPIO_Pin_2); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_2); GPIO_ResetBits(GPIOA, GPIO_Pin_3); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_3); GPIO_ResetBits(GPIOA, GPIO_Pin_4); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_5); GPIO_ResetBits(GPIOA, GPIO_Pin_6); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_6); GPIO_ResetBits(GPIOA, GPIO_Pin_7); delay_m(200); GPIO_SetBits(GPIOA, GPIO_Pin_7); GPIO_ResetBits(GPIOB, GPIO_Pin_8); delay_m(200); GPIO_SetBits(GPIOB, GPIO_Pin_8); GPIO_ResetBits(GPIOB, GPIO_Pin_9); delay_m(200); } } /** * @brief 初始化GPIO * @param None * @retval None */ void GPIOConfig(void) { // GPIO初始化结构体 GPIO_InitTypeDef GPIOInitStruct; // 开启GPIOA、GPIOB外设时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 设置GPIOA初始化结构体 GPIOInitStruct.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 初始化GPIOA并将引脚置1 GPIO_Init(GPIOA, &GPIOInitStruct); GPIO_SetBits(GPIOA, GPIOInitStruct.GPIO_Pin); // 设置GPIOB初始化结构体 GPIOInitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz; // 初始化GPIOB并将引脚置1 GPIO_Init(GPIOB, &GPIOInitStruct); GPIO_SetBits(GPIOB, GPIOInitStruct.GPIO_Pin); } /** * @brief 延时1us * @param None * @retval None */ static void delay_1us() { __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); __nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop(); } /** * @brief 粗略延时t us * @param t * @retval None */ void delay_u(int t) { while(t--) delay_1us(); } /** * @brief 粗略延时t ms * @param t * @retval None */ void delay_m(int t) { while(t--) delay_u(1000); } 运行结果 同样通过mcuisp软件利用串口下载编译好的.hex程序文件,可以看到8个LED灯依次循环点亮: 完结撒花✿✿ヽ(°▽°)ノ✿ |
|
|
|
只有小组成员才能发言,加入小组>>
2518 浏览 0 评论
1098浏览 2评论
710浏览 1评论
460浏览 0评论
201浏览 0评论
346浏览 0评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-27 11:12 , Processed in 1.198127 second(s), Total 48, Slave 40 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号