• 如何判断一段程序是否是裸机程序?



    在嵌入式MCU领域,一般将不移植操作系统直接烧录运行的程序称为裸机程序

    一般来说,非易失性存储,时钟,图形显示,网络通讯,用户I/O设备…都需要硬件依赖。

    基于硬件基础,内存管理、文件系统、多线程调度、UDP、TCP…都需要硬件支持的基础上平台依赖(平台库支持)。

    甚至一些数学库,依据底层是否提供硬件支持,其实现效率与精度都可能天差地别。

    因此,即使是单片机裸机的程序,因为硬件和平台的不同,大多都不能直接移植。

    严格来说,C语言标准库更应该叫“推荐支持的一些函数”,至于最终支不支持,还得看平台实现。底层中没有标准,别说库函数了,就算是写进C标准的一些语法规范,在很多底层环境中也未必能够支持。如果你想写移植性尽可能强的代码,不要写一些奇技淫巧的语法,用较老但不是远古的语言标准,选用绝大多数编译器都支持的编译器拓展,尽可能少写依赖硬件或平台库(哪怕是标准库)的代码。

    当然你也可以和我一样走走极端,不依赖三方库标准库从头实现内存管理,数据结构及数学库,渲染器,脚本编译器,虚拟机…

    至少只要资源足够,大部分情况代码是可以不需修改直接移植的.(绝大部分情况不推荐这么干)

    PainterEngine 一个由C语言编写的完整开源的跨平台图形应用框架www.painterengine.com/

    img


    “比如c c++中的clock()函数,它在time.h中定义,可是我就不懂它到底能否在裸机下运行,还是必须要有操作系统呢?这个到底要怎么判断呀?”

    *什么是裸机程序?*

    裸机程序就是指嵌入式系统的软件在没有操作系统的支持下运行,有时也称谓“裸跑”。一般C51/STM32 采用keil开发工具,其本身提供了汇编语言的启动代码。有了基本设置的执行环境。那么你编译出来的就是 裸机程序 。

    *答:“c++中的clock()函数,它在time.h中定义,可是我就不懂它到底能否在裸机下运行” 这个是可以运行的,*keil IDE 已经连接生成到 XXX.hex 文件里面了。刷机即可执行代码。


    如果你所谓的裸机程序,是指能适用于MCU平台的、不运行于OS的程序。那么,你只要用到了系统资源(时钟、网络、内存、IO口等),那就是依赖于具体平台的。

    就说你具体例子:clock(),这个函数是个什么的?

    不懂的时候,先man手册查一下。但凡你能查到的函数,基本是依赖于OS的。文档里面也会提到锁遵循的标准(“CONFORMING TO”)。当然,也有一些函数如printf是C库实现的,你用裸机编程,也需要include对应平台的库。

    clock()用来取得应用程序从起到到当前,所用占用的CPU时间。很显然,涉及到CPU时间(计时功能),并非裸机程序。

    NAME
           clock - determine processor time
    
    SYNOPSIS
           #include 
    
           clock_t clock(void);
    
    DESCRIPTION
           The clock() function returns an approximation of processor time used by the program.
    
    RETURN VALUE
           The  value  returned  is  the  CPU time used so far as a clock_t; to get the number of seconds used, divide by
           CLOCKS_PER_SEC.  If the processor time used is not available or its value cannot be represented, the  function
           returns the value (clock_t) -1.
    ...
    CONFORMING TO
           POSIX.1-2001,  POSIX.1-2008, C89, C99.  XSI requires that CLOCKS_PER_SEC equals 1000000 independent of the ac‐
           tual resolution.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    纯逻辑的代码,比如算法,流程控制等,不涉及硬件资源,通常是裸机程序。


    题主问题问的不是很严谨,但是我理解题主想问的是什么。

    题主是想知道那些标准c库里的函数是否有运行依赖,那么很简单,第一步打开编译生成可执行文件对应的map文件,找到程序里你有疑惑的函数看他们是在那个lib文件内定义的,如果这个lib的源码是开源的,例如glibc,newlibc里的,那么去找到对应的源码实现查看他们的源码是否有运行时依赖,如果有syscall之类系统调用,那么就需要操作系统,如果没有,那么就不需要。

    如果题主发现你的libc库不是开源方案,那么请联系给你提供编译工具链的供应商,要求他们给出说明或授权源码给你,亦或是查找供应商的文档资料描述确认。

    最后,常见的嵌入式裸机开发开源方案一般使用newlibc库,这个库如果适配了所有桩函数后,基本上所有的库函数都可以在裸机运行。


    裸机和操作系统的区别是,裸机的应用程序是在特权模式下运行的,而基于操作系统的嵌入式系统的应用层程序是在非特权模式下运行,需要通过操作系统才能访问到一些内存和IO

    拿cortex m3处理器为例,cortex m3支持两种模式,一种是线程模式,一种是处理模式,线程模式就是我们普通程序代码执行时所在的模式,当出现异常时会进入处理模式,一开始上电时,cpu在初始化后会默认进入具有特权的线程模式,之后的线程模式将一直具有特权。

    而当加入操作系统后,在初始化操作系统时,会进行一步操作,那就是通过修改寄存器的值,将线程模式的特权关掉,只允许运行在处理模式下的操作系统具有特权。

    所以从最底层的逻辑来讲,通过查看有没有关线程模式特权的汇编可以来进行判断

    当然我们还有更简单的一些方法,就是看应用层有没有调用操作系统向上提供的api接口函数。

    这就是我对于这一题的认知与解答,希望可以帮到你。

    另祝,学习愉快~


    你说的这个叫freestanding。能不能裸机运行,主要是看这个功能是否需要与内存以外的东西交流。

    你在内存里面瞎写写,排个序,这全套下来都只涉及到内存和立即数,所以是不需要os的。一般来说,要与外界交互的都要os支持。时间是os用定时器硬件提供给你的,文件是os提供的。

    new和printf是例外。前者本质上只要在启动前记下空闲内存的位置大小就能实现;后者因为太常用了,所以裸机库一般会专门实现,或者重定向(不知道重定向这个说法是否规范,单片机喜欢这样说)。


    这里有个表。这个表只是说裸机环境下有哪些可以使用,但真的能不能使用,还得看有没有提供相关的库,有没有链接进去。像单片机就很少用new,更没见过用exception的。

    img


    没法判断一段程序代码是不是裸机程序

    对于编译好的elf程序,你可以读取elf头判断是不是裸机程序,比如用file命令或者readelf命令

    clock()是在time.h中声明,但是一般不在头文件里实现,编译器会链接标准库,标准库提供clock()的具体实现

    有些编译器会链接newlib或者mculib这样不完整的需要打桩的标准库,我们称之为裸机编译器


    作者:林小浩
    链接:https://www.zhihu.com/question/41802793/answer/2160174159
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    **写博客可能是程序员增加额外收入,体现自身价值的最简单方式了。**本人做程序员6-7年来,通过写博客和文章获得了可观的收入,同时也被一些互联网大佬“翻过牌”,认识了一些更加优秀的人。

    下面是我写的一些文章汇总。最让我自豪的是,**我写的《图解算法小册》,解析 150 道高频算法面试题目,**纯原创的内容,曾经在 github 排到过全球榜首,全网下载超过 10w 次。同时拥有一个近 10w 粉丝的公众号。

    从0到年薪50w你要学的所有编程知识点!145 赞同 · 12 评论文章img

    img

    下面说一下我这些年写博客的一个感受。

    一、初衷

    我写文章(或者说博客)的初衷很简单,就是因为我学习技术比较慢,很多东西看完了,第二天肯定忘。都说好记性不如烂笔头,所以就各种记笔记。

    在北京为了图个高性价比的房租,就各种搬家,每次搬家都会丢点东西,之前记得笔记本都丢得差不多了。还有就是,随着技术越学越多,感觉之前比较简单的笔记就没什么用了,扔掉又觉得可惜。

    为了留作纪念,就想着如何永久保存。那么博客或者云笔记就是一个非常好的方式。于是就开始了我的文章创作之路。

    创作前期,由于笔记记得非常烂,很多东西都写得非常潦草,很多自己都看不懂,总感觉这样没什么意义,于是开始不断的对内容进行整理、完善。

    慢慢的我的技术文章(博客)创作之路步入正轨。

    二、创作历程

    创作前期经常把一些文章写在CSDN、博客园之类的平台上,写一篇技术文章之后,就各种搬运。记得当时每天光复制粘贴就耗费好长时间,复制的平台有很多,比如CSDN、博客园、企鹅号、天涯社区、大鱼号、百家号等等。我的想法就是,管他有没有人看,先占个地。

    创作前期主要写技术文章,简单梳理个逻辑,然后就各种拼凑,文章也质量要求很简单,只要自己能看懂就行,不管别人能不能看懂。

    当我看见第一条鼓励的评论时,彻底的改变了我对待写文章的一个态度。也体会到“我的文章可能在影响很多人”。从此开始认真的写好每一篇文章,同时也将我的写作战场从各大平台转到了“公众号”。

    随着时间推移,我的**《图解算法小册》诞生了。**这也是我比较自豪的一个技术系列,这个系列让我获得了很多粉丝。这时我对写作的体会也慢慢加深。

    于是在创作风格上也慢慢开始转变,从写一些技术文章到写一些程序员热点以及程序员生活等。详细创作方式之后单独写一篇文章介绍一下。

    三、创作收获

    创作收获有很多,可能大部分人都比较关心,到底赚了多少钱。详细内容大家可以看这里。

    月薪 4 万人民币是一种怎样的感受?367 赞同 · 37 评论回答img

    我重点想说的是一些其他的收获。

    首先,创作是一个痛苦的过程,比较消耗人的耐性,很多人都很难坚持下来,当然,我也放弃了很多次,幸亏的我的创作初衷比较简单,能记录东西就好,反正学技术也得记录内容。就这样不抱着太大期望去做这件事,反而更容易成。

    因此,这也是最大的收获,之前做所有事都是三分热血,这件事让我清楚,我也是可以坚持一件事。

    其次,就是对个人职业的帮助,有了网络曝光度后,让更多的人看见了我,了解了我这个人。其中有很多互联网公司的大佬,甚至有很多人给我开出高薪。这是作为一个普通程序员很难遇到的。也非常感谢各位大佬的赏识。

    最后,就是认识了各种各样优秀的人,同时,也和很多人有了深入的合作,和大佬们言语之间扩展了自己的思维高度,对待个人成长和人生的态度都有了新的转变。

    当然还有就是个人的技术、职业发展都有了质的进步。

    四、建议

    最后给各位程序员同仁一个建议,要想被更多人看见,就得学会曝光自己,让更多人了解你。


    我不知道其它程序员写博客的原因,我写技术博客的动机如下。

    1 通过持续输出,让自己有个更好的工作和挣钱的状态,不至于躺平。

    2 为我出书和挣钱做准备,同时通过写博客,推广我的书和各种挣钱渠道。

    3 通过写博客,更看到更多大牛的文章和博客,从而能更好地探索其它的挣钱渠道。

    再说下我写博客的收益。

    1 通过写博客的锻炼,我出了不少技术书,我在找工作时,这些我出的技术书很好地帮到了我。

    2 让我能不断找到了提升流量的方法,从而能更好地给我带来收益。

    3 同时给我带来了直接的现金的收益。

    接下来我就详细展开写下我写技术博客的历程。

    我是17年开始在博客园里写博客的,如下的几篇博客,或者是点击量众多,或者是被博客园选做编辑推荐,放在博客园网站的顶端位置。

    如何在面试中介绍自己的项目经验 - hsm_computer - 博客园www.cnblogs.com/JavaArchitect/p/7586949.html

    进大厂也就这回事,工作后2到3年进大厂操作指南 - hsm_computer - 博客园www.cnblogs.com/JavaArchitect/p/12665483.html

    借着谈转正感想的时机,再聊聊外企和互联网公司的工作体验 - hsm_computer - 博客园www.cnblogs.com/JavaArchitect/p/11732544.html

    用象棋的思维趣说IT人的职业发展和钱途 - hsm_computer - 博客园www.cnblogs.com/JavaArchitect/p/11065287.html

    从国际象棋与象棋的走法差异,再趣说IT人提升能力和增收方式 - hsm_computer - 博客园www.cnblogs.com/JavaArchitect/p/11246162.html

    当时写这篇文章的时候,不像现在一样为了推广和为了钱,所以感觉写的时候有种“灵气加持”的神奇感受,写到关键处,感觉脑海里自然就出现了后面该出现的最好的用辞。

    那个时候,有很多公众号的运营者要求来转载我的文章,有时一篇文章出来,来的晚的人,还无法转载成。说是转载,但不少人其实是以“原创”的方式放在他们的公众号里的,顶多最后用段文字来给明出处,到现在我自己公众号要用我自己的文章还不成。

    其实那些肯标明转载的已经算比较良心了,更多的是就直接抄袭。但是我还是得说句比较矫情的话,我写这些文章时产生的灵气加身的感受,其实是我最大的收获,我后面的职业发展,以及到出版社去出书,还真靠了这些灵气和灵感。

    后面我就出书了,也开始出书和做公众号了。也到知乎来写文章了。在知乎里,我输出的博文给我带来的流量更为客观,现在一天能给我带来2万多的流量,最近比较爆款的是如下两篇。

    HR如何筛掉千锋、达内、黑马出来的学员?2190 赞同 · 442 评论回答

    计算机专业曾经有哪些方向非常火后来却凉了?884 赞同 · 238 评论回答img

    其实再说句矫情的话,写文章,出书和做公众号,我目前靠这得到的收益并不多,但通过干这个,让我找到了能模仿和学习的对象。

    比如我在刚写博客时,认为点击量过万就不错了,通过写博客过程中的找资料,我顺带揣摩出了写高流量文章的技巧。再如,本来我认为干兼职就是出视频和出书,但现在我通过实践,也积累了不少变现的技巧,与之对应的,我公众号的广告收益,以及在知乎上的付费咨询收益也在与日俱增,同时我更打算开辟在线视频等营收渠道。

    所以说,我非常支持程序员持续写技术博客,出文章,出书犹在其次,靠出文章出书在面试中更好地证明自己,也只是附带的收益。通过持续写博客,一方面能让自己处在积极向上的状态,而不会在空闲时经常去消遣,而且通过写文章写多了,灵气和灵感自然也就来了。

    更为重要的时,程序员通过持续写博客,更能很好地开拓自己的眼界,比如能知道该怎么进大厂,当前有什么比较值钱的技术以及怎么提升,以及知道当前自己做努力的领域有哪些大牛,同时能模仿这些大牛的操作来进一步提升自己。

    当然,当程序员积累到一定程度,或许可以通过其他更高效的方式提升自己和挣钱,但在积累阶段,持续写技术博客,确实是一个高效提升自己的方法。


    作者:啊距离具体
    链接:https://www.zhihu.com/question/438340661/answer/2618494025
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    谢邀,路过顺便说几句。

    一、程序必需元素分析

    首先,所有的程序必然是周期性运行的死循环,哪怕中断程序。中断程序可以看成以单片机时钟频率或者外设时钟频率不停查询标志位,然后根据标志位状态运行的特殊程序。

    **第二,**不同功能的程序运行周期不一致,但都可以归纳为几个常用的周期,例如100us,1ms.10ms.100ms和1s,没有必要自己跟自己过不去,新增一些奇怪的周期。

    **第三,**不同功能程序对周期的准确性不一样,例如蜂鸣器io口驱动,对周期精度要求很高,如果翻转频率不一样会导致音色变得奇怪。电机驱动的ad中断,显示扫描的驱动都属于这类。这类一般靠中断和外设硬件完成。另外一类是对周期精度不敏感的,例如按键响应,误差个几十毫秒也没啥问题。

    **第四,**程序是用来解决问题的,解决问题的基本方法就是将大化小,所以一个程序由不同功能程序协同完成,为了协同,不同子程序之间需要通讯。

    综上,我们的程序架构要反映以上的四点,(而且建议大家要积累自己的框架,完善框架,这样每次出问题了,能根据框架很快定位到问题点。)

    二、程序架构分析

    1. 对于硬周期程序,使用中断、或者外设执行,例如100us定时程序。PWM生成等。

    2)对于软周期程序,使用主循环提供的定时标志运行。

    3)每个任务都必需尽快完成自己的工作,然后结束。每个任务都使用查询的方法,查询到自己需要的结果就运行,查询不到就跳过。(对于阻塞,等上了RTOS或者使用PT_THREAD宏实现,有兴趣的话,可以新增这个话题)

    4)部分任务需要硬周期与软周期配合完成任务,例如,串口通讯,串口先将数据缓存下来,再由软周期任务进行处理,等会再举例。

    5)任务间的通讯,通常使用生产者消费者模式(每一个任务不是用来产生消息,就是用来处理消息,所以任务可以根据消息来进行划分),并且保证消费者消费速度快于生产速度,减少使用队列缓存等技术。例如按键扫描与按键处理程序,按键每10ms检测一次按键,有按键按下后产生按键标志。按键处理程序也10ms运行一次,确定按键标志,因此任务之间的通讯使用全局变量进行。

    综上,基本程序框架如下,每一个程序必然包含如下的代码。

    #include "global.h" /*global.h文件包含所有模块的子头文件*/
    static uint8_t timeFlag100us,timeFlag1ms,timeFlag10ms,timeFlag100ms,timeFlag1s
    /*100us中断程序*/
    void  isr_100usr() {
         static uint8_t timeCnt1ms,timeCnt10ms,uint16_t timeCnt100ms;
         timeCnt1ms++;timeCnt10ms++;timeCnt10ms++;
         timeFlag100us = ~0;
         if(timeCnt1ms>=10)  {timeCnt1ms   = 0; timeFlag1ms   = ~0;}
         if(timeCnt10ms>=100) {timeCnt10ms  = 0; timeFlag10ms  = ~0;}
         if(timeCnt100ms>=1000){timeCnt100ms = 0; timeFlag100ms = ~0;}
    }
    /*需要用到1S的通常都是需要时间较准确的场合,所以使用单独时钟中断          */
    /*若不需要太准确,可以放在100us_isr上,很多程序也用不到1S的周期,所以很少用*/
    void isr_1s(){
        timeFlag1s = ~0;
    }
    
    void main(void){
         clk_init();/*系统时间初始化*/
         hw1_init(); /*一般硬件初始化,一般每个硬件的初始化都单独一个初始化函数,*/
         hw2_init(); /*  那么我们就可以通过Main程序来进行索引查找我们的程序*/
         /* hw...._init*/
         sw1_init();/*软件初始化,跟上面一样,每个软件模块单独一个初始化函数*/
         sw2_init();/*软件初始化,跟上面一样,每个软件模块单独一个初始化函数*/
         /* sw....._init  */ 
         while(1){
              if(timeFlag100us == 0)continue; timeFlag100us = 0;
              sw100us();/*100us软周期程序*/
              if(timeFlag1ms == 0)  continue; timeFlag1ms   = 0;
              sw1ms();/*1ms软周期程序*/
              if(timeFlag10ms == 0) continue; timeFlag10ms = 0;
              sw10ms();/*10ms软周期程序*/
              if(timeFlag100ms == 0)continue; timeFlag100ms = 0;
              sw100ms();/*100ms软周期程序*/
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    三、基本程序结构-普通程序

    二上面说的是整体的软件框架,现在说的是每个子程序框架是怎么样,这小节说的是普通程序。刚才说到每个程序应该使用查询而不是死等的方法进行。现在以按键程序为例。

    #include "global.h" /*global.h文件包含所有模块的子头文件*/
    
    /*按键扫描程序,在主循环每10ms执行一次*/
    static uint16_t keyBuf[KEY_MAX];
    uint8_t key_cmd;
    void KeyIint()
    {
         key_cmd = KEY_CMD_NULL;
    }
    void  keyRun(void){
         if(IO_KEY1 == 1){
                 if(keyBuf[0] < 300)keyBuf[0]++;
                 if(keyBuf[0] == 5)key_cmd = KEY_0_DOWN;     /*有按键按下并且进行5次滤波*/
                 if(keyBuf[0] == 100)key_cmd = KEY_0_1S_LNG;/*1S长按*/  
          }
          else{
                if(keyBuf[0] >=5)) key_cmd = KEY_0_SHORT; /*有部分程序是按键按下时处理,有部分是松开时处理*/
                keyBuf[0] = 0;
          }
          if(IO_KEY2 == 1){
               /* just like up*/
           }else{
              /*........*/
           }
          if(IO_KEY3 == 1){
               /* just like up*/
           }else{
              /*........*/
           }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    然后到key.h文件,该问题将生产内容提供给消费者,至于谁是消费者,key不关心。

    /*  key.h文件 ,该文件包含在global.h上 */
    #ifndef __KEY_H__
    #define __KEY_H__
    /*需要的头文件*/
    void KeyIint(); /*在main文件里初始化调用*/
    void  KeyRun(void);/*这样main函数才能调用*/
    extern uint8_t key_cmd;
    enum{
        KEY_CMD_NULL = 0,
        KEY_0_DOWN     ,
        KEY_0_1S_LNG,
        KEY_0_SHORT,
       /* ........others */
    }
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有生产者必然有消费者,按键扫描负责产生按键消息,那么必然有一个按键处理程序进行处理

    #include "global.h"
    
    void KeyProcInit()  /*按键处理初始化函数*/
    {
       /*do something*/
    }
    
    void KeyProcRun(void){/*按键处理主程序,负责消费key_cmd,主程序每10ms运行一次*/
        if(key_cmd == KEY_0_DOWN){
             buzz_on_cmd  = BUZZ_SHORT; /*蜂鸣器短响一下,由峰鸣器程序提供*/
         }
          if(key_cmd == KEY_0_1S_ONG){
             buzz_on_cmd  = BUZZ_LONG; /*蜂鸣器长响一下,由峰鸣器程序提供*/
         }
         key_cmd = KEY_CMD_NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    keyproc.h的文件就不写了,麻烦。。。。。

    三、基本程序结构-软、硬周期程序配合

    刚才的蜂鸣器程序就是最好的软、硬件周期配合的例子,因为蜂鸣器IO口翻时间需要比较精准,所以需要使用硬件定时器完成,但对于命令的接收只需要保证足够快就行,而且也不一一定要比生产快,因为对于大部分应用来说,蜂鸣器响声不是致使的,偶然丢弃也没问题。所以为了CPU资源,有此东西可以做取舍的。当然现在只是举例而已。

    #include "gloal.h"
    
    uint8_t buzz_on_cmd ;
    static uint8_t buzz_on_cnt;
    
    /*蜂鸣器中断程序,由100us中断程序调用*/
    void BuzzIsr(){
         if(buzz_on_cnt >0){
             IO_BUZZ^=1;
         }
    }
    void Buzzinit(){
    buzz_on_cmd = 0;
    }
    
    /* 在主程序10ms周期运行*/
    void BuzzRun(){
        
        if(buzz_on_cmd == BUZZ_SHORT){
             buzz_on_cnt = 5;
         }
         if(buzz_on_cmd == BUZZ_LONG){
             buzz_on_cnt = 20;
         } 
         buzz_on_cmd  = 0;
    
         if(buzz_on_cnt >0) buzz_on_cnt--;
         else               IO_BUZZ = IO_OFF;
    
    }
    
    /*头文件的东西就不写了。。。。。。。。*/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    四、基本程序结构-软周期硬件查询程序

    在使用串口通讯的时候,我们可以使用中断,也可以使用查询的方式,但我个人一般是尽量减少中断数量,所以串口通讯波特率不高的时候会尽量用查询。这里主要是为了演示如何减少程序堵塞。

    #include "global.h"
    /*下的是提供给其他程序使用的,其他程序将_u8SendCnt,_u8SendSize_u8SendBuf与uart程序通讯*/
    uint8_t  _u8SendBuf[UART_SEND_BUF_MAX,_u8SendCnt,_u8SendSize;
    void UartInit()
    {
    
    }
    
    /*主程序100usr执行一次*/
    /*所以有时候串口发送字节会有一两百us的延时,但一般不会影响使用*/
    /*下面假设发送波特率是9600,一个字节的发送时间就是1ms左右*/
    void UartSendRun()
    {
          if(_u8SendCnt < _u8SendSize){
              if(TI == 1){/*51单片机典型的串口发送
                  TI = 0;
                  SBUF = _u8SendBuf[_u8SendCnt ++];
               }
          }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    串口发送的解释程序,从该例子可以看出,我们可以看到uartSend程序不关心谁启动了他,谁填充了数据。所以以后如果我们要增加发送协议,可以单独新增一个程序用来填充该数据。但这样的话,会导致程序间争夺uasrSend,当然可以增加同步锁来解决,具体的可以参考RTOS。但你也可以自己想想办法看怎么解决,对于你学RTOS挺有好处的。

    #include "global.h"
    
    void UartComIint(){
    
    }
    
    /*每10ms执行一次*/
    void UartComRun(){
      static uint8_t timeCnt;
      if(timeCnt++<= 5)return;/*就是程序每50ms才执行一次啦。
      if(_u8SendCnt < _u8SendSize) return;/*查询是否已经发送完*/
      timeCnt = 0;
    /*填写发送数据帧*/
     _u8SendBuf[0]= 0xxx;
    _u8SendBuf[1] = 0xxx;
     ......
      _u8SendBuf[10] = 0xxxx;
     /* 通过这些变量通知uartSend程序开始发送*/
     _u8SendCnt = 0;
     _u8SendSize = 11;
     TI= 1;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    五、基本程序结构-状态机

    状态机说难不难,说复杂不复杂,也不在这里写了,有空会新增这个话题。

    六、最终程序结构

    #include "global.h" /*global.h文件包含所有模块的子头文件*/
    static uint8_t timeFlag100us,timeFlag1ms,timeFlag10ms,timeFlag100ms,timeFlag1s
    /*100us中断程序*/
    void  isr_100usr() {
         static uint8_t timeCnt1ms,timeCnt10ms,uint16_t timeCnt100ms;
         BuzzIsr();/*优先执行,尽量减少IO抖动*/
         timeCnt1ms++;timeCnt10ms++;timeCnt10ms++;
         timeFlag100us = ~0;
         if(timeCnt1ms>=10)  {timeCnt1ms   = 0; timeFlag1ms   = ~0;}
         if(timeCnt10ms>=100) {timeCnt10ms  = 0; timeFlag10ms  = ~0;}
         if(timeCnt100ms>=1000){timeCnt100ms = 0; timeFlag100ms = ~0;}
    }
    /*需要用到1S的通常都是需要时间较准确的场合,所以使用单独时钟中断          */
    /*若不需要太准确,可以放在100us_isr上,很多程序也用不到1S的周期,所以很少用*/
    void isr_1s(){
        timeFlag1s = ~0;
    }
    
    void main(void){
         clk_init();/*系统时间初始化*/
         hw1_init(); /*一般硬件初始化,一般每个硬件的初始化都单独一个初始化函数,*/
         hw2_init(); /*  那么我们就可以通过Main程序来进行索引查找我们的程序*/
         /* hw...._init*/
         sw1_init();/*软件初始化,跟上面一样,每个软件模块单独一个初始化函数*/
         sw2_init();/*软件初始化,跟上面一样,每个软件模块单独一个初始化函数*/
    KeyIint();
    KeyProcInit();
    Buzzinit();
     UartInit();
         /* sw....._init  */ 
         while(1){
              if(timeFlag100us == 0)continue; timeFlag100us = 0;
              sw100us();/*100us软周期程序*/
                UartSendRun();
              if(timeFlag1ms == 0)  continue; timeFlag1ms   = 0;
              sw1ms();/*1ms软周期程序*/
              if(timeFlag10ms == 0) continue; timeFlag10ms = 0;
              sw10ms();/*10ms软周期程序*/
             KeyProcRun();
             BuzzRun();
            UartComRun();
          keyRun();
              if(timeFlag100ms == 0)continue; timeFlag100ms = 0;
              sw100ms();/*100ms软周期程序*/
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    我个人比较喜欢10ms程序,所以一般都放在10ms运行。

    七、程序运行时间分析

    可以看到每个时间,程序执行数量都不一样,那么必然会导致每个时间标签出现抖动。对于我们来说,最重要的是抖动是否在接受范围内,不能接受那么该放在中断执行。而且每个程序都使用的是查询的或者状态机的方法,执行时间都很短。实际使用的时候,需要放个IO口在100us的程序时进行翻转,用示波器观测IO频率,评估抖动是否能接受,不能就让任务进行划分,或者选一个更好的单片机。

    八、文件结构

    建议每个功能模块使用一个文件,方便定位源代码和进行单元测试。

    九、总结

    哈,很多人会问,为什么100us的查询不直接在while1那里一直查询?因为在100us查询的话,我们可以相对准确的设定我们的查询时间和次数。

    第二、程序想到哪写到哪,每个程序都能单独的进行运行和测试 。如果能使用指针函数,全局变量都不需要使用,要定位问题时,可以先一些程序注释掉,先不运行,也妨碍其他程序运行。

    第三、为以后使用RTOS打下基础。开始学会怎么划分任务,任务怎么通讯。以后用上了RTOS后,每个程序变成RTOS的任务就行了。对于学习RTOS我认为有好处。而且随着这个框架使用多了,使能发现它的不足,开始懂得为啥需要RTOS了。

    第四、因为是随手而写(手机码字,头疼),篇幅也有限,很多知识点也没写上,例如不使用RTOS怎么写堵塞程序(就是刚才说的PT-thread),全局变量怎么管理,状态机,生产者消费者模型等。不足之处请原谅哈。

    第五、单片机编程思路应该能拓展到计算机上,计算机也只是一个大循环不停的套小循环。


    看了下别的回答,推荐的《时间触发嵌入式系统设计》这本书我看过,值得一看,我现在的程序基本上受这个影响出来的。

    程序上把应用层和底层分开,底层调用顶层可以用函数回调,比如说按键的处理底层扫描按键和顶层处理具体按键消息要分开。应用层把不同的状态逻辑分开,比如说待机和运行做的事情肯定是不一样的。

    我现在的程序应用层是事件驱动的,底层是模块化的,接口是统一的格式。一定要抽象出这个模块的特性,比如说蜂鸣器有源无源模块给出去的接口是一致的,底层改有源无源不要影响顶层使用蜂鸣器驱动。又比如说按键抽象出来统一的事件和键值,底层按键是扫描的还是独立的不要暴露给顶层去处理。

    粗颗粒度的时间用一个统一的时间切片管理,细颗粒度的延时在不影响别的任务执行时用delay也没关系。一些模块比如红外需要用IO中断+定时器的直接在模块内部处理,中断任务短小是不影响主循环任务执行的。

    任务以状态来驱动,每次进任务处理完就退出去,不要死等下一个状态。比如说按键处理的过程可以大概分为空闲、消抖、长按判断、重复长按判断,这些状态中间时间间隔很长,但处理起来其实很简单,用状态来解决掉状态中间的延时,让出CPU给别的任务。


    看了很多回答,我感觉大部分没有介绍的一点是:

    对于一个需求明确的项目,如果你使用裸机实现比较困难的话,那么往往使用各种框架或RTOS来实现,也是依然困难的。


    无论是裸机还是现成框架或者RTOS,哪种方法来实现你的项目,你都要对项目进行

    1.功能划分

    2.各个功能之间如果有联动,就会有信号传递;

    3.各种耦合的解耦;

    4.资源的占用和释放(比如串口收发);

    5.输入输出的合理采集、控制;

    6.各个功能模块之间的“调度”(借用RTOS的概念来总结。也就是意味着哪怕是裸机也会有简单的调度)

    仓促这么总结,一般项目大体如是,有遗漏也有可能,可以留言提醒。


    当这些事情做好,你会发现,无论是裸机还是RTOS,都要遵循你的这些顶层设计。

    其中裸机就需要自己来做调度,自己做资源管理,自己做信号量的收发控制,特点是灵活完全可控,代价是容易自己搞出各种坑;

    而RTOS提供给性能优秀的调度、资源管理、信号收发机制,代价就是你必须按照RTOS的规范来使用他们,如果使用方式不合理,也会出各种坑。

  • 相关阅读:
    传统虚拟机和容器的对比
    javax.net.ssl.SSLException: Connection reset
    【Vue面试题十七】、你知道vue中key的原理吗?说说你对它的理解
    基于虚拟同步发电机的光伏混合储能并网系统Simulink仿真
    使用springboot实现jsonp|jsonp的实现|JSONP的实现使用springboot
    Sentinel: 分布式系统的流量防卫兵
    fastAdmin表格列表的功能
    git第一次推送gitlab项目
    Java接口和抽象类的区别
    单链表的插入删除
  • 原文地址:https://blog.csdn.net/qq_41854911/article/details/127740000