完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
大家好,我是惊觉。失踪了三个月,我回来了。给大家带来一个好消息和一个坏消息。坏消息是,我尚未满血复活,Ardupilot第四篇将继续延期。好消息是,公众号恢复更新,先出一系列提升编码能力的文章。 全国电赛在即,昨天母校老师联系我,想让我给学弟们做下赛前培训。我做过很多年的培训,很早就发现了一个问题:同学们在为比赛做准备时,往往只注重去学习使用各种各样的传感器,自动控制算法,各种驱动。同学们只关注如何去实现功能,而忽视了如何把代码写得更好,更健壮,更易扩展和维护。如果在比赛之前,先准备好高质量的代码框架,基础模块,熟练掌握调度技巧,将极大提高赛时的开发和调试效率。 所谓高质量,涉及到很多方面,比如: 函数、变量的命名有统一的规则 基础接口要简单易用 设计模块时需层次分明,高内聚低耦合 尽量避免重复代码 笔者不打算一一讲解这些设计原则,而是介绍一些实际的基础模块,讲解它们的设计思路,注意事项和编程技巧,并在此过程中让大家理解相关的设计原则。 毫秒级定时模块 作为系列开篇,本文先介绍一个非常基础的模块:毫秒级定时模块。 友情提醒,本模块比较基础,可能有的同学对此非常熟悉,不过文末还有一个重要的小技巧噢。有基础的同学,可直接往后翻,跳到“再看comm_delay”一节。 此模块提供基础的定时功能,可细分为两种: 延时功能:毫秒极延时。 定时功能:过一段时间后执行一项操作。 可能有的同学会想,直接用单片机的定时器嘛,一个任务用一个定时器,有啥好讲的。其实不然。定时模块肯定要依赖于硬件定时器,但是一个任务用一个定时器的话,会有如下问题: 浪费资源。硬件定时器不仅仅只有定时功能,还有捕获输入信号,输出PWM,编码器等功能。如果滥用基础定时功能的话,等用到上述功能时,将无资源可用。 某个任务与某个定时器绑定在一起,提高了系统耦合程度,可移植性差。 因此,我们需要一个统一的,可移植性强的定时模块。 我们再回头看下两个基础的功能,延时和定时。 延时示例,1秒打印1次hello。comm_delay实现毫秒极延时。 void comm_delay(uint32_t ms); while (1) { printf("hellorn"); comm_mdelay(1000); } 定时示例,1秒打印1次hello。comm_get_ms返回当前系统时间,即系统从启动到现在经过了多少毫秒。 uint32_t comm_get_ms(void); while (1) { cur_time = comm_get_ms(); if (cur_time >= timeout) { printf("hellorn"); timeout = cur_time + 1000; } } 可能有的同学觉得上述两项功能差不多,而定时比延时的代码要复杂。定时的代码确实多一些,不过它具有并发能力,即支持多个定时任务同时进行。 定时示例,1秒打印1次you,2秒打印1次me。 static void show_you(void) { static uint32_t timeout = 0; uint32_t cur_time = 0; cur_time = comm_get_ms(); if(cur_time < timeout) { return; } printf("yourn"); timeout = cur_time + 1000; } static void show_me(void) { static uint32_t timeout = 0; uint32_t cur_time = 0; cur_time = comm_get_ms(); if(cur_time < timeout) { return; } printf("mern"); timeout = cur_time + 2000; } int main(int argc, char **argv) { while (1) { show_you(); show_me(); } return 0; } 小结: 延时功能需要提供一个延时函数。 定时功能需要提供获取系统时间的函数。 即: void comm_delay(uint32_t ms); uint32_t comm_get_ms(void); 其实延时函数很简单,因为它也可以看成是一个定时任务: void comm_delay(uint32_t ms) { uint32_t timeout = comm_get_ms() + ms; while(comm_get_ms() < timeout); } 系统时间 实现comm_get_ms,即记录系统时间,自然要靠硬件定时器啦。大家用的单片机,无论是TI,STM32,NXP等,大部分都是cortex-m的内核,该内核有一个专门干这事情的定时器:SysTick timer。其名称为系统滴答定时器,只有简单的定时功能。配置好reload计数并使能后,其由reload值递减至0,触发中断,再从reload递减。如果根据其时钟配置相应的reload值,实现每1ms触发1次中断,那就可以记录毫秒 级的系统时间。 其配置函数位于单片机驱动库的CMSIS组件中,一般的工程都包含了这个组件,比如笔者使用TRUEStudio创建的工程: 下面是stm32的示例。SystemCoreClock为单片机的主频,这也是SysTick的输入时钟。SystemCoreClock / 1000即为1ms的定时计数,将reload配置为此值即可实现每1ms触发1次定时中断。其他单片机方法类似。 #include "stm32l1xx.h" #include static uint32_t sys_tick = 0; void sys_tick_init(void) { if(SysTick_Config(SystemCoreClock / 1000)); NVIC_SetPriority(SysTick_IRQn, 0); } uint32_t sys_tick_get(void) { return sys_tick; } void SysTick_Handler(void) { sys_tick++; } 用法非常简单,单片机启动时调用sys_tick_init配置并使能SysTick,每1ms触发1次SysTick_Handler,其内对当前时间sys_tick进行加1操作。应用层通过sys_tick_get获取当前时间。 SysTick_Handler在中断向量表中指定,大家根据具体的MCU对号入座。 comm_get_ms只需对sys_tick_get进行简单的封装: uint32_t comm_get_ms(void) { return sys_tick_get(); } 再看comm_delay 我们再看一下毫秒极延时的实现,大家觉得它有问题吗? void comm_delay(uint32_t ms) { uint32_t timeout = comm_get_ms() + ms; while(comm_get_ms() < timeout); } 对于只需要进行几分钟演示的电赛来说,它没有问题。不过,电赛只是同学们实践所学的一条途径。正儿八经的产品,需要具有足够的健壮性,可长期稳定地运行。上述代码能长期运行吗? static uint32_t sys_tick = 0; comm_get_ms是对sys_tick_get的简单封装,而sys_tick_get返回的是一个32位无符号整型变量,它记录的是系统从启动到现在所经过的毫秒数。32位无符号整型变量最大能表示多长的时间呢? 2^32 / 1000 / 3600 / 24 = 49.71 其可记录49天。在49天后,sys_tick将会溢出,从零重新开始累加。为了方便描述,要使用到一个宏UINT32_MAX。UINT32_MAX表示32位无符号整型变量的最大值,即0xffffffff。 假设我们要延时1分钟,即60000ms。当前时间为UINT32_MAX - 59999,下面计算timeout。 uint32_t timeout = comm_get_ms() + ms; timeout发生溢出,计算结果为0。 UINT32_MAX - 59999 + 60000 = UINT32_MAX + 1 = 0 那么下面的等待循环将立刻退出,而不需要等待1分钟。 while(comm_get_ms() < timeout); 在接下来的1分钟内,comm_get_ms(60000)都是失效的,每1分钟执行1次的任务将不停地执行。还有其他溢出的场景,这里不再一一描述。我们只要明确一点就好:comm_delay不能长期运行。 健壮的comm_delay 怎么修改comm_delay以解决溢出问题呢?其实很简单,直接给出答案: void comm_delay(uint32_t ms) { uint32_t timeout = comm_get_ms() + ms; while(comm_get_ms() - timeout > UINT32_MAX / 2); } 我们简单地验证几个场景: 当前时间为10ms,延时2ms。 先计算timeout: timeout = 10 + 2 = 12 下面看看从现在开始,什么时候while(comm_get_ms() - timeout > UINT32_MAX / 2);会退出。 第0ms秒时,当前时间为10,10 - 12 = -2,请注意,comm_get_ms() - timeout中的操作数都是uint32_t类型,即32位无符号整型,它们相减的结果还是无符号整型。所以-2 --> UINT32_MAX - 1 > UINT32_MAX / 2,循环继续。 第2秒时,当前时间为12,12 - 12 = 0 < UINT32_MAX / 2,循环等待结束。 此种场景,成功实现延时2ms。 当前时间为(UINT32_MAX - 1)ms,延时2ms。 之所以定成UINT32_MAX - 1,是想测试时间溢出的场景,2ms后时间溢出。 先计算timeout,timeout在加2时溢出,最终结果为0。 timeout = (UINT32_MAX - 1) + 2 = UINT32_MAX + 1 = 0 下面看看从现在开始,什么时候while(comm_get_ms() - timeout > UINT32_MAX / 2);会退出。 第0秒时,当前时间为(UINT32_MAX - 1),(UINT32_MAX - 1) - 0 = (UINT32_MAX - 1) > UINT32_MAX / 2,循环继续。 第2秒时,当前时间如timeout一样加2溢出,最终为0。0 - 0 = 0 < UINT32_MAX / 2,循环等待结束。 此种场景,成功实现延时2ms。 总结 经过两种情况的测试,我们发现,无论计算过程中时间有无溢出,改进后的comm_delay都圆满完成延时。 这种实现的原理是什么呢?原理很重要噢,否则大家在使用时,可能把大于和小于关系搞反,或者是把被减数与减数的关系搞反。至于原理是什么呢,今天来不及讲了,请待下回分解。 |
|
|
|
只有小组成员才能发言,加入小组>>
3309 浏览 9 评论
2988 浏览 16 评论
3490 浏览 1 评论
9049 浏览 16 评论
4083 浏览 18 评论
1167浏览 3评论
601浏览 2评论
const uint16_t Tab[10]={0}; const uint16_t *p; p = Tab;//报错是怎么回事?
592浏览 2评论
用NUC131单片机UART3作为打印口,但printf没有输出东西是什么原因?
2329浏览 2评论
NUC980DK61YC启动随机性出现Err-DDR是为什么?
1892浏览 2评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-19 02:48 , Processed in 1.153124 second(s), Total 79, Slave 60 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号