摘要:本作品使用STM32F103作为主控。是大二上《嵌入式应用开发》课程的期末大作业。复刻B站的智能手表。有时间会加上RTOS和更多外设。
目录
《嵌入式应用开发》大作业
题目:基于STM32F103RCT6的
智能手表的设计与实现
1.项目应用背景及需求分析
随着科技的不断发展,人们对智能穿戴设备的需求逐渐增加。智能手表作为一种便携、实用的智能穿戴设备,逐渐受到了广大用户的青睐。它不仅具备传统手表的时间显示功能,还融合了智能化的特性,可以连接到手机或其他智能设备,提供丰富的应用和服务。智能手表的市场需求在近年来不断增长,预计到2025年,全球智能手表的销量将达到1.2亿部,这无疑表明了智能手表的市场需求在近年来不断增长。
智能手表的核心部件之一是微控制器,它负责控制各种外围设备,如显示屏、传感器、蓝牙、电池等,以及执行应用程序。微控制器的性能、功耗、成本等因素直接影响智能手表的用户体验和市场竞争力。因此,微控制器的选择和优化是智能手表设计和制造过程中的关键步骤。
总的来说,智能手表的发展和普及不仅需要科技的进步和市场的推动,还需要微控制器等关键部件的持续优化和创新。只有这样,智能手表才能更好地满足用户的需求,提供更优质的服务,从而在激烈的市场竞争中脱颖而出。
在这样的背景下,本项目旨在设计与实现一款基于STM32F103RCT6的智能手表,该微控制器是一款32位的单片机,由意法半导体公司生产,具有高性能、低功耗、丰富的外设等特点。以满足用户对于智能穿戴设备的需求,提供更加便捷、智能的使用体验。通过充分利用STM32F103RCT6的性能和丰富的外设资源,实现多种功能,包括但不限于时间显示、温度监测、小游戏娱乐等。使智能手表具有一定的娱乐性和实用性。
2.各模块功能说明
2.1GPIO输入输出模块功能说明
GPIO是通用输入输出端口的简称,可以通过软件来控制其输入和输出。单片机芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、 控制以及数据采集的功能。
GPIO分为三种模式:输入模式、输出模式和复用模式。输入模式用来读取外部信号,输出模式用来驱动外部设备,复用模式用来连接内部外设,如定时器、串口等。
1. 四种输入模式
GPIO_Mode_IN_FLOATING 浮空输入模式
GPIO_Mode_IPU 上拉输入模式
GPIO_Mode_IPD 下拉输入模式
GPIO_Mode_AIN 模拟输入模式
2. 四种输出模式
GPIO_Mode_Out_OD 开漏输出模式
GPIO_Mode_Out_PP 推挽输出模式
GPIO_Mode_AF_OD 复用开漏输出模式
GPIO_Mode_AF_PP 复用推挽输出模式
STM32F103有7个GPIO端口,分别为GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF和GPIOG。每个端口都有一个对应的时钟使能位,需要在RCC(复位和时钟控制)模块中打开才能使用。
2.2EXTI—外部中断/事件控制器模块配置说明
EXTI(External interrupt/event controller)—外部中断/事件控制器,管理了控制器的20个中断/事件线。 每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。EXTI可以实现对每个中断/事件线进行单独配置, 可以单独配置为中断或者事件,以及触发事件的属性。
图1-1 NVIC框图
EXTI有20个中断/事件线,每个GPIO都可以被设置为输入线,占用EXTI0至EXTI15, 还有另外七根用于特定的外设事件。
图1-2 EXTI中断_事件线
EXTI0至EXTI15用于GPIO,通过编程控制可以实现任意一个GPIO作为EXTI的输入源。由 图1-2 EXTI中断_事件线 可知, EXTI0可以通过AFIO的外部中断配置寄存器1(AFIO_EXTICR1)的EXTI0[3:0]位选择配置为PA0、 PB0、PC0、PD0、PE0、PF0、PG0、PH0或者PI0,见 图1-3 EXTI0输入源选择。其他EXTI线(EXTI中断/事件线)使用配置都是类似的。
图1-3 EXTI0输入源选择
EXTI 的配置步骤一般包括以下几个方面:
1.使能 IO 口时钟,配置 IO 口模式为输入。
2.开启 SYSCFG 时钟,设置 IO 口与中断线的映射关系。
3.配置 NVIC,使能中断。
4.初始化 EXTI,选择触发方式。
5.编写 EXTI 中断服务函数。
2.3SysTick精确延时模块配置说明
SysTick(System Timer)是 Cortex-M3 内核的一个定时器,它是一个 24 位的递减定时器,当计数到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。
假设STM32F103工作在72MHz,即72000000Hz,意味着1s时间内,会计数72000000次。那么1ms则计数72000000/1000=72000次。这个72000就可以作为系统滴答定时器的初始值,将这个值写入系统滴答定时器,定时器在每个时钟周期减1,减到0时,就刚好是1ms,同时产生中断通知,再次加载72000如此反复。
SysTick 可以用来实现精确的延时功能,不占用系统定时器,而且代码的移植性很好,适用于任何 Cortex-M 内核的 MCU。
SysTick 的配置步骤一般包括以下几个方面:
1.更新系统时钟,获取 SystemCoreClock 的值。
2.调用 SysTick_Config 函数,设置 SysTick 的计数值,使能中断,使能定时器。
定义全局变量,用来记录延时时间。
3.编写 SysTick 中断服务函数,让全局变量递减。
4.封装延时函数,根据全局变量的值判断延时是否结束。
2.4TIM定时器模块说明
TIM(Timer)是 STM32 的一个外设,它可以实现定时、计数、输入捕获、输出比较、PWM 生成、编码器接口等功能。
STM32 的定时器分为三类:基本定时器(Basic Timer)、通用定时器(General-purpose Timer)和高级定时器(Advanced Timer)。
图1-4 TIM定时器分类
基本定时器只能实现定时功能,通用定时器除了定时功能外,还可以实现输入捕获、输出比较、PWM 生成等功能,高级定时器包含了通用定时器的所有功能,还可以实现编码器接口、死区控制、互补输出等功能。
STM32 的定时器的时钟源可以来自内部时钟或外部时钟,内部时钟的频率由 APBx 总线的分频系数决定,外部时钟的频率由外部信号的频率决定。
STM32 的定时器的工作原理是:定时器的时钟源经过预分频器(Prescaler)后,得到计数器(Counter)的时钟,计数器从 0 开始递增或递减,当计数器的值等于自动重载寄存器(Auto-reload Register)的值时,产生一个更新事件(Update Event),并将自动重载寄存器的值重新装载到计数器中,开始新一轮的计数。
图1-5 TIM定时器框图
STM32 的定时器的配置步骤一般包括以下几个方面:
1.使能 TIMx 时钟,配置 TIMx 的时钟源。
2.初始化 TIMx,设置预分频器、自动重载寄存器、计数模式等参数。
3.配置 NVIC,使能中断,设置中断优先级。
4.使能 TIMx,开始计数。
5.编写 TIMx 中断服务函数,处理更新事件或其他事件。
STM32 的定时器的初始化结构体 TIM_TimeBaseInitTypeDef 包含以下成员:
1.TIM_Prescaler:预分频器的值,可设置范围为 0 至 65535,实现 1 至 65536 分频。
2.TIM_Period:自动重载寄存器的值,可设置范围为 0 至 65535,决定了定时器的周期。
3.TIM_CounterMode:计数模式,可选为向上计数、向下计数或中心对齐模式。
4.TIM_ClockDivision:时钟分频,可选为不分频、二分频或四分频,影响数字滤波器的采样时钟。
5.TIM_RepetitionCounter:重复计数器的值,只对高级定时器有效,可设置范围为 0 至 255,决定了 PWM 的重复次数。
STM32 的定时器的初始化函数为 void TIM_TimeBaseInit (TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct),它可以根据传入的结构体参数设置 TIMx 相关的寄存器。
STM32 的定时器的中断服务函数有以下几种:
1.TIMx_IRQHandler:用于 TIMx 的更新中断。
2.TIMx_CC_IRQHandler:用于 TIMx 的捕获/比较中断。
3.TIMx_BRK_TIMx_IRQHandler:用于 TIMx 的刹车中断。
4.TIMx_UP_TIMx_IRQHandler:用于 TIMx 的更新中断。
5.TIMx_TRG_COM_TIMx_IRQHandler:用于 TIMx 的触发/通信中断。
2.5DMA直接存储器访问模块配置说明
DMA(Direct Memory Access)是一种可以让外设或存储器之间直接传输数据而不经过 CPU 的技术,它可以提高系统的效率和性能。
DMA(直接存储器存取)是用来给外设与存储器以及存储器与存储器提供高速的数据传输。外设到存储器、存储器到外设、外设到外设都是可以传输的。FLSH,SRAM,挂在APB1、APB2与AHB上的外设都可以作为DMA传输的源点和目标点。数据可以通过DMA快速地移动而不需经过CPU。这使得CPU资源可以用于其他操作。两个DMA控制器一共有12个通道(DAM1有7个,DAM2有5个),每一个都能专注地管理一个或多个外设的存储器访问请求。有一个仲裁机制处理DMA请求的优先级问题。
DMA 的工作原理是:当 CPU 初始化 DMA 传输后,DMA 控制器会根据配置的参数和外设的请求信号,自动从源地址读取数据并写入目的地址,同时更新计数器和地址指针,直到传输完成或停止。
图1-6 DMA存储器框图
DMA 的配置步骤一般包括以下几个方面 :
1.使能 DMA 控制器时钟,选择 DMA 数据流和通道。
2.初始化 DMA 数据流,设置传输方向、传输模式、传输数据量、传输数据宽度、地址增量模式、地址指针、优先级等参数。
3.配置 NVIC,使能 DMA 中断,设置中断优先级。
4.使能 DMA 数据流,开始传输。
5.编写 DMA 中断服务函数,处理传输完成或传输错误等事件。
DMA 的初始化结构体 DMA_InitTypeDef 包含以下成员 :
DMA_DIR:DMA 传输方向,可选为存储器到外设 DMA_DIR_MemoryToPeripheral、外设到存储器 DMA_DIR_PeripheralToMemory 或存储器到存储器 DMA_DIR_MemoryToMemory。
DMA_BufferSize:DMA 传输数据量,可设置范围为 0 至 65535。
DMA_PeripheralInc:外设地址增量模式,可选为使能 DMA_PeripheralInc_Enable 或禁止 DMA_PeripheralInc_Disable。
DMA_MemoryInc:存储器地址增量模式,可选为使能 DMA_MemoryInc_Enable 或禁止 DMA_MemoryInc_Disable。
DMA_PeripheralDataSize:外设数据宽度,可选为字节 DMA_PeripheralDataSize_Byte、半字DMA_PeripheralDataSize_HalfWord 或字 DMA_PeripheralDataSize_Word。
DMA_MemoryDataSize:存储器数据宽度,可选为字节 DMA_MemoryDataSize_Byte、半字DMA_MemoryDataSize_HalfWord 或字 DMA_MemoryDataSize_Word。
DMA_Mode:DMA 传输模式,可选为正常 DMA_Mode_Normal 或循环 DMA_Mode_Circular。
DMA_Priority:DMA 优先级,可选为低 DMA_Priority_Low、中 DMA_Priority_Medium、高DMA_Priority_High或非常高 DMA_Priority_VeryHigh。
DMA_M2M:DMA 存储器到存储器模式,可选为使能 DMA_M2M_Enable 或禁止 DMA_M2M_Disable
DMA 的初始化函数为 void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct),它可以根据传入的结构体参数设置 DMAy_Channelx 相关的寄存器。DMA 的中断服务函数为 void DMA1_Channelx_IRQHandler(void)。
2.6ADC模/数转换模块说明
ADC(Analog-to-Digital Converter)是一种将连续变化的模拟信号转换为离散的数字信号的器件,比如将温度感器产生的电信号转为控制芯片能处理的数字信号 0101,这样 ADC 就建立了模拟世界的传感器和数字世界的信号处理与数据转换的联系。12位ADC是一种逐次逼近型模拟数字转换器。它有多达18个通道,可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。
ADC 的工作原理是:首先,对输入的模拟信号进行采样,将时间上连续变化的信号转换为时间上离散的信号,即将时间上连续变化的模拟量转换为一系列等间隔的脉冲,脉冲的幅度取决于输入模拟量;其次,对采样后的信号进行保持,使得每个脉冲的幅度在一定的时间内保持不变,以便进行转换;然后,对保持后的信号进行量化,将每个脉冲的幅度归化到与之接近的离散电平之上,这个过程会产生一定的量化误差;最后,对量化后的信号进行编码,将每个离散电平用二进制数表示,得到最终的数字信号。
图1-7 ADC模/数转换框图
STM32 的ADC 多达18 个通道,其中外部的16 个通道就是框图中的ADCx_IN0、ADCx_IN1...ADCx_IN5。这16 个通道对应着不同的IO 口,具体是哪一个IO 口可以从手册查询到。其中ADC1/2/3 还有内部通道:ADC1的通道16连接到了芯片内部的温度传感器,Vrefint 连接到了通道17。ADC2 的模拟通道16 和17 连接到了内部的VSS。ADC3 的模拟通道9、14、15、16 和17 连接到了内部的VSS。
图1-8 ADC模/数转换I/O口分配
STM32F103 的 ADC 模块的配置步骤一般包括以下几个方面 :
1.使能 ADC 时钟,配置 ADC 的时钟源和分频系数。
2.初始化 ADC,设置 ADC 的工作模式、转换序列、触发源、数据对齐等参数配置 NVIC,使能 ADC 中断,设置中断优先级。
3.初始化 DMA,设置 DMA 的传输方向、传输数据量、传输数据宽度、地址增量模式、地址指针、优先级等参数。
4.配置 ADC 通道,设置通道的采样时间、参考电压、模拟看门狗等参数。
5.使能 ADC,开始转换。
6.编写 ADC 中断服务函数,处理转换完成或模拟看门狗等事件。
STM32F103 的 ADC 模块的初始化结构体 ADC_InitTypeDef 包含以下成员 :
ADC_Mode:ADC 工作模式,可选为独立模式 ADC_Mode_Independent、规则注入同时模式 ADC_Mode_RegInjecSimult、规则同时模式 ADC_Mode_RegSimult、注入同时模式 ADC_Mode_InjecSimult、规则交替模式 ADC_Mode_RegSimult_AlterTrig、快速内插模式 ADC_Mode_FastInterl、慢速内插模式 ADC_Mode_SlowInterl。
ADC_ScanConvMode:ADC 扫描转换模式,可选为使能 ENABLE 或禁止 DISABLE。
ADC_ContinuousConvMode:ADC 连续转换模式,可选为使能 ENABLE 或禁止 DISABLE。
ADC_ExternalTrigConv:ADC 外部触发源,可选为
定时器1的CC1事件 ADC_ExternalTrigConv_T1_CC1、
定时器1的CC2事件 ADC_ExternalTrigConv_T1_CC2、
定时器1的CC3事件 ADC_ExternalTrigConv_T1_CC3、
定时器2的CC2事件 ADC_ExternalTrigConv_T2_CC2、
定时器3的TRGO事件 ADC_ExternalTrigConv_T3_TRGO、
定时器4的CC4事件 ADC_ExternalTrigConv_T4_CC4、外部中断线11 ADC_ExternalTrigConv_Ext_IT11 或软件触发 ADC_ExternalTrigConv_None。
ADC_DataAlign:ADC 数据对齐方式,可选为左对齐 ADC_DataAlign_Left
或右对齐 ADC_DataAlign_Right。
ADC_NbrOfChannel:ADC 转换通道数,可设置范围为 1 至 16。
数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。其中转换结束中断很好理解,跟我们平时接触的中断一样,有相应的中断标志位和中断使能位,我们还可以根据中断类型写相应配套的中断服务程序。
规则和注入通道转换结束后,除了产生中断外,还可以产生DMA 请求,把转换好的数据直接存储在内存里面。要注意的是只有ADC1 和ADC3 可以产生DMA 请求。
2.7OLED模块说明
OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。OLED由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。OLED显示技术具有自发光的特性,采用非常薄的有机材料涂层和玻璃基板,当有电流通过时,这些有机材料就会发光,而且OLED显示屏幕可视角度大,并且能够节省电能。
4 线 SPI 接口方式连接 OLED 模块和微控制器。SPI 是一种串行同步通信协议,它使用 4 根线来传输数据,分别是:
SCLK:时钟线,用于同步数据的发送和接收。
MOSI:主机输出从机输入线,用于主机向从机发送数据。
MISO:主机输入从机输出线,用于从机向主机发送数据。
CS:片选线,用于选择要通信的从机。
OLED本身是没有显存的,它的显存是依赖于SSD1306提供的。SSD1306的显存总共为128 * 64bit大小,SSD1306将这些显存分为了8页。每页包含了128个字节,总共8页,这样刚好是128*64的点阵大小。
图1-9 OLED显存
OLED屏幕原理:
STM32内部建立一个缓存(共128*8个字节),每次修改的时候,只是修改STM32上的缓存(实际上就是SRAM),修改完后一次性把STM32上的缓存数据写入到OLED的GRAM。
2.8RTC模块说明
RTC 模块是一种实时时钟模块,它可以在单片机中提供精确的时间、日期和闹钟等功能。RTC 模块可以在断电或低功耗模式下继续运行,只要有一个备用电源供应。RTC 模块通常使用一个 32.768 kHz 的低速外部晶体或陶瓷谐振器作为时钟源,也可以选择使用 HSE 时钟除以 128 或 LSI 时钟作为时钟源。RTC 模块由两个部分组成:APB1 接口和 RTC 核心。APB1 接口用于和 APB1 总线相连,可以访问 RTC 的相关寄存器。RTC 核心由一组可编程计数器组成,可以产生不同的中断信号,如秒中断、闹钟中断和溢出中断。
图1-10 RTC 模块的原理框图
两个 32 位寄存器包含二进码十进数格式 (BCD) 的秒、分钟、小时( 12 或 24 小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。
RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或从待机模式唤醒后, RTC的设置和时间维持不变。
系统复位后,对后备寄存器和RTC的访问被禁止,这是为了防止对后备区域(BKP)的意外写操作。执行以下操作将使能对后备寄存器和RTC的访问:
● 设置寄存器RCC_APB1ENR的PWREN和BKPEN位,使能电源和后备接口时钟。
● 设置寄存器PWR_CR的DBP位,使能对后备寄存器和RTC的访问。
一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据。
配置RTC寄存器:
1.查询RTOFF位,知道RTOFF的值为1.
2.置CNF值为1,进入配置模式。
3.对一个或者多个RTC寄存器进行写操作。
4.清除CNF标志位,退出配置模式。
5.查询RTOFF,直到RTOFF位变1,已确认写操作已经完成。
仅当CNF标志位被清除时,写操作才能进行,这个操作至少需要3个RTCCLK周期。
3.寄存器参数设置
3.1GPIO输入输出模块配置
void KEY_Init(void) //按键GPIO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键端口的时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
// 选择按键的引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
// 设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
// 使用结构体初始化按键
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
void LED_GPIO_Config(void) //LED GPIO初始化
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd(LED1_GPIO_CLK | LED2_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED2_GPIO_PIN;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure);
/* 关闭所有led灯 */
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
}
// OLED引脚初始化
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_13, (BitAction)(x))
#define OLED_SCLK_Clr() GPIO_ResetBits(GPIOB, GPIO_Pin_12) // 0
#define OLED_SCLK_Set() GPIO_SetBits(GPIOB, GPIO_Pin_12) // SCL 1
#define OLED_SDIN_Clr() GPIO_ResetBits(GPIOB, GPIO_Pin_13) // 0
#define OLED_SDIN_Set() GPIO_SetBits(GPIOB, GPIO_Pin_13)
3.2EXTI—外部中断/事件控制器模块配置
static void NVIC_Configuration(void) //按键NVIC配置
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 配置中断源:按键1 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
/* 配置抢占优先级 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 配置子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断通道 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 配置中断源:按键2、3,其他使用上面相关配置 */
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI_Key_Config(void) // 按键中断配置
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
KEY_Init();
/*开启按键GPIO口的时钟*/
// RCC_APB2PeriphClockCmd(KEY1_INT_GPIO_CLK,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
/* 配置 NVIC 中断*/
NVIC_Configuration();
/*--------------------------KEY1配置-----------------------------*/
/* 选择EXTI的信号源 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
/* EXTI为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 上升沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/*--------------------------KEY2配置-----------------------------*/
/* 选择EXTI的信号源 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource13);
EXTI_InitStructure.EXTI_Line = EXTI_Line13;
/* EXTI为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 下降沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/*--------------------------KEY3配置-----------------------------*/
/* 选择按键用到的GPIO */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
/* 配置为浮空输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 选择EXTI的信号源 */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource11);
EXTI_InitStructure.EXTI_Line = KEY3_INT_EXTI_LINE;
/* EXTI为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 下降沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
void EXTI0_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if (EXTI_GetITStatus(EXTI_Line0) != RESET) // key1
{
// LED1 取反
LED1_TOGGLE;
extikey = 1; // 按键一按下
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI15_10_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if (EXTI_GetITStatus(EXTI_Line13) != RESET) // key2
{
// LED2 取反
LED2_TOGGLE;
extikey = 2;
//按键二按下
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line13);
}
if (EXTI_GetITStatus(EXTI_Line11) != RESET) // key3
{
LED1(0);
extikey = 3; // 按键二按下
EXTI_ClearITPendingBit(EXTI_Line11);
}
}
3.3TIM定时器模块
static void BASIC_TIM_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
// 设置中断来源
NVIC_InitStruct.NVIC_IRQChannel = TIM7_IRQn;
// 设置主优先级为 1
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
// 设置抢占优先级为1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
static void BASIC_TIM_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
// 开启定时器时钟,即内部时钟CK_INT=72M
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
TIM_InternalClockConfig(TIM7);
// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
TIM_TimeBaseStruct.TIM_Period = 10000 - 1;
// 时钟预分频数为
TIM_TimeBaseStruct.TIM_Prescaler = 7200 - 1;
// 时钟分频因子 ,基本定时器没有,不用管
//TIM_TimeBaseStruct.TIM_ClockDivision=TIM_CKD_DIV1;
// 计数器计数模式,基本定时器只能向上计数,没有计数模式的设置
//TIM_TimeBaseStruct.TIM_CounterMode=TIM_CounterMode_Up;
// 重复计数器的值,基本定时器没有,不用管
// TIM_TimeBaseStruct.TIM_RepetitionCounter=0;
// 初始化定时器
TIM_TimeBaseInit(TIM7, &TIM_TimeBaseStruct);
}
void BASIC_TIM_Init(void)
{
BASIC_TIM_NVIC_Config();
BASIC_TIM_Config();
// 清除计数器中断标志位
TIM_ClearFlag(TIM7, TIM_FLAG_Update);
// 开启计数器中断
TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
// 使能计数器
TIM_Cmd(TIM7, ENABLE);
}
// 定时器7中断服务程序
void TIM7_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM7, TIM_IT_Update) != RESET)
//检查TIM7更新中断发生与否
{
Num++;
TIM_ClearITPendingBit(TIM7, TIM_IT_Update );
//清除TIMx更新中断标志
}
}
3.4DMA直接存储器访问模块配置
static void DMA_Mode_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
/* 打开DMA时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* 复位DMA控制器 */
DMA_DeInit(ADC_DMA_CHANNEL);
/* 配置 DMA 初始化结构体 */
/* 外设基址为:ADC 数据寄存器地址 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(ADCx1->DR));
/* 存储器地址,实际上就是一个内部SRAM的变量 */
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&ADC_ConvertedValue;
/* 数据源来自外设 */
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
/* 数据长度 */
DMA_InitStructure.DMA_BufferSize = 1;
/* 外设寄存器只有一个,地址不用递增 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
/* 存储器地址固定 */
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;
/* 外设数据大小为半字,即两个字节 */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
/* 内存数据大小也为半字,跟外设数据大小相同 */
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
/* 循环传输模式 */
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
/* DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响 */
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
/* 禁止存储器到存储器模式,因为是从外设到存储器 */
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
/* 初始化DMA */
DMA_Init(ADC_DMA_CHANNEL, &DMA_InitStructure);
/* 使能 DMA 通道 */
DMA_Cmd(ADC_DMA_CHANNEL, ENABLE);
}
3.5 ADC模/数转换模块
static void ADCx_Mode_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
/* 配置ADC时钟为PCLK2的8分频,即9MHz */
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
/* 打开ADC时钟 */
ADC_APBxClock_FUN(ADC_CLK, ENABLE);
/* ADC 模式配置 */
/* 只使用一个ADC,属于单模式 */
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
/* 禁止扫描模式,多通道才要,单通道不需要 */
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
/* 连续转换模式 */
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
/* 不用外部触发转换,软件开启即可 */
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
/* 转换结果右对齐 */
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
/* 转换通道1个 */
ADC_InitStructure.ADC_NbrOfChannel = 1;
/* 初始化ADC */
ADC_Init(ADCx1, &ADC_InitStructure);
/* 配置 ADC 通道转换顺序为1,第一个转换,采样时间为55.5个时钟周期 */
ADC_RegularChannelConfig(ADCx1, ADC_CHANNELWD, 1, ADC_SampleTime_239Cycles5);
/* 使能温度传感器和内部参考电压 */
ADC_TempSensorVrefintCmd(ENABLE);
/* 使能ADC DMA 请求 */
ADC_DMACmd(ADCx1, ENABLE);
/* 开启ADC ,并开始转换 */
ADC_Cmd(ADCx1, ENABLE);
/* 初始化ADC 校准寄存器 */
ADC_ResetCalibration(ADCx1);
/*等待校准寄存器初始化完成 */
while (ADC_GetResetCalibrationStatus(ADCx1));
/* ADC开始校准*/
ADC_StartCalibration(ADCx1);
/*等待校准完成 */
while (ADC_GetCalibrationStatus(ADCx1))
;
/* 由于没有采用外部触发,所以使用软件触发ADC转换 */
ADC_SoftwareStartConvCmd(ADCx1, ENABLE);
}
3.6RTC模块
static void RTC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; // RTC全局中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级越小,优先级越高;相同抢占优先级的中断不能嵌套;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能该通道中断
NVIC_Init(&NVIC_InitStructure); // 根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
NVIC_InitStructure.NVIC_IRQChannel = RTCAlarm_IRQn; // 闹钟中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 比RTC全局中断的优先级高
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
static void RTC_Alarm_EXIT(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_ClearITPendingBit(EXTI_Line17);
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Line = EXTI_Line17;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
u8 RTC_Init(void)
{
// 检查是不是第一次配置时钟
u8 temp = 0;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 使能PWR(电源)和BKP(存储器)外设时钟
PWR_BackupAccessCmd(ENABLE); // 使能备份寄存器(后备寄存器访问)
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) // 从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
{
BKP_DeInit(); // 复位备份区域
RCC_LSEConfig(RCC_LSE_ON); // 设置外部低速晶振(LSE),使用外设低速晶振
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && temp < 250) // 检查指定的RCC标志位设置与否,等待低速晶振就绪
{
temp++;
delay_ms(10);
}
if (temp >= 250)
return 1; // 初始化时钟失败,晶振有问题
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 设置RTC时钟(RTCCLK),选择LSE作为RTC时钟
RCC_RTCCLKCmd(ENABLE); // 使能RTC时钟
RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
RTC_WaitForSynchro(); // 等待RTC寄存器同步
RTC_ITConfig(RTC_IT_SEC | RTC_IT_ALR, ENABLE); // 使能RTC秒中断
RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
RTC_EnterConfigMode(); /// 允许配置
RTC_SetPrescaler(32768 - 1); // 设置RTC预分频的值
//设置预分频,使用外部晶振为32.768kHz,想要1S中断,则预分频设置为32767
RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
RTC_ExitConfigMode(); // 退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050); // 向指定的后备寄存器中写入用户程序数据
}
else // 系统继续计时
{
RTC_WaitForSynchro(); // 等待最近一次对RTC寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC | RTC_IT_ALR, ENABLE); // 使能RTC秒中断
RTC_WaitForLastTask(); // 等待最近一次对RTC寄存器的写操作完成
RTC_EnterConfigMode(); /// 允许配置
RTC_WaitForLastTask();
RTC_ExitConfigMode(); // 退出配置模式
}
RTC_NVIC_Config(); // RCT中断分组设置
RTC_Alarm_EXIT();
RTC_Get(); // 更新时间
return 0; // ok
}
3.7Menu菜单模块
多级菜单采用常见的结构体索引法
多级菜单是现代嵌入式系统、软件界面设计、甚至日常生活中的一项常见功能。它允许用户在层次结构中浏览不同级别的菜单项,从而简化了复杂功能的访问和使用。多级菜单的实现通常采用结构体索引法,这种方法不仅高效,而且易于维护和扩展。
在实现多级菜单时,首先需要定义菜单项的结构体。结构体通常包含以下几个关键字段:
- 菜单项名称:用于存储菜单项的名称或显示文本。
- 子菜单指针:用于指向下一级菜单的结构体数组,若为空则表示该菜单项没有子菜单。
- 操作函数指针:当菜单项选择后,执行对应的操作函数。对于叶子节点的菜单项(即没有子菜单的项),通常会绑定具体的功能操作。
- 父菜单指针:用于指向当前菜单项的上一级菜单,便于实现返回功能。
- 菜单项数量:记录当前菜单包含的子菜单数量,以便在导航时使用。
void (*current_operation_index)();
Menu_table table[30] =
{
{0, 0, 1, 0, (*home)}, // 一级界面(主页面) 索引,向下一个,确定,退出
{1, 2, 5, 0, (*Temperature)}, //二级界面 STM32F103芯片内部测温温度显示
{2, 3, 6, 0, (*Palygame)}, //二级界面 游戏
{3, 4, 7, 0, (*Setting)}, //二级界面 显示当前时间设置
{4, 1, 8, 0, (*Info)}, //二级界面 信息
{5, 5, 5, 1, (*TestTemperature)}, //三级界面:STM32F103芯片测量温度
{6, 6, 6, 2, (*ConrtolGame)}, // 三级界面:谷歌小恐龙Dinogame
{7, 7, 9, 3, (*Set)}, // 三级界面:设置时间
{8, 8, 8, 4, (*Information)}, // 三级界面:作者相关信息,系统运行总时长
};
uint8_t func_index = 0; // 主程序此时所在程序的索引值
void Menu_key_set(void)
{
if (extikey == 1 && (func_index != 6))
{
func_index = table[func_index].next; // 按键next按下后的索引号
extikey = 0;
OLED_Clear();
}
if ((extikey == 2) && (func_index != 6))
{
func_index = table[func_index].enter; // 按键enter按下后的索引号
extikey = 0;
OLED_Clear();
}
if (extikey == 3)
{
func_index = table[func_index].back; // 按键back按下后的索引号
extikey = 0;
OLED_Clear();
}
current_operation_index = table[func_index].current_operation; // 执行当前索引号所对应的功能函数
(*current_operation_index)(); // 执行当前操作函数
}
4.程序思路流程图
图2-1 STM32F103RCT6设计总体框图
图2-2 STM32F103RCT6程序设计流程图
5.心得体会
通过做这个项目,我收获了很多知识和技能,同时也有些不足,主要有以下几点:
学习了STM32F103RCT6的特性和功能:STM32F103RCT6是一款基于ARM Cortex-M3内核的高性能单片机,具有丰富的片内外设、低功耗、高速度、高可靠性等特点,适合用于各种复杂的嵌入式应用。我通过阅读其数据手册和参考手册,了解了其内部结构、寄存器配置、时钟系统、中断系统、外设驱动等内容,为后续的编程和调试打下了基础。
学习了OLED显示屏的驱动和显示:OLED显示屏是一种自发光的显示器件,具有高对比度、低功耗、可视角广、响应速度快等优点,适合用于智能手表等小型设备的显示。我通过阅读其数据手册和参考资料,了解了其工作原理、通信协议、初始化过程、显示缓存、显示命令等内容,编写了相应的驱动函数和显示函数,实现了点、线、矩形、字符、图片等显示效果。
在学习RTC的过程中,遇到了很多问题和困难,例如时钟源的选择,工作模式的切换,中断源的设置等。我通过查阅数据手册,参考示例代码,搜索网络资源,逐一解决了这些问题,也学到了很多知识和技巧。我觉得学习RTC的过程是一个非常有趣和有收获的过程,也让我对RTC的应用有了更深的理解和掌握。
随着工业化和自动化的发展,如今基本上所有项目都离不开显示终端。而多级菜单更是终端显示项目中必不可少的组成因素。项目中的多级菜单UI使用了较为常见的结构体索引法去实现功能与功能之间的来回切换,搭配STM32芯片内部测温,OLED,RTC,LED,KEY等器件实现高度智能化一体化操作。索引法的优点:可阅读性好,拓展性也不错,查找的性能差不多是最优,就是有点占用内存空间。通过后期的学习将加入RTOS系统,提高CPU利用率,提高系统的可靠性和实时性。使用局部刷新和高级算法来提高OLED屏幕的刷新率和显示效果。本项目目前还处于最初代版本,十分简易,我将继续学习去精进优化该多级菜单项目。其中,UI界面中的电池与信号目前都还处于贴图状态,通过后期的学习使用库仑计来测量电池电量。
为了增加智能手表的娱乐性,在OLED屏幕上复刻了谷歌的小恐龙游戏。通过绘制和构建游戏的图形和模型,以及设置游戏的逻辑和规则,来实现一个简单而有趣的小游戏。这个游戏可以作为一个有挑战性的项目,也可以作为一个放松心情的娱乐方式。
总的来说,这个项目让我收获了很多,不仅提高了我的硬件设计和软件开发的能力,也增强了我的创新思维和解决问题的能力,也让我体验了嵌入式系统的魅力和乐趣。我希望我能继续深入学习和探索这个领域,做出更多更好的作品。