一、multi_timer简介
网红multi_timer是一个极其轻量的软件定时器,只要你的MCU容量够的情况下,就可以无限拓展成为N个定时器,这在一定程度上方便了定期器资源较少的MCU,但有经验的老工程师会说:"我可以只用一个定时器,用计数器+标志位的方式也可以COPY出N个定时器呀,资源少也阻挡不了我对它的充分利用"。是的没错,但multi_timer对比老工程师方法的优势在哪里呢?它可以取代传统的标志位+计数器的判断方式,让程序看起来更加优雅更加好维护。
特点:简单、优雅、便捷、易维护
二、multi_timer的使用方法
1、定义一个multi_timer结构体变量
Timer timer1 ;
2、注册并初始化multi_timer定时器
timer_init(&timer1, timer1_callback, TIMER_TIMEOUT_500MS, TIMER_TIMEOUT_500MS);
3、启动multi_timer定时器
timer_start(&timer1);
4、设置1ms硬件定时器循环调用计数器以提供时基
void xxx_callback(void) { timer_ticks(); }
5、在while循环中循环调用multi_timer的后台处理函数
while(1) { //.... timer_loop(); }
三、multi_timer实战
实战演练1:在STC15F104W-35I-SOP8上实践
买这个小模块的原因是后面需要做一些开源项目,想做一些传感器,最后用stm32或者其它的MCU与它建立通信用,还有一个用途就是以后移植一些开源项目,我希望现在低端一点的平台上验证(如果低端跑不了就直接上能跑的),后面再在高端点的平台上实践,有时间有条件我也会多在别的平台上跑跑,这样相当于积累了多个平台的开发经验,这款CPU完全兼容51单片机的指令集,所以把它当成51单片机来用就行了。
这个小板子对应的原理图如下:
限于文章篇幅,如果想多了解这个小板子的信息,可以去我的CSDN博客上看看,之前写了介绍:
https://blog.csdn.net/morixinguan/article/details/105130462
下面直接看实战需求功能描述:
1、用multi_timer创建软件定时器1,用来以500ms的频率让LED灯交替闪烁。 2、用multi_timer创建软件定时器2,当定时10s到达以后,常亮LED,并且删除multi_timer创建的软件定时器1和软件定时器2。
创建51的Keil4工程,然后开始编写代码:
1、打开Keil4,然后创建一个AT89C51的工程
2、将multi_timer添加到keil4工程
3、创建一个Package目录,将multi_timer的程序文件添加进来
4、编写代码
#include <reg51.h> #include "multi_timer.h" Timer timer1 ; Timer timer2 ; /*用于定时10s的计数器*/ int Counter = 0 ; /*根据板子原理图,灯位于P3^3*/ sbit LED = P3 ^ 3 ; /*晶振频率为12M*/ #define FOSC 12000000L /*指令速度为12T*/ #define command_speed 12 /*用multi_timer创建的定时器1定时时间 单位:ms*/ #define TIMER_TIMEOUT_500MS 500 /*用multi_timer创建的定时器2定时时间 单位:ms*/ #define TIMER_TIMEOUT_1S 1000 void timer0_init(void); void timer1_callback(void); void timer2_callback(void); void main(void) { LED = 0; timer0_init(); timer_init(&timer1, timer1_callback, TIMER_TIMEOUT_500MS, TIMER_TIMEOUT_500MS); timer_init(&timer2, timer2_callback, TIMER_TIMEOUT_1S, TIMER_TIMEOUT_1S); timer_start(&timer1); timer_start(&timer2); while(1) { timer_loop(); } } /*multi_timer回调函数1调用*/ void timer1_callback(void) { /*LED灯电平翻转*/ LED = !LED ; } /*multi_timer回调函数2调用*/ void timer2_callback(void) { /*当计数器到达10次以后删除所有创建的软件定时器 计数器清0,将LED电平置为1,常亮 */ ++Counter ; if(Counter == 10) { Counter = 0 ; LED = 1 ; timer_stop(&timer1); timer_stop(&timer2); } } /*硬件定时器初始化*/ void timer0_init(void) { TMOD = 0x00; TH0 = (65536 - FOSC / command_speed / 1000) >> 8; TL0 = (65536 - FOSC / command_speed / 1000); EA = 1; ET0 = 1; TR0 = 1; } /*利用系统定时器产生1ms的定时中断*/ void timer0() interrupt 1 { TH0 = (65536 - FOSC / command_speed / 1000) >> 8; TL0 = (65536 - FOSC / command_speed / 1000); /*multi_timer计数器自增*/ timer_ticks(); }
4、程序编译与固件生成
我们看到编译过后,整个程序的大小仅占用1.3K多,确实够轻量!接下来将生成的.hex文件下载到开发板上。
最终程序按照我的设计思路完美运行!这里相当于带大家重新复习了下51单片机平台的基本使用。
实战演练2:在小熊派开发板上实战
关于小熊派,这是一个基于stm32的网红物联网开发平台,昨天收到王总给我赠送的板子以后就写了相关的评测,大家可以点击下面链接看:
接下来我们在这个平台上把实战演练1的需求实现一下,首先先看小熊派开发板的原理图,找到LED的位置:
使用stm32cubmx配置基础工程:
1、芯片选型,这里选择stm32l431rctx
2、配置rcc时钟以及串行调试接口
这里我选择的是高速,时钟的话,直接用系统默认的内部时钟也可以,时钟默认配置最高80MHz。
因为以前被坑过,导致程序没法下载了,所以习惯性配置这个选项,后续有时间我写篇文章解释下。
3、配置LED
4、配置串口调试
方便根据调试信息查看程序执行流程。
5、生成Keil5基础工程
实际开发建议硬件外设分模块,这样看起来不要把所有的生成全部都挤到main.c里面去了,这点让我非常讨厌,所以生成工程时候习惯点击设置以下这一项:
接下来点击生成代码:
1、将multi_timer添加到keil5工程
2、创建一个Package目录,将multi_timer的程序文件添加进来
3、编写代码
由于篇幅限制,只看我自己代码添加的位置:
main.h
/* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ /*添加必要的头文件*/ #include <stdio.h> #include "multi_timer.h" /* USER CODE END Includes */
main.c
/* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /*用multi_timer创建的定时器1定时时间 单位:ms*/ #define TIMER_TIMEOUT_500MS 500 /*用multi_timer创建的定时器2定时时间 单位:ms*/ #define TIMER_TIMEOUT_1S 1000 /* USER CODE END PD */ /* USER CODE BEGIN PV */ Timer timer1 ; Timer timer2 ; /*用于定时10s的计数器*/ int Counter = 0 ; /* USER CODE END PV */ /* USER CODE BEGIN PFP */ /*定义重定向,这样才能使用printf函数*/ int fputc(int ch, FILE *file) { return HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000); } /*multi_timer回调函数1调用*/ void timer1_callback(void) { /*LED灯电平翻转*/ //LED = !LED ; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } /*multi_timer回调函数2调用*/ void timer2_callback(void) { /*当计数器到达10次以后删除所有创建的软件定时器 计数器清0,将LED电平置为1,常亮 */ ++Counter ; if(Counter == 10) { Counter = 0 ; //LED = 1 ; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); printf("LED灯常亮\n"); timer_stop(&timer1); printf("关闭定时器1\n"); timer_stop(&timer2); printf("关闭定时器2\n"); } } /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ timer_init(&timer1, timer1_callback, TIMER_TIMEOUT_500MS, TIMER_TIMEOUT_500MS); timer_init(&timer2, timer2_callback, TIMER_TIMEOUT_1S, TIMER_TIMEOUT_1S); timer_start(&timer1); timer_start(&timer2); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ timer_loop(); } /* USER CODE END 3 */ }
stm32l4xx_it.c
这里利用系统时钟的1ms的时基,就不用重新去创建一个硬件定时器了。
/** * @brief This function handles System tick timer. */ void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ timer_ticks(); /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ /* USER CODE END SysTick_IRQn 1 */ }
4、程序编译与固件生成
生成固件
选择调试和下载器,这里是st-link
选择下载程序后复位,如果不选择则需要按开发板上的硬件复位。
编译成功后直接点击下载
5、程序执行(看串口调试助手)
实验成功!
四、multi_timer设计思想
所谓有道是知其然而知所以然,这么优秀的作品,我们有必要来了解一下:
4.1 multi_timer的数据结构及参数含义
typedef struct Timer { uint32_t timeout; uint32_t repeat; void (*timeout_cb)(void); struct Timer* next; } Timer;
参数含义:
参数 | 含义 |
timeout | 定时时间(ms) |
repeat | 循环定时触发时间 |
timeout_cb | 定时器回调处理函数 |
next | 指向下一个定时器节点 |
4.2 multi_timer的函数解析
程序文件里的全局变量:
//timer handle list head. static struct Timer* head_handle = NULL; //Timer ticks static uint32_t _timer_ticks = 0;
4.2.1 定时时基触发:timer_ticks
/** * @brief background ticks, timer repeat invoking interval 1ms. * @param None. * @retval None. */ void timer_ticks() { _timer_ticks++; }
这个函数的功能主要是产生计数,而产生计数一定要有另外一个介质去驱动它运行。
4.2.2 定时器初始化:timer_init
//Timer ticks static uint32_t _timer_ticks = 0; /** * @brief Initializes the timer struct handle. * @param handle: the timer handle strcut. * @param timeout_cb: timeout callback. * @param repeat: repeat interval time. * @retval None */ void timer_init(struct Timer* handle, void(*timeout_cb)(), uint32_t timeout, uint32_t repeat) { handle->timeout_cb = timeout_cb; handle->timeout = _timer_ticks + timeout; handle->repeat = repeat; }
初始化主要是给结构体参数进行赋值操作,首先要确定是哪个结构体成员,由handle参数确定,当定时时间到了要做什么事情,由timeout_cb(定时器回调处理函数)参数确定,定时时间多长才会触发所谓的事情,由timeout(定时时间)参数确定,如果这个功能需要重复触发,我们就需要给repeat(循环定时触发时间)参数()指定。
4.2.3 定时器启动:timer_start
/** * @brief Start the timer work, add the handle into work list. * @param btn: target handle strcut. * @retval 0: succeed. -1: already exist. */ int timer_start(struct Timer* handle) { struct Timer* target = head_handle; while(target) { if(target == handle) return -1; //already exist. target = target->next; } handle->next = head_handle; head_handle = handle; return 0; }
这里将定时器句柄添加到链表里进行保存,循环指向链表的下一个节点去添加定时器节点,如果发现是同一个定时器句柄,则直接返回-1,表示当前添加句柄不合法。
4.2.4 定时器停止:timer_stop
/** * @brief Stop the timer work, remove the handle off work list. * @param handle: target handle strcut. * @retval None */ void timer_stop(struct Timer* handle) { struct Timer** curr; for(curr = &head_handle; *curr; ) { struct Timer* entry = *curr; if (entry == handle) { *curr = entry->next; } else curr = &entry->next; } }
这里非常巧妙的使用了一个二级指针curr,指向了对应定时器句柄的地址,通过循环遍历,找到对应的句柄后将其删除。
4.2.5 定时器循环调用后台处理:timer_loop
/** * @brief main loop. * @param None. * @retval None */ void timer_loop() { struct Timer* target; for(target = head_handle; target; target = target->next) { if(_timer_ticks >= target->timeout) { if(target->repeat == 0) { timer_stop(target); } else { target->timeout = _timer_ticks + target->repeat; } target->timeout_cb(); } } }
这个函数实现非常简单,就是通过不断遍历链表各个节点,判断是否到达定时时间(timeout参数),如果到达了定时时间,没有指定循环定时触发时间(repeat参数)的时候,这时就会把当前定时器句柄给移除,如果指定了循环定时触发时间(repeat参数),则定时时间会被重新赋值,直到下一个定时到来,接下来会一直循环触发。
不得不说,这真是一个良好的程序设计,值得学习和应用,针对作者这样的思想,还可以衍生出更多的开源项目。
实践工程下载
链接:https://pan.baidu.com/s/1xwCnkMDnwjPTrKd8ulw58w 提取码:eo5y 复制这段内容后打开百度网盘手机App,操作更方便哦
注意:
大部分朋友的电脑都同时装了Keil5和Keil4,不小心操作有时打开Keil4工程会卡死,解决方法如下:
将下面这个文件删除,再重新打开就不会了。
往期精彩
实战经验:stm32cubMX自动生成遇到的坑爹问题(Freertos)