历史上的今天
返回首页

历史上的今天

今天是:2024年10月22日(星期二)

正在发生

2019年10月22日 | 请工程化,定制化你的单片机代码

2019-10-22 来源:51hei

- 前言 -

时至今日,依旧看到很多小伙伴们放着单片机里的定时器不用,动辄delay1s(); delay500ms();虽然简单粗暴,但是其实是很不妥当的。


还有很多需要按键的程序,动不动就“while(k1==0);”的等按键松开,这样的代码只是“为了满足某个功能“而设计的代码,而非”为了保证产品的质量“而设计的代码。

我们应该时刻考虑“如果这傻逼用户不按我的标准操作来搞我的产品,我该怎么写程序?”,而不是”只要客户乖乖听我的,这样做是没问题的,就行了;出了问题都是他乱搞,跟我有个毛关系”,后面的思维是典型的只顾自己爽的思维,我们应当想方设法的提高产品的可靠度。


OK,扯远了,现在只是想提出一些设计的思路。


part1 . 系统的时基 & 依托于时基去设计函数


- 系统的时钟基线 -

关键词,是“时钟基线”。

我们需要设置一个定时器,这个定时器会告诉单片机:“好的,现在已经过了1ms了(或者100ms?这都没关系)”单片机一拍脑袋:“啊,已经到了1ms了么,那我就把某一件事情做一下吧”。

大致像这样:

这个程序的意思就是,让单片机每隔1ms就do_sth()一次(定时器的初始化函数省略了没画);

这样写还可以让中断函数非常的简洁,几乎一进去就出来,基本不会影响到被中断打断的函数的工作;

以传递关键参数的形式,大循环里只要查一查这个flag1ms值,知道什么时间要做什么事情就对了。


- 按键函数举例 -

上图中这个do_sth()可以是任意函数,以按键函数keyPress()为例,可以这样写:

void keyPress(){

    static unsigned int key_press_time = 0;  // ……请记得标为静态变量

    if(K1==0){

        if(++key_press_time <=0 ) --key_press_time;//计量按键时间,并避免数据溢出

        if(key_press_time==3000){

            //在此写下按键长按3s时要做的事情;

        }

    }else{

        if(20<=key_press_time && key_press_time < 3000){

            //大于20ms小于3s,视为短按,在此写下写短按的处理代码

        }

        key_press_time=0;

    }

}


这里有很多可以研究的地方:

①“if(++key_press_time <=0 ) --key_press_time;”这一句,看起来用“++key_press_time”就能搞定,但是,谁也不能保证沙雕用户真的不会按按键超过65秒的啊;万一他真的按了65576ms;单片机还就真的以为用户“短按”了一次呢(65576-65536=40ms,属于短按范畴),这下那个短按程序段也会被执行一次;现在这样写,哪怕你按100年也没关系了,反正我的单片机就每隔1ms进来看一次,K1这个按钮你想按多久就按多久,掉在我的范围内我就处理你,超出我的范围我就无视你。


②“if(key_press_time==3000){ ; }”这里的3000只是随便设置的的一个时间(3s),如果你需要做按键长短按功能,这里就是你的长按程序所放的位置;你也可以不用3000;用2000,50000都没事,别超过65534就行(提问:为什么这里又是65534呢?);


③“if(20<=key_press_time && key_press_time < 3000)”这里,前面的>=20是个消抖的设计,客户再强也是人类,再强的人类也不可能1秒按一个按键超过20次;也就是不可能短于50ms的时间;这里用了20ms相当于兼容 男上加男的快男 也来按按键了。后面<3000是不能和长按的时间冲突,因为3s我们已经人为的设置成长按时间节点了。


好,现在知道相关的程序该怎么写了,我们回到刚刚讨论的时钟基线处。



-  多个时钟基线的方案  -

刚刚的例子中我们只展示了1ms的时基,对于单片机来说,1ms已经很慢了,但对于人和某些设备来说来说还是快的不行,比如一些本就需要10ms间隔的通信设置,需要隔100ms才能刷一次的显示,需要50ms才能重新采的某些传感器等等,这些东西放进1ms的do_sth()里面肯定是不行的(其实也行,但不方便管理来着)——怎么办呢?


——好办,放进10ms 或50ms 或100ms 的do_sth() 里就OK了。

——但是我们只有1个定时器啊,怎么弄几个时基呢?

——1个定时器就够了,其他的时基都基于那个1ms的时基来就好。我们已经有了flag1ms,当然可以弄几个flag10ms乃至flag1000ms,只要你需要,就能设置。

如下图,我们通过参数控制参数的形式,将flag1ms扩展为多个时基,使得程序渐渐的趋于模块化,精准化。

这里有个要注意的地方,就是函数从宏观上来看,确实是“每隔1ms就do_sth();每隔10ms就do_sth1();每隔100ms就do_sth2()"。


但是,从微观上看,单片机是没法在同一时刻做2件事情的!所以,每到10ms的时候,单片机会”先把1ms的事情做完再做10ms的事“;每到100ms的时候,单片机会”先把1ms的事情做完再做10ms的事,再做100ms的事“。


现在,我们的系统已经趋于成型了,不过要注意一点,这些做事情的子函数里万万不得有delay函数了(放几个nop倒是无伤大雅),好不容易划分好了时钟基线,你在1ms的时基里塞上几个delay2ms是要闹怎样。

还有一个问题,那就是子函数不要拖泥带水,写得越简洁越好,赶紧把事情做完,把控制权还给单片机,由它来决定接下来要做什么。


-  函数应该放在哪个时基里?  -

这也是关键的地方,这要求我们要对自己的程序要有清楚的把握。以及一定的产品思维。

总的原则还是没变:所有的函数都要写得简洁干净,不要有任何模块的delay()加起来超过200us!

一般来说,按键按一次不会超过50ms,放到flag10ms或者flag20ms的时间段里是没什么关系的(注意,这时候函数里的3000表示的就不是3s了,而是30s或者60s!这是遵循乘法原则的!);

LCD显示之类的,控制某个LED之类的,100ms一次就行了,人眼视觉暂留之类的看不出问题的。


一般:检测,通信这类的子程序都能放到flag10ms或者flag20ms里。输出,显示这类的放flag100ms就OK了。


flag1ms里面应该放什么呢?我的意见是尽量什么也别放,空着都OK,因为其实没什么东西需要刷新得这么快的。


如果有特别需要关照的部分,比如说步进电机的驱动啥的,请放到另一个定时器中断里(单片机有俩定时器的,不用白不用),按你需要的来设置。


定时器的中断触发时间建议不要低于0.5ms,不然进中断就太频繁了,谁也不会希望自己正看着小电影的时候,爸妈过来拍你的房门吧?


—— 2019年6月10日更新 ——

Part2.将代码模块化,降低耦合度

- 降低代码间的耦合度 -

通俗点说,耦合度就是表示两个东西“你中有我,我中有你”的程度,代码间的耦合度越高,你修改起来就越费劲,有时候看到代码黏糊糊的像一大坨蜜糖,难免会让人不知从何处下口。


还是用个例子来说明一下吧——《电风扇系统设计1》

假设我们要写个电风扇的控制程序 —— 按键方面:风扇有开关键,强风弱风切换,摆头控制3个按键(注意,这里的按键是那种大开大合的开关按键,不是那种轻触按键); 显示方面:每个按键旁边各有1个小led灯,共3个灯; 输出方面:有个控制风扇摆头的负载,有个控制风扇转不转的负载,有个控制风扇转得快或者慢的负载。


或许有人三下两下就画了个这样的流程图(一般风扇没开的时候是不能摆头的,这里就先不管了):

然后就随手写出了这样的代码:

老实说,这没啥问题,因为我们的题目要求是那么的简单,简单的要求那就简单的实现吧。虽然这里的按键、显示、负载的程序代码像蜜糖一样黏糊糊的,但是代码也才十几行,谁会在意呢?也大致画一下如上代码的结构吧:

但是,别忘了2点!①是客户的需求随时都会改动;②是这么简单的项目还用不到你做。所以,我们稍微,加点难度上来看看?


《电风扇系统设计2》

在《风扇1》的基础上,①风扇没开时不能摆头;②风扇没开时,为了与没电作区分,要让3个LED灯同时以1s为周期闪烁;③开风扇后若是5小时内按键都没有变化过,则自动关掉风扇(避免久开,当然此后LED也要闪烁),若想重开风扇必须把“风扇开关按键”先关掉再打开才行。


说句实话,先前的那份代码也到此为止了,如果有人还想在原来的代码上改出满足现在的需求的代码,我只能祝他好运。虽然也不是不能改,但是若是想用你那delay500ms来满足我的闪烁要求;或者硬是嵌入一些绕来绕去的东西在里面,那肯定没有从头再来要省事。


那么——现在该怎么做呢?

首先,把粘连的模块分开,按键归按键,显示归显示,输出归输出。就像这样子:

这样的话,大家各做各的事情,互不干扰,需要共享的信息则通过关键参数来传递。这里我们设置了3个全局变量:key_on_flag, key_strong_flag, key_sheak_flag; 在keyPress()函数里可以修改这3个变量的值,然后在display()和output()函数里查询这3个变量的值来控制显示和输出。这时候的代码结构就是这种样子的了——各个模块分开,用关键参数传递信息。

所谓的“模块化,降低耦合度”大致就是这种感觉,现在你需要在哪里改动就单独改动哪个部分的,不至于瞻前顾后、束手束脚了。


还没完呢,第二步,把我们的“时基”弄进来(不然上一堂课就白讲了),如图所示:

如此一来,关于那些指定时间的、指定频率的要求,我们也能轻松的应对了。比如这时候的display()函数,可以这样写(这个其实还有缺陷,不过先凑合一下)——

相信通过这样一个例子,大家应该能看到模块化对你的软件系统有多大的改善了。模块间的耦合度降低之后,自有一种九阳神功中的“他强由他强,清风拂山岗。它弱由他弱,明月照大江。”的感觉。你那边爱怎么变就怎么变,我这边有必要就改动,没必要就不改动,将彼此间的影响减到最低。


时间有限,模块化的解说先到这里,后续有时间会将状态机等知识尽可能的讲解给大家。同时还得把这个需求变更后的《电风扇系统设计2》的软件给写完。


——2019年6月11日 ,今天没空更贴,暂时先优化一下排版——

——2019年6月14日,修复了教程中的keyPress()和display()函数中的两个静态变量忘了加static关键字的bug——

——2019年6月16日,更新,状态机上篇 ——

part3.使用状态机,帮助你管理系统状态(上篇)

(本章较为复杂,需要读者有较好的数电和C语言功底,方能融汇贯通)

- 状态机入门(有基础的可以跳过本小节) -

直白的说,状态机就是若干个“当前状态 + 触发条件 = 新状态( + 附加动作)”的公式。


状态机可以画成图的形式(《状态迁移图》),也可以做成表格的形式(《状态分析表》),如果大家还有数字电子技术的功底的话,应该对下图还不至于太陌生。

↑此为某状态迁移图举例(与下表同义)

↑此为某状态分析表举例(与上图同义)

如果上述图表使用公式来表达的话那就是4个公式(取决于图的箭头数,或者表的跳转数),用公式的话,公式的数量不但多,看起来也很麻烦,所以我们一般用《图》或者《表》来描述你的状态机。


注意,一般的状态机是包含“动作”的,这里为了教学方便,略过了“动作”(后面会加回来的)。


状态机的使用有助于我们更直接,更便捷的管理我们的系统工作。尤其是在系统比较复杂的时候。


- 用程序来表现状态机 -

状态用枚举量为佳,因为我们应当保证系统的状态处于可控范围内,枚举量是我们工程师自定义的一个量,可以使系统处处受我们控制。

触发条件用bit变量即可,触发了就是1,没触发就是0;当然char之类也可以,但没必要。

有基础的同学可以使用"位结构体"来优化这些触发条件的内存,这里不提,请自行百度。

ok,那现在可以先定义我们的状态机变量了(以上述图表为例)。


typedef enum{

    STATE1,

    STATE2,

    STATE3

}ENUM_STATE;     //定义ENUM_STATE枚举类型,表状态


ENUM_STATE system_state = STATE1;    //定义上述枚举类型的枚举变量system_state, 初始化为STATE1

bit test_flag_a,  test_flag_b, test_flag_c;   //定义3个触发条件的bit变量。


那么,状态机的程序要怎么写呢?其实我们观察《状态分析表》的时候,有人会喜欢根据当前状态,分析触发条件,来决定下一刻的状态;有人会喜欢从触发条件开始,看看现在的状态是否受这种触发条件影响,而进入新的状态。

——好吧,有两种观察方法,就有2种写法,读者可以自由选择自己喜欢的写法。

写法1(根据当前状态,看触发条件是否有效):

void systemStateCtrl(){

    switch(system_state){

        case STATE1:

            if(test_flag_b) system_state = STATE2;

        break;

        case STATE2:

            if(test_flag_a) system_state = STATE1;

            else if(test_flag_c) system_state = STATE3;//条件a比条件c优先

        break;

        case STATE3:

            if(test_flag_a) system_state = STATE1;

        break;

        default:

        break;

    }

}



写法2(根据触发条件,看当前状态是否需要改变):

void systemStateCtrl(){

    if(test_flag_a){

        if(system_state==STATE2 || system_state==STATE3)

            system_state = STATE1;

    }

    else if(test_flag_b){

        if(system_state==STATE1)

            system_state = STATE2;

    }

    else if(test_flag_c){

        if(system_state==STATE2)

            system_state = STATE3;

    }

    else{

        ;

    }

}


这2种写法各有特点,并没有优劣之分,各位可以自取所需。非要比较的话,我个人认为:第一种写法"好读一些",第二种写法"好写一些"。


- 根据状态的值,控制系统的工作流 -

“系统的工作”很好理解,就是系统现在在干什么的意思,那么“系统的工作流”是什么意思?


——系统的工作流,表示系统在某段时间内的工作流程。

——为什么要普及“工作流”这个东西呢?

——因为,很多情况下,某个状态的工作不是写死的,而是可变的。比如:冰箱在制冷时,制冷器并不是一直开着的,而是一段时间开,一段时间关;洗衣机在洗衣服时,滚筒不是一直开着的,而是先等注水完成之后,正着转一段时间,然后反着转一段时间,然后又正着转……就是有一系列的工作步骤,这些工作的步骤其实就是我们所说的工作流。


如果你非要把洗衣机在洗衣服时,滚筒正着转和反着转分成2种新的状态的话……一路走好。


总之,为了给状态下的动作有一定的预留空间(因为天知道,需求会不会发生变化),我们需要给每个状态都做一套关于此状态下的动作的设计。


这个程序写起来也很简单,嵌入位置上,直接塞进状态机的屁股后面就行。如下:

/* 状态机程序 */

void systemStateCtrl(){

    //你的状态机程序

    systemStateWork();//把状态工作程序放这里

}

/* 状态工作程序 */

void systemStateWork(){ //设计你各个状态下的工作

    switch(system_state){

        case STATE1:

            do_sth1();break;

        case STATE2:

            do_sth2();break;

        case STATE3:

            do_sth3();break;

        default:

            break;

    }

}



- 例程:《电风扇系统设计2》的状态机初版 -

状态机真的是一个很庞大的知识点啊,好不容易把理论说完了,接下来诸位看看我的实例吧。


这个系统设计的需求我就不再重复了,各位往回看一看就能找到,关键在于,需求③我们还未处理【③开风扇后若是5小时内按键都没有变化过,则自动关掉风扇(避免久开,当然此后LED也要闪烁),若想重开风扇必须把“风扇开关按键”先关掉再打开才行。】。

那么开始吧,我们在一开始,会将系统的状态分成“风扇开”和“风扇关”两种,直接由风扇的开机键控制切换。但是,多了新的需求之后,开机键就不好使了——因为有种“风扇关”的状态,这时候的开机键也是按下的!经过一番思索,为了和真正的“风扇关”作区别,我们可以再创造一种新的状态——“停机”!也就是开机太久了,需要停机休息。停机时候的摇头键,强弱风键都无效,只有开机键松开,才能让你退出“停机”,进入"关机"。


根据我们的分析,可以画出系统的状态迁移图:

同样的,状态分析表。

相关程序如下

typedef enum{

    STATE_OFF,

    STATE_ON,

    STATE_STOP

}ENUM_STATE;     //定义ENUM_STATE枚举类型

ENUM_STATE system_state = STATE_OFF; //定义枚举变量system_state, 初始化为STATE_OFF

bit key_on_flag, key_off_flag, work_too_long_flag; //定义3个触发条件的bit变量(其实用2个就行)

void systemStateCtrl(){

    if(key_on_flag){

        if(system_state==STATE_ON || system_state==STATE_STOP)

            system_state = STATE_OFF;

    }

    else if(key_off_flag){

        if(system_state==STATE_OFF)

            system_state = STATE_ON;

    }

    else if(work_too_long_flag){

        if(system_state==STATE_ON)

            system_state = STATE_STOP;

    }

    else{

        ;

    }

    systemStateWork();//把状态工作程序放这里

}

void systemStateWork(){ //设计你各个状态下的工作

    switch( system_state ){

        case STATE_OFF:

            do_sth1();    //关机时的工作

        break;

        case STATE_ON:

            do_sth2();    //开机时的工作

        break;

        case STATE_STOP:

            do_sth3();    //超时停机时的工作

推荐阅读

史海拾趣

德力康(DLK)公司的发展小趣事

作为一家有社会责任感的企业,DLK公司始终将社会责任和可持续发展作为企业发展的重要内容。公司积极参与公益事业和社会活动,为当地经济发展和社会进步做出了积极贡献。同时,DLK公司注重环保和节能工作,采用环保材料和生产工艺,减少了对环境的污染和破坏。通过履行社会责任和推动可持续发展,DLK公司赢得了社会的广泛认可和尊重。

请注意,以上故事框架仅供参考,具体的故事内容需要根据公司的实际情况和具体事件进行编写。

Cristek Interconnects Inc公司的发展小趣事

随着环保意识的日益增强,Cristek Interconnects Inc公司积极响应国家号召,将环保理念融入到企业的生产经营中。公司采用环保材料和生产工艺,减少了对环境的污染。同时,公司还加大了对环保技术的研发力度,推出了一系列环保型电子产品连接器,为行业的可持续发展做出了贡献。

这五个故事只是Cristek Interconnects Inc公司在电子行业发展中的一部分缩影,它们展现了公司在技术创新、质量管理、市场拓展、供应链优化和环保理念践行等方面的努力和成就。这些故事共同构成了Cristek Interconnects Inc公司发展的精彩篇章,也为公司的未来发展奠定了坚实的基础。

DINTEK公司的发展小趣事

面对日益复杂的供应链环境,Cristek Interconnects Inc公司进行了深入的供应链优化。公司与多家优质供应商建立了长期稳定的合作关系,确保了原材料的稳定供应和质量可靠。同时,公司还引入先进的供应链管理系统,提高了供应链的透明度和效率,为公司的快速发展提供了有力保障。

晶群科技(Gem-micro)公司的发展小趣事

随着市场竞争的加剧,Cristek Interconnects Inc公司意识到质量管理的重要性。于是,公司投入大量资源,建立了一套完善的质量管理体系,从原材料采购到生产流程控制,再到产品出厂检验,每一个环节都严格把关。这种严谨的质量管理态度,使得Cristek的产品在行业中享有良好的声誉,赢得了客户的信赖。

Advanced Detector Corp公司的发展小趣事

Advanced Detector Corp公司成立于上世纪80年代,由一群热衷于探测器技术研发的科学家和工程师创立。在创立初期,ADC便专注于开发高精度、高灵敏度的探测器技术,以满足当时日益增长的电子测量需求。公司通过持续的技术创新,逐渐在探测器领域取得了突破性的进展,并成功推出了一系列具有竞争力的产品。

ECS公司的发展小趣事

ECS公司成立于XXXX年,由一群热衷于云计算技术的工程师创立。在创立初期,公司就明确了以提供高效、弹性的云服务为目标。他们深入研究了虚拟化技术、自动化管理等关键技术,成功推出了ECS服务,为客户提供按需分配的计算资源。这一创新的服务模式迅速吸引了众多客户的关注,ECS公司开始在云服务市场崭露头角。

问答坊 | AI 解惑

给初学单片机的40个实验

给初学单片机的40个实验 网上转载仅供参考!!!!!!…

查看全部问答>

【晒电路】模拟隔离电路图

没想到第一个参加这个活动,也算是抛砖引玉一下了,希望有后来人能够找到更强悍的! 我晒得这个是一个用光作为媒介的调频传输系统。发射机把565锁相环用作电压控制振荡器,使光电隔离器的发光二极管按照与输入电压成正比的速率而闪光。光电晶体管 ...…

查看全部问答>

源于与高于让我们在“鱼和渔”之间去取舍

RS232接口总是让我们爱恨交织,N多年前有个偷电式一只PNP/NPN偷电式串口盛行于当下,其最早的知识产权ZENYIN同学估计当追溯到小齐(XIAO-QI)叔叔那里,近几年随着欲望的膨胀,ZENYIN作了改进,改进的电路如下: 有这样炫彩: 1.速率更高,实测可以 ...…

查看全部问答>

学林电子www.51c51.com

下载50mb 的开发资料包:实例,原理图,keil 正式版,下载实验板免费申请中 下载50个单片机程序实例和开发板原理图,学林电子免费开发板新年助学活动报名啦! 申请主贴地址:  http://www.51c51.com/bbs/thread-44274-1-1.html…

查看全部问答>

arm bank的问题

我在网上看到S3C2410A将系统的存储空间分成8个bank,每个bank的大小是128M字节。 每个bank都有一个nGCSx对应 nGCSx被叫做片选,片选上可以连接内存 那是不是一个256M的内存链接到上述一个片选上,因为一个片选对应的bank的大小只有128M,就会浪费 ...…

查看全部问答>

STM32F103ZET6PA0问题?

用PA0做为IO按键输入,加了一个上拉电阻。 当你按下按键时,PA0没有被拉低,依旧是高。 请用过ZET6的兄弟们,指点一下。 程序如下: void GpioInit(void) { /* Configure all unused GPIO port pins in Analog Input mode (floating in ...…

查看全部问答>

【MSP430共享】基于嵌入式实时操作系统的电子熔断丝的设计

应用嵌入式实时操作系统实现了智能式电子熔断的设计. 介绍了由 MS P 4 3 0 F I 4 9组成的电  子熔断丝系统的工作原理、 硬件电路及其在计算机实时操作系统 ~ c / o s - Ⅱ平台上应用软件的编程技巧. 实验证明, 智能电子熔断丝能够克服传统 ...…

查看全部问答>

求助 汽车整车模型

有哪位大大能帮我建个二自由度的汽车的整车模型,研究性能是横向稳定性的??…

查看全部问答>