[讨论] 一起讨论:如何做一个单片机程序通用模版

辛昕   2012-6-4 00:24 楼主
随着一年多的真实产品开发——虽然,嘿嘿,按照他们的说法,我这实在有点非主流,但在后期的调试中吃了不少苦头,于是在蛋疼的调试中,以及在阅读诸如《代码大全》这些书籍时,渐渐地萌生了一种如何做一个方便调试,方便扩展,方便移植的单片机程序模板。
而我,目前是渐渐地把它用在我自己的一个个人项目,一个用stm8的项目上,实际上,对这个模板的认识,最初只是有一种模糊的看法,渐渐的渐渐的,理论越来越清晰,但是,还需要在实际操作中得到完善和补充——比如之前对于io口的抽象,我一直简单的以为一个宏定义就可以解决,但如果不是到前天晚上 sjl 的那个问题,我也没有想到这个问题没有我想的那么简单,虽然后来我还是很快想到了办法解决。

所以,这只是一个开放讨论的帖子,把我的一些想法,以及我已经在做的部分代码贴出来,希望大家一起讨论讨论,给点建议,或者评论评论,只希望,我,还有大家,能够在共同讨论中,找到一种也许更好的方式去写我们的单片机程序。
(这里插一句题外话:事实上,我从来没有特别把单片机编程和一般意义上的编程分离开来,在我看来他们是一回事,并且也应该尝试使用一般意义上编程的一些成熟经典,方法,理论,来改善,来构建高质量的代码。)

--------------------------------------------------------------------------------------------------------------------------------------------------
首先解释一下,这个所谓的 单片机程序通用模板,我对它的定义,目前可以用以下特征来界定:
1.它是通过恰当的分层,使其应用层和硬件底层抽离开来,这样做的目的,主要是 实现 不同MCU可以用最少的改动来实现移植,并保持良好兼容性。
2.它是容易增删模块的,按照我个人的看法,单片机的功能模块,自然是以外设为单位来划分比较合适。
   ——注意,在这里,IO口也算是一种外设,当然,利用IO口去点亮LED,数码管或者读按键,这个和IO口的设置初始化是两回事。因为IO口设置和初始化可以为很多事情服务,点亮LED和读键只是其中一个功能。
--------------------------这个界定还不算很完整,但它的确是我目前主要考虑的----------------------------------

对于这个模板,我最先考虑的是利用分层和抽象的思想,实现第一个目标。
现在先来说说这个基本思路,纸上谈谈兵先~~

1.任何一个单片机程序都会包含两大层,一个是硬件底层的操作,比如操作相应寄存器,去打开某些外设,通过外设获取外部信息,或者影响外部。
另一个则是 上层应用层,比如说,我要对通过单总线读来的温度值进行存储,显示。

这两个层是单片机程序的最顶层 和 最底层。
最顶层,往往只是一个 main函数,当然还包括其他中断子函数,它们就像一个一个独立的线程,和main是可能并行处理的。

最底层,对于玩51的时候,可能没有这个概念,因为我们都是直接操作寄存器的。但是如果接触过其他单片机,特别是st系列的,了解过它那个固件库的必然就会知道我说的正是这个东西。

stm8/32都有一个架构上几乎一模一样的固件库——我想,很多人也许和我一样,最初的编程规范习惯,以及之所以会产生这种分层思想,就是因为受到这个固件库的启发。
这个最底层的目的是,使我们不需要直接面对寄存器,而是以外设为单元,方便使用。

假如有一天,我们需要在51上或者其他MCU上也使用这个思想,我们也可以参照这种分类,来自行写一个属于我们的给51的,给430的固件库。
对于我们熟悉的51,我相信这实在是一个简单不过的事情。

在最底层 和 最高层之间,按照我的理解,还需要做两个层。

我们叫做第二层 和 第三层,最高层为第一层,最底层为第四层。

第二层就是我们的功能模块能,它都包含一些什么东西呢?
它包含我们需要操作的外设,比如说 ds18b20啊,比如说数码管显示啊,比如说读键啊.....等等,因为,我们把所有这些都定义为 外设。

如果对比st的固件库,也许你会认为我多此一举,不,是多此两举。
第一,为什么要把外设的操作脱离寄存器那个层次的硬件底层?
第二,就算是把外设单独出去吧,那为什么还有一层呢?不可以直接在 第四层基础上,建立一个外设层么——就好像,把st现有的固件库里,把那些很基本的gpio啊,exti啊等这些辅助性的更一般的,通用的部分 和 什么 iic 啊 spi啊之类的外设分离就好了。

之所以这么做,我的考虑在于:
1.我这个模板并不只是给stm8或者stm32使用,我是希望它可以与MCU无关;
   所以,在st提供的固件库,可以操作gpio啊iic等固件库的基础上,我希望增加一个 硬件抽象层,这一层,它可以通过一个宏选择到对应具体不同MCU的那个第四层硬件底层API函数。
   举个简单的例子,还是以st固件库为例。

    对于gpio的操作,通常由 设置方向 和 读写两个方向。
    这些操作st的固件库里都有,但是,现在,为了兼容不同的MCU,我要在第三层这个抽离层,再次封装一个gpio的 设置方向函数,读函数,写函数,而它将会通过一个宏,来选择st固件库里相应的操作函数。
以我写的一段代码为例:

  1. #define STM8S
    /*这个宏是用于选择stm8s,这个取决于你的这个程序将使用在哪一个MCU上*/

    #include "gpio.h"

    /*
    gpio.h中的内容

    /*
    For distinguish different MCU.
    1.Here will be used a Macro to choose different GPIO hardware
    headfile.
    */


    #ifndef _GPIO_
    #define _GPIO_

    #ifdef STM8S
    #include "stm8s_gpio.h"
    #include "datastruct.h"


    /* Typedef Data-struct for compation */

    typedef GPIO_TypeDef GPIO_PORT;
    typedef GPIO_Pin_TypeDef GPIO_PIN;
    typedef GPIO_Mode_TypeDef GPIO_MODE;
    typedef BitStatus GPIO_BIT;

    /*
    I/O mode & direction

    In-Mode,concern two types:
    1.Float Input(FL)
    2.Pull-up(PU)
    Interrupt will not setting here;

    Out-Mode,no care about speed,concern two types:
    1.OD
    2.Push-Pull(PP)
    default set the IO state to LOW,and no care about speed,as STM8 MCU can be set
    different speed for GPIOs.
    */

    #define IN_FL GPIO_MODE_IN_FL_NO_IT
    #define IN_PU GPIO_MODE_IN_PU_NO_IT
    #define OUT_OD GPIO_MODE_OUT_OD_LOW_FAST
    #define OUT_PP GPIO_MODE_OUT_PP_LOW_FAST

    /*
    Basic Operation on IO,Include:
    1. Set 1,Set 0;
    2. Read in data;

    All above two,include operation on bit & port
    */



    /* Function declaration */

    void Set_GPIO_Mode(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin,GPIO_MODE mode);
    void Set_Port_Mode(GPIO_PORT* GPIOx,GPIO_MODE PortMode[8]);

    void Write_Port(GPIO_PORT* GPIOx,uint8_t PortValue);
    void Write_Pin_High(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin);
    void Write_Pin_Low(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin);

    uint8_t Read_Port(GPIO_PORT* GPIOx);
    GPIO_BIT Read_Pin(GPIO_PORT* GPIOx,GPIO_PIN GPIO_Pin);


    #endif
    #endif
    */

    void Set_GPIO_Mode(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin,GPIO_MODE mode)
    {
    GPIO_Init(GPIOx, GPIO_Pin, mode);
    }

    void Set_Port_Mode(GPIO_PORT* GPIOx,GPIO_MODE PortMode[8])
    {
    GPIO_PIN PinMask[8] = {
    GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_3,
    GPIO_PIN_4,GPIO_PIN_5,GPIO_PIN_6,GPIO_PIN_7,
    };
    U8 i;

    for(i = 0;i < 8;i++)
    Set_GPIO_Mode(GPIOx,PinMask[i],PortMode[i]);
    }

    //---------------------------------------------------------------------
    void Write_Port(GPIO_PORT* GPIOx,U8 PortValue)
    {
    GPIO_Write(GPIOx, PortValue);
    }

    void Write_Pin_High(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin)
    {
    GPIO_WriteHigh(GPIOx, GPIO_Pin);
    }

    void Write_Pin_Low(GPIO_PORT* GPIOx, GPIO_PIN GPIO_Pin)
    {
    GPIO_WriteLow(GPIOx, GPIO_Pin);
    }

    //-----------------------------------------------------------------------
    U8 Read_Port(GPIO_PORT* GPIOx)
    {
    return GPIO_ReadInputData(GPIOx);
    }

    GPIO_BIT Read_Pin(GPIO_PORT* GPIOx,GPIO_PIN GPIO_Pin)
    {
    return GPIO_ReadInputPin(GPIOx, GPIO_Pin);
    }


这是一份对应于stm8s的抽象层gpio源文件。
我们可以以此为例子,写出针对于另一个平台的抽象层gpio.c

但是,这里要遵循一个原则:
那就是,所有的这些源文件里的函数,必须是同名。
比如说,不管是51的还是430的抑或stm8,设置IO方向的,永远叫 Set_Gpio_Direction(.....)
它们唯一的区别,只是在不同的宏条件下,实际的函数实现内容是不一样的。

这种同名不同内容,正是实现MCU抽象的核心方法所在。

BTW:
当然,刚刚我突然想,反正这个第三层抽离层,针对不同MCU都不一样,那还不如直接跳过第四层,直接把固件库里对寄存器的操作直接复制过来,把第四层去掉好了。
我觉得这也是不错的并且更直接的方法,我最开始总认为要第四层,那纯粹是因为受 st提供了一个固件库的影响所致。

太长了,先发表,,接下来的内容,,回复里待续~~~

强者为尊,弱者,死无葬身之地

回复评论 (29)

2推荐 虚V界 

唉,眼睛累,看不下了……
点赞  2012-6-5 16:44

第二层 模块抽象层

补充一句。 经过了第三层的 硬件抽象层以后,我们再向上时,我们操作任何东西,都不希望见到,或者说会屏蔽掉具体的硬件信息,比如说,在第二层及其以上,我们绝对不会直接操作P0 P1,GPIOA GPIOB,它们将通过 第二层的头文件定义,从这一层开始向上屏蔽。包括第二层自身的操作函数,也将见不到这类名字。

 第二层 这一层是以模块为单位抽离的。
我们要分清楚,模块的定义是什么?
比方说,ds18b20肯定算一个模块,这个你肯定不会怀疑,那么,gpio算不算一个模块呢?这个问题也许很奇怪,也许你会说,当然算啊,你第三层不是就以gpio为例子实现的 硬件抽象 么?
这里,要区分,那个硬件抽象 只是对gpio的基本操作,但是,并不代表,所有需要gpio来完成的功能,就属于那个抽象层。
比方说,我要用gpio来显示数码管,那数码管就是一个 模块,其实说白了,ds18b20也是用一个gpio来实现单线读写的。

所以,这里一定要区分清楚。

但有一个问题比较复杂。
那就是,对于一些硬件抽象层次的 模块该如何处理。
举个例子,在51里,我们没有硬件的 iic spi等外设——这里说的是 标准8051,不包括那些增强型。
而stm8s却一个不缺。

这个时候,如果我们把它归入 第三层 硬件抽象层,那以后,遇到51,或者那个MCU并不存在硬件上的数字接口,需要软件模拟的那该怎么办呢?
很明显,不能归入第三层,因为第三层是对 硬件 的抽象,但这种数字接口本身不属于 硬件必备,它不如gpio这样必不可确。
那我们就归到第二层吧?也不行,为什么呢?
第二层本来经过第三层隔离,它已经不再需要处理和相关硬件相关的信息,如果我们现在遇到一个硬件上存在数字接口的MCU,那我们势必要为此改动这个 第二层,但在我们的原则上,它是不应该因为换MCU而更换的。

在这里,我最终会选择,把它归为 第三层,尽管,它实际上,并不是实际存在的硬件,而是软件模拟,但是软件模拟所需的硬件底层信息已经在第三层同层的其他函数里包含了。而向上,它就如同一个真实存在的硬件一般,这样,至少我可以保证,第二层是无需更改的。

这一部分,我只能给出 一个 我写的 segment源文件,它是 动态扫描数码管(不通过IO扩展方式的),至于 前面更多篇幅讨论的数字接口部分,由于我还没有做到,所以,嘿嘿,暂时给不了了,只能纸上谈兵了。
下面贴segment的代码,之前合在一起发有点乱,这次,头文件和源文件分两个发,更清楚。

  1. #ifdef STM8S

    #ifndef _SEG_
    #define _SEG_
    #include "stm8s.h"
    #include "gpio.h"
    #include "datastruct.h"

    #define MAX_SEG 11

    /**/
    #define Seg_Bit_Enable 0

    /*
    Hardware pin define
    / ./ ./ ./ ./

    bit_control seg_control
    1 a
    2 b
    3 f
    4 e
    ----------- d
    dp
    c
    g
    left-->right
    */



    /*
    connecting:
    seg_control: PB
    bit_control:
    1:PD3
    2:PD5
    3:PD7
    4:PD6
    */

    #define Seg_Bit_Port GPIOD

    #define Seg_Bit_1 GPIO_PIN_7
    #define Seg_Bit_2 GPIO_PIN_5
    #define Seg_Bit_3 GPIO_PIN_3
    #define Seg_Bit_4 GPIO_PIN_6

    #define Seg_Dig_Port GPIOB


    void Segment_IO_Initial(void);
    void Segment_Show(GPIO_PORT *SegPort,GPIO_PIN SegNum,U8 value);
    void Segment_Not_Show(GPIO_PORT *SegPort,GPIO_PIN SegNum,U8 value);
    void Segment_demo(void);

    void Segment_Bit_Enable(GPIO_PORT* Port,GPIO_PIN Pin);
    void Segment_Show_Value(GPIO_PORT* Port,U8 value);

    #endif
    #endif



  1. #define STM8S
    #include "segment.h"


    /* Segment code*/
    /*
    abcdefg dp --- bit 0~7
    0 1 2 3 4 5 6 7 8 9
    0xc0 0xf9 0xa4 0xb0 0x99 0x92 0x82 0xf8 0x80 0x90

    if showdrop.
    minus 0x80

    0x40 0x79 0x24 0x30 0x11 0x12 0x02 0x78 0x00 0x10
    */
    const U8 SegCode[MAX_SEG] = {
    0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,
    0xff
    };

    void Segment_IO_Initial(void)
    {
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_0,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_1,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_2,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_3,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_4,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_5,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_6,OUT_OD);
    Set_GPIO_Mode(Seg_Dig_Port, GPIO_PIN_7,OUT_OD);

    Set_GPIO_Mode(Seg_Bit_Port, Seg_Bit_1, OUT_OD);
    Set_GPIO_Mode(Seg_Bit_Port, Seg_Bit_2, OUT_OD);
    Set_GPIO_Mode(Seg_Bit_Port, Seg_Bit_3, OUT_OD);
    Set_GPIO_Mode(Seg_Bit_Port, Seg_Bit_4, OUT_OD);
    }

    void Segment_Show(GPIO_TypeDef *SegPort,GPIO_Pin_TypeDef SegNum,uint8_t value)
    {
    Write_Pin_Low(Seg_Bit_Port,SegNum);
    }

    void Segment_Not_Show(GPIO_TypeDef *SegPort,GPIO_Pin_TypeDef SegNum,uint8_t value)
    {
    Write_Pin_High(Seg_Bit_Port,SegNum);
    }

    void Segment_demo(void)
    {
    Segment_Show(Seg_Bit_Port,Seg_Bit_1,0);
    Segment_Show(Seg_Bit_Port,Seg_Bit_2,0);
    Segment_Show(Seg_Bit_Port,Seg_Bit_3,0);
    Segment_Not_Show(Seg_Bit_Port,Seg_Bit_4,0);
    }

    void Segment_Bit_Enable(GPIO_PORT* Port,GPIO_PIN Pin)
    {
    Segment_Show(Port,Pin,0);
    }

    void Segment_Show_Value(GPIO_PORT* Port,U8 value)
    {
    Write_Port(Port,value);
    }

[ 本帖最后由 辛昕 于 2012-6-4 00:40 编辑 ]
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 00:28
注明:
上面这个对于gpio引脚的处理,并不成熟,因为实际上,很可能他们的port口和pin脚都是乱的。
所以这种处理只适用于还是比较规律的IO连接,如果要实现任意连接,还得使用类似于我在 代码大全那个贴子里 给sjl回复里写的那段代码的方法。

通过一个算式,求出所属端口号 和 引脚数。
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 00:43
通用模板,最简单的处理就是写一些宏定义,包括常用的转化和算法。
再深一点,就是写个类似于M3上面的库。然后移植神马的都很方便。对于430,我是看到过个人有把各种外设写成“库”的,还有周立功公司,也写了很多库,类似于SD卡软件包,IP软件包,flash软件包等等,还有在TI的库上拓展M3的库,让更实用。
如果还要深入,就像这次hanker试用一样。triton.zhang把TI的库由外设包,延伸到了板级支持包,然后在头文件里修改参数就能很好的移植,做成开源,不断完善,再这个基础上再来开发M3/M4就更高效了。
点赞  2012-6-4 10:39
周立功公司的流明库里,有个芯片级的头文件,他把M3每个型号的GPIO的引脚的全部揉进去了,编程的时候都不需要管引脚了,直接宏一个芯片,就处理好了
点赞  2012-6-4 10:43
专业一点的叫法是 HAL-硬件抽象层
生活就是油盐酱醋再加一点糖,快活就是一天到晚乐呵呵的忙 =================================== 做一个简单的人,踏实而务实,不沉溺幻想,不庸人自扰
点赞  2012-6-4 11:27
周立功制作的库的模板的确很不错 值得学习
点赞  2012-6-4 12:42

回复 5楼 zca123 的帖子

都给个链接或者分享分享呗。
不过M3没碰过就是
430倒有开发板在手上
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 14:52

回复 8楼 辛昕 的帖子

M3的库,每家公司的都不一样。比如stellaris库
点赞  2012-6-4 20:12

引用: 原帖由 zca123 于 2012-6-4 10:43 发表 周立功公司的流明库里,有个芯片级的头文件,他把M3每个型号的GPIO的引脚的全部揉进去了,编程的时候都不需要管引脚了,直接宏一个芯片,就处理好了

 

原来是周立功公司自己做,现在TI的官方库也有了这个文件pin_map.h

点赞  2012-6-4 21:51
作为初学者很感谢楼主的分享,有没有具体的库和其他的文件分享啊
点赞  2012-6-4 22:01

回复 11楼 liujun19891030 的帖子

正在编写中......
不过听他们说,有个周立功的库
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 22:21

回复 10楼 zca123 的帖子

那么,你的意思是,直接到TI的那个对应的片子的页面去找咯?
额,TI的,我找过omap3,这个经历比较悲催,没找到啥有用的。

不过stm8的,我找st官网倒是比较成功
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 22:23
我也觉得这个很有必要,减少了很多的移植工作,尤其是在移植的时候出现问题还得调试。给LZ推荐一个网站,苏州大学的嵌入式实验室,有个 王宜怀 老师,搞 飞思卡尔的M4,K系列的单片机,他出来一个库,用的就是通用性的这个思想,LZ可以看看,借鉴一下。
点赞  2012-6-4 23:05

回复 14楼 upc_arm 的帖子

谢谢!
强者为尊,弱者,死无葬身之地
点赞  2012-6-4 23:42

回复 14楼 upc_arm 的帖子

估计是用K60的苦逼的孩子吧。。。。
点赞  2012-6-4 23:55

分享一下下载到的 王宜怀老师 的 M4的一本书

听了 楼上的哥们的介绍,我搜索了一下 这位 王宜怀老师 的资料。

好几本都是书的介绍,还有一篇王宜怀老师 写的 嵌入式学习的误区和方法。
都是说的实实在在的话,而且其中也有我非常关心的几个信息点:

用 一般编程 的 方法和思想,不要把 单片机程序 看成独特的存在,不要电子化它,也不要软件化它.......
他也强调了 把程序写好,封装好,以便成为库方便调用的思想,所以,尽管我刚下载到这本书,还没开始看,但我有理由相信,至少,在他的程序里,不会出现
用while,甚至是 不同时间延迟都可以封装成不同子函数(这是我看过的好几本书上做ds18b20时所用的方法)——那种方法,我只是还在学校时用过两回,现在不会再用,以后也不会再用,因为我曾跟别人说过:
这样的程序,只有在教科书上才会看到,不然再快的MCU,遇到这种写法,都会傻得不成样子......

废话不多说了,这两天,花了很多精力在写这两个帖子(另一个是 代码大全),也时刻关心回复,果不负望,抛出去的砖真的引来了不少 美玉,可以让我有更多参考和学习。

谢谢大家。

晚上看了好一会的王宜怀老师的搜索页,心想总不能啥都不做,于是,找到这本M4的教程,居然有pdf下载,于是下载了,分享到这里~~希望对大家有帮助~~

[ 本帖最后由 辛昕 于 2012-6-5 00:28 编辑 ]
强者为尊,弱者,死无葬身之地
点赞  2012-6-5 00:18
msp430也有库的呀,其他的还有BSL呢

[ 本帖最后由 lyzhangxiang 于 2012-6-5 12:58 编辑 ]
点赞  2012-6-5 12:51
给楼主参考
点赞  2012-6-5 13:00
学习了 很好
点赞  2012-6-5 15:46
12下一页
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复