使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业6 三个时钟:系统脉搏、现实时间与定时器
零、 创新点
《嵌入式技术与基础 第六版》原书教材随书附赠了一块stm32L431的开发板,配套内容都是以此为基础打造的,甚至作者还专门为此开发了一套ide,但是:
- 现在stm32主流的教程生态用的都是stm32f103c8t6这款芯片,原书的教程虽然能很好的为你解释嵌入式开发的原理,但是要进一步学习,你还是不得不寻找其他教程,购买主流的开发板
- 作者的ide完全是出于本书学习设计的,对于实际开发毫无意义,若将来想做点嵌入式,还需要重新学习一套开发栈
- 全新套书卖100。作为抠门带血生,肯定要考虑二手的,但是二手书通常不会带有原书的附件。单买开发板,某宝60,某鱼甚至没有。起码截至我使用该书学习的时间(2024年3月)是这样的,后期使用此书的前辈多了,估计二手市场会变好。
- 最重要的是,我已经有一个stm32f103c8t6的开发板了
综上,无论是从学习角度还是省钱角度,使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容都是非常合适的
这套博客是广州大学计算机学院嵌入式系统课程的作业,我用stm32f103c8t6开荒,也是为了后来的同学做贡献
一、 系统脉搏:systick中断
1. 简介
如果晶振是stm32物理上的心脏,发出的时钟信号指挥时序逻辑电路步调一致,那么systick就是stm32软件上的脉搏
STM32中的Systick是一个非常实用的内嵌硬件模块,主要用于提供精确的定时和延时功能。它是ARM Cortex-M系列处理器中的一种系统定时器,专为实时操作系统(RTOS)设计,但同样适用于无操作系统环境下的任务调度、心跳定时、延时函数实现等多种应用场景。下面是Systick的一些主要特点和作用:
- 时间基准:Systick可以作为一个精确的时间基准源,为系统提供固定周期的时钟脉冲。这对于需要定时执行某些操作的应用来说至关重要,比如控制循环的时序管理、定时中断触发等。
- 延时功能:在没有操作系统或轻量级应用中,Systick常被用来实现软件延时。通过配置Systick的重载寄存器和计数方式,可以方便地产生精确的延时时间,这对于初始化代码、简单状态机控制等场景非常有用。
- RTOS支持:对于运行在STM32上的实时操作系统(RTOS),Systick通常作为系统滴答定时器使用,是RTOS进行任务调度、时间片管理的基础。它帮助RTOS跟踪系统时间和决定何时切换到下一个任务,保证了任务执行的实时性。
- 心跳定时:在一些监控或自检系统中,Systick可以作为心跳定时器,定期触发自检程序或者发送心跳信号,确保系统正常运行并及时发现异常情况。
- 计数器功能:虽然Systick本质上是一个向下计数的计数器,但它也可以通过编程实现计数器的多种应用,比如事件计数、循环计数等。
Systick的结构相对简单,主要包括以下几个关键部分:
- 计数器寄存器:用于设置或读取当前计数值。
- 重载寄存器:设定计数器的重载值,当计数器递减到0时,会自动重新加载这个值并继续计数。
- 控制及状态寄存器:用于配置Systick的工作模式(例如是否使能、是否采用系统时钟作为输入等)以及查询当前状态。
- 校正值寄存器(可选):在某些Cortex-M处理器中,用于补偿系统时钟的不精确性。
Systick的优点在于其高精度、低 overhead(因为它直接连接到CPU内核),且不占用额外的GPIO或外设资源,非常适合于嵌入式系统中的各种定时需求。
2. HAL库中的代码
这是hal库systick中断的相关函数
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
我们常用的HAL_Delay()
函数就依赖于uwTick
变量
3. 小实验
利用SysTick定时器编写倒计时程序,如初始设置为2分30秒,每秒在屏幕上输出一次时间,倒计时为0后,红灯亮,停止屏幕输出,并关闭SysTick定时器的中断
HAL_IncTick
函数是若定义的,可以通过在主函数中重写HAL_IncTick
函数以实验计时
以下是关键代码:
int timems_counter = -1;
void HAL_IncTick(void)
{
uwTick += uwTickFreq;
if (timems_counter != -1){
if(timems_counter == 0){
printf("\r\nend\n");
HAL_GPIO_TogglePin(RED_GPIO_Port, RED_Pin);
timems_counter = -1;
}
else{
if(timems_counter%1000 == 0){
printf("\r\ntime:%d\n",timems_counter);
}
timems_counter--;
}
}
}
int main(){
//cude ide 自动生成的相关初始化代码 ......
printf("\r\nstart!\n");
timems_counter = (2 * 60 + 30)*1000;
for(;;){
}
}
实验结果符合预期
二、 现实时间:RTC
1. 简介
RTC,即Real-Time Clock,中文译为实时时钟,是一种在微控制器或计算机系统中用于追踪当前日期和时间的专用电路或模块。它能够在设备断电后继续运行,依靠电池或其他低功耗电源维持时间的准确性,从而确保系统在重新上电时能立即获得准确的时间信息。RTC通常提供秒、分、小时、日期、月份、年份以及星期等信息,并且可以配置闹钟功能以在特定时间触发中断。
在STM32系列微控制器中,RTC是一个集成的硬件模块,设计用于实现低功耗下的时间跟踪功能。STM32的RTC具有以下特点:
- 低功耗运行:STM32的RTC可以在所有电源模式下工作,包括低功耗模式,这使得它成为电池供电设备的理想选择。
- 时钟源多样性:STM32的RTC支持多种时钟源,包括低速内部时钟(LSI)、外部32.768kHz晶体振荡器或者通过PLL输出的时钟。使用外部晶振能够提供更高的时钟精度。
- 日历功能:提供完整的日历功能,可以设置和读取年、月、日、星期、时、分、秒等信息。
- 闹钟功能:STM32的RTC支持多个闹钟配置,可以设置一次性闹钟或者周期性闹钟,当到达设定时间时,会生成中断请求,唤醒系统或执行特定任务。
- 时间戳功能:可以作为时间戳使用,记录事件发生的确切时间,这对于数据记录、故障诊断等应用非常重要。
- 定时器功能:除了标准的时间日期功能外,RTC还可以作为长时间定时器使用,非常适合需要长时间计时的应用场景。
- 校准功能:某些STM32型号的RTC还提供了校准功能,允许用户通过软件调整时钟速率,以补偿由于温度变化或其他因素引起的时钟漂移。
使用STM32的RTC通常涉及以下几个步骤:
- 初始化RTC时钟源和控制寄存器。
- 配置RTC的日历(如日期和时间)。
- 如果需要,设置闹钟或定时器功能。
- 开启RTC并根据需要使能相关中断。
- 在应用中定期读取RTC值或等待中断信号进行时间管理。
通过STM32的HAL库或LL库,开发者可以方便地配置和控制RTC模块,实现各种时间相关的应用需求。
2. HAL库中的代码
初始化RTC
在使用RTC之前,首先需要对其进行初始化。这包括设置RTC的时钟源、预分频等参数。
HAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);
设置时间
使用HAL库函数设置RTC的时间。
HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
其中,sTime
是一个结构体,包含小时、分钟、秒等信息,Format
指定时间格式,通常为RTC_FORMAT_BIN
或RTC_FORMAT_BCD
。
设置日期
类似地,可以设置RTC的日期。
HAL_StatusTypeDef HAL_RTC_SetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
RTC_DateTypeDef
结构体包含了年、月、日和星期等信息。
读取时间与日期
读取RTC当前的时间和日期。
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format);
配置闹钟
设置RTC闹钟,可以设置闹钟的时间和闹钟的模式(例如,一次触发、周期性等)。
HAL_StatusTypeDef HAL_RTC_SetAlarm(RTC_HandleTypeDef *hrtc, RTC_AlarmTypeDef *sAlarm, RTC_AlarmTypeDef *AlarmSubSecond);
其中,sAlarm
定义了闹钟的具体时间,而AlarmSubSecond
(可选)可以用来设置亚秒级的精度。
闹钟中断处理
当闹钟触发时,通常会通过中断服务例程来响应。
void HAL_RTC_AlarmIRQHandler(RTC_HandleTypeDef *hrtc){
HAL_RTC_AlarmIRQHandler(&hrtc);
}
在中断服务例程中,你需要清除中断标志,并执行相应的处理逻辑。
3. 小实验
利用RTC显示日期(年月日、时分秒),每秒更新。并设置某个时间的闹钟。闹钟时间到时,屏幕上显示有你的姓名的文字,并点亮绿灯。
先在cube ide中配置,启动rtc及其闹钟中断
HAL_RTC_AlarmIRQHandler
会回调HAL_RTC_AlarmAEventCallback
函数,所以我们这里重定义该函数即可实现中断后自动调用我们的代码
关键代码如下:
void Rtc_time_show(void)
{
RTC_TimeTypeDef gTim={0};
RTC_DateTypeDef gDat={0};
HAL_RTC_GetTime(&hrtc,&gTim,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&gDat,RTC_FORMAT_BIN);
printf("\r\nNow Date:20%02d-%02d-%02d\n",gDat.Year,gDat.Month,gDat.Date);
printf("\r\nNow Time:%02d:%02d:%02d\n",gTim.Hours,gTim.Minutes,gTim.Seconds);
}
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc){
printf("\r\nluokairui:alarm interupt\n");
HAL_GPIO_TogglePin(GREEN_GPIO_Port, GREEN_Pin);
}
int main(){
//cude ide 自动生成的相关初始化代码 ......
RTC_AlarmTypeDef sAlarm ;
sAlarm.Alarm=RTC_ALARM_A;//闹钟A
sAlarm.AlarmTime.Hours=0x0;//时
sAlarm.AlarmTime.Minutes=0x0;//分
sAlarm.AlarmTime.Seconds=0x10;//秒
HAL_RTC_SetAlarm_IT(&hrtc,&sAlarm,FORMAT_BCD);//启动闹钟
for(;;){
HAL_GPIO_TogglePin(YELLO_GPIO_Port, YELLO_Pin);
Rtc_time_show();
HAL_Delay(1000);
}
}
结果符合预期
三、 定时器: TIM
1. 简介
STM32系列微控制器集成了多种功能强大的定时器,称为TIM(Timer),这些定时器可以用于多种目的,包括产生精确的延时、生成PWM信号、执行输入捕获(如测量脉冲宽度)、输出比较以及编码器接口等。根据功能和复杂度的不同,STM32的定时器大致可以分为以下几类:
- 基本定时器(Basic Timers):包括TIM6和TIM7。这类定时器较为简单,通常只有计数器和自动重载寄存器,没有外部触发输入和输出比较功能。它们通常用于简单的延时或定时任务,只能向上计数,并且使用内部时钟源。
- 通用定时器(General Purpose Timers):如TIM2至TIM5以及TIM9至TIM14。这些定时器比基本定时器更强大,具有更多的功能,如外部触发输入、输出比较、中断和DMA请求等。它们可以用于生成PWM、输入捕获、定时等多样化的应用。通用定时器通常有4个独立的通道,可以配置为输入捕获或输出比较模式。
- 高级定时器(Advanced Timers):包括TIM1和TIM8。高级定时器功能最为全面,除了具备通用定时器的所有功能外,还额外支持更复杂的操作,比如针对电机控制的互补输出、更多通道、更高分辨率的计数器(例如TIM1具有16位的计数器和4个16位的自动重载寄存器)、以及更灵活的时钟源选择。它们常用于高级控制算法,如PID控制、电机控制等。
TIM的工作原理:
- 时钟源:定时器的工作首先需要一个时钟源,这可以是系统时钟经过分频后的时钟,或者是外部时钟输入。
- 预分频器(Prescaler, PSC):时钟源的频率通常较高,为了获得更宽范围的定时周期,定时器会有一个预分频器来降低时钟频率。预分频的计算公式通常是
CK_CNT = CK_PSC / (PSC + 1)
,其中CK_PSC
是输入时钟频率,CK_CNT
是计数器时钟频率。 - 计数器(Counter):计数器根据预分频后的时钟进行递增或递减计数。当计数器从0计数到自动重装载寄存器(ARR)的值时,会产生一个溢出事件,这时计数器会重新从0开始计数,并可以触发中断或DMA传输。
- 自动重装载寄存器(Auto-Reload Register, ARR):定义了计数器的最大计数值,也是定时周期的基础。
- 中断和DMA:定时器可以在特定事件(如溢出、比较匹配等)发生时产生中断,或者通过DMA传输数据,以通知CPU或自动处理数据。
配置STM32的TIM定时器通常涉及以下几个步骤:
- 初始化定时器结构体:创建并初始化一个
TIM_HandleTypeDef
结构体,指定使用的定时器实例、初始化参数(如时钟源、预分频值、自动重装载值等)。 - 时钟使能:通过HAL库或直接设置寄存器使能对应的时钟。
- 配置定时器模式:根据需求配置为定时模式、输入捕获模式、输出比较模式等。
- NVIC配置:如果需要使用中断,还需要配置NVIC(嵌套向量中断控制器)。
- 启动定时器:调用相应的函数启动定时器。
在编程时,开发者通常使用STM32CubeMX工具来图形化配置定时器,并自动生成初始化代码,然后在用户代码中添加具体的定时器控制逻辑。
2. HAL库中的代码
初始化定时器
初始化基本定时器(不包含中断或DMA)。
HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);
配置定时器时钟源和分频
配置定时器的时钟源和预分频器。
HAL_StatusTypeDef HAL_TIM_OscConfig(TIM_HandleTypeDef *htim);
启动定时器
启动基本定时器(连续向上计数模式)。
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim);
启用定时器中断并启动
启动定时器并在到达更新事件时触发中断。
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);
设置定时器计数值
设置TIM计数器的当前值。
void HAL_TIM_SetCounter(TIM_HandleTypeDef *htim, uint32_t Counter);
获取定时器计数值
获取TIM计数器的当前值。
uint32_t HAL_TIM_GetCounter(TIM_HandleTypeDef *htim);
在中断服务程序中处理定时器事件
处理定时器中断,通常需要用户在这个函数中调用以处理具体的中断情况。
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim);
输入捕获相关函数
输入捕获中断回调函数,当输入捕获事件发生时被调用。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim);
输出比较/PWM生成
配置定时器的某个通道为输出比较模式,常用于生成PWM信号。
HAL_StatusTypeDef HAL_TIM_OC_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef *sConfigOC, uint32_t Channel);
3. 小实验
利用PWM脉宽调制,交替显示红灯的5个短闪和5个长闪。
先在cube ide中配置tim1定时器通道1 PWM生成模式
关键代码如下:
int main()
{
//cude ide 自动生成的相关初始化代码 ......
HAL_TIM_Base_Start(&htim1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
for (;;)
{
for (int j = 0; j < 5; j++)
{
for (int i = 0; i < 100; i++)
{
__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
for (int i = 100; i > 0; i--)
{
__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, i);
HAL_Delay(1);
}
}
for (int j = 0; j < 5; j++)
{
for (int i = 0; i < 100; i++)
{
__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, i);
HAL_Delay(5);
}
for (int i = 100; i > 0; i--)
{
__HAL_TIM_SetCompare(&htim1, TIM_CHANNEL_1, i);
HAL_Delay(5);
}
}
}
}
结果符合预期
关于作者:
- 邮箱:luokairui@carry.fit
- 个人博客:carry blog
- CSDN主页:_:Carry-CSDN博客
- Github主页:C-a-r-r-y
欢迎联系!