微波EDA网,见证研发工程师的成长!
首页 > 研发问答 > 嵌入式设计讨论 > MCU和单片机设计讨论 > 单片机编程之 - 程序模块化及可复用性(2)转载资料!

单片机编程之 - 程序模块化及可复用性(2)转载资料!

时间:10-02 整理:3721RD 点击:
转载一个网上资料---electrlife网友的资料,觉得不错,发一下!

单片机编程之 - 程序模块化及可复用性(二)
关于这个贴子的原由请查看http://www.amobbs.com/thread-5580903-1-1.html
另外申明:技术只是实现产品的一种工具,因此你应该花更多的时间去关注产品的本身!
    终于开始写第二节了,如果你已经认真看完第一节,你应该多少对面象接口这种形式的程序方法有所感觉了,
此刻你应该已经试着把以前的各个传感器的驱动改成这种形式了!如果你还没有,那开始阅读这节之前,先
简单实践下吧!
   上一节中我们实现了两个模块,一个是软件IIC模块,另一个是EEPROM 24LC512模块,有了这两个模块,接下来
我们就开始学习如何使用这个EEPROM。在使用EEPROM之前,我把上一节的关于EEPROM面要考虑的问题再次复制过来:
1、EEPROM一般做什么使用
2、EEPROM的操作如何在多任务中使用
3、如何避免错误程序的多次误写(EEPROM有擦除次数的)
4、EEPROM如何考虑写时掉电,且如何识别错误并恢复
5、如何提高EEPROM的写效率,即那写的10MS延时
首先我们解决 1、EEPROM一般做什么使用?
我们知道EEPROM一般都是作为参数存储使用的,如传感器的校准系数、产品
生产日期、产品名称等等。那如何组织这些信息并进行管理呢?
我想最原始的方法应该主是类似如下这样:
  • #define EEP_ADC_PARAM_OFFSET        0x100
  • #define EEP_PRODUCT_TYPE_OFFSET     0x104
  • #define EEP_PRODUCT_NAME_OFFSET     0x108
  • int test_eeprom_param(void)
  • {
  •     uint32_t adc_param;
  •     return dev_24lcxx_read(&dev_ee24lc512, EEP_ADC_PARAM_OFFSET, &adc_param, sizeof(adc_param));
  • }

[color=rgb(51, 102, 153) !important]复制代码

如果你还在用以上方式,你千万不要说出来,因为太原始了,太暴力了!:-) !
这种方式的最大问题是,当你发现你先前定义的一个参数的字节设的太小时,
你需要更改所有参数的偏移量,对于我这样的懒人来说这是个巨大的工作量。
而这些工作其实可以让编译器来完成,如下所示:
  • struct nvram_sysparam {
  •     uint32_t    type;               /* 设备类型   */
  •     uint32_t    rev;                /* 设备版本号 */
  •     char        sn[32];             /* 设备序列号 */
  •     char        name[32];           /* 设备名称   */
  •     uint32_t    adc_param;
  • };
  • #define plat_offsetof(type, member)         ((unsigned long)(&((type *)0)->member))
  • int test_eeprom_param(void)
  • {
  •     uint32_t adc_param;
  •     return dev_24lcxx_read(&dev_ee24lc512, plat_offsetof(struct nvram_sysparam, adc_param),
  •                          &adc_param, sizeof(adc_param));
  • }

[color=rgb(51, 102, 153) !important]复制代码

有了上述的改变,我们可以省去了计算各个参数偏移量的工作,当增加新的参数或是改变老的参数
后,plat_offsetof宏这义会自动帮我们计算。但在这里我们发现还有一个小问题就是关于参数的字节
数,当调用dev_24lcxx_read函数时我们应当以EEPROM存储的这个参数的字节数为准,因此sizeof(adc_param)
这种做法不是推荐的,如果EEPROM里参数的字节数发生变化,那么你的应用程序用所有和这个参数的相关的操作
都得重新改过,这是我这种懒人地接受的,因此你需要下面的宏及使用方式:
  • #define plat_offsetof(type, member)         ((unsigned long)(&((type *)0)->member))
  • #define plat_paramsizeof(type, member)      (sizeof(((type *)0)->member))
  • int test_eeprom_param(void)
  • {
  •     /* 注意这里需要对其进行赋值为0操作 */
  •     uint32_t adc_param = 0;
  •     return dev_24lcxx_read(&dev_ee24lc512, plat_offsetof(struct nvram_sysparam, adc_param),
  •                          &adc_param, plat_paramsizeof(struct nvram_sysparam, adc_param));
  • }

[color=rgb(51, 102, 153) !important]复制代码

     好了到这里我们已经可以很方便增加改变EEPROM中参数的大小了,这所有改变几乎不影响应用程序。
如果对于小型的MCU开发,或是资源紧张的MCU写到里或许已经可以很好的使用了,唯一的遗憾是对于
函数中    uint32_t adc_param = 0; 变量,我们在定义时需要初始化并定义此变量比实际EEPROM中
的字节数多,至少应该相等。这点需要特别注意!但是我们不应该只满足于此,上面两种方式都存在
一个共同的问题,就是可维护性!如果出现以下情景你该如何处理?
1、你的程序处于调试阶段,struct nvram_sysparam 结构的成员基本是每天都要增加
   当你辛苦用万用表调整的ADC系数,因为你的增加新参数导致其偏移改变,所以不得
   不重新使用万用表调整的ADC系数,当这种参数如果有几十个时,我相信你会崩溃!
2、你的程序已经运行在设备上,但当某人市场部提出增加新功能时,你不得不给老机器升级
   程序,但你发现新功能需要新的参数增加时,你傻眼了,以前的设备运行时的参数及数据都
   需要重新输入,这时候你还会崩溃的!
   
      有人看了以上问题,可能会说,这还不简单吗,我增加新的参数时只从struct nvram_sysparam最后
加入,那以上问题不是都解除了吗?的确这样确实是可以解决,但是这种方式是行不通的,至于原因
请住后看。
      上面说了那么多,其实都不是今天的主题,只是餐前开胃,接下来才是今天的正餐:
      
程序模块化及可复用性的原则三:应用模块接口尽量使用ASCII字符格式而非二进制流格式
为了大家能时刻记住前面说过的原则,我再次复制过来:
程序模块化及可复用性的原则一:面像接口编程
程序模块化及可复用性的原则二:硬件的抽象接口尽量通用简单
    对于 程序模块化及可复用性的原则三,真可谓是无边无际,往大了说可能我自己也都是门外汉,因此
我只能住小了说::-),对于这个原则我不想作过多的解释,因为我觉得如果你真要想感受到此原则的好处
确实是需要实际使用中体会!好了开始我们今天的正餐程序模块化及可复用性。
    我们知道在前面一节当中我们对两个设备进行了抽象,一个是IIC,一个EEPROM,细心的读者或许会发现
前的抽象都是针对具体的设备,也就是这种设备是看的到,摸得着的,它们都有很规范的操作时序及操作集合,
因此在进行抽象时并不是特别困难。接下来我们说下关于一些看不到摸不着的设备抽象:
    有了上面的EEPROM管理的基础,我们现在想想我们需要一个统一的EEPROM管理操作集合供上层应用代码,
那么如何把这些集合操作组织起来呢?下面就看下我的做法,当然可能别人还有更出色的做法!对于EEPROM
或是FRAM之类的这种非易失性的存储器我统称他们为NVRAM,对于NVRAM我们需思考给上层怎样的接口,或者
上层需要什么时候样的接口功能,为了程序的通用和模块化,因此我们需要一个完善的NVRM接口及功能,而
具体的功能如下:
1、允许拥有多的NVRAM,且这些NVRAM可以是不同或是相同类型
2、应用程序不需要知道是什么类型的NVRAM,应用程序都视一种设备NVRAM,统一管理
3、多个不同或是相同的NVRAM在应用层可合并成一个大容量的NVRAM使用
4、当然应用程序也可以把一个NVRAM设备分成多个区分并作为多个NVRAM使用
5、NVRAM设备具有错误识别及自动恢复能力,也即写时掉电的问题
    有了上面的要求,我们就有了目标,接下来就可以向着目标前进了!到这里似乎来原则三还没有什么
关系,别着急接下来你就可以看到了!


///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
首先我们先设计出NVRAM给应用层的接口,注意设计接口的原则,我的设计如下所示
  • int nvram_set(struct nvram_partition *partition, const char *name,
  •               const char *buf, int size);
  • int nvram_get(struct nvram_partition *partition, const char *name,
  •               char *buf, int size);
  • int nvram_set_ulong(struct nvram_partition *partition, const char *name,
  •                     unsigned long value);
  • int nvram_get_ulong(struct nvram_partition *partition, const char *name,
  •                     unsigned long *value);

[color=rgb(51, 102, 153) !important]复制代码

     这里我对部分参数进行一下说明:
struct nvram_partition *partition 表示指向分区的指针,因为我们要求NVRAM是可以以分区的形式进行操作的,因此我们的函数
的基本结构应该是以分区作为对象的,这点需要理解。
const char *name 表示你需要读取的参数的名称,注意,这里使用了名称来操作NVRAM分区中的参数,这个是重点,也正是体现了
“程序模块化及可复用性的原则三:应用模块接口尽量使用ASCII字符格式而非二进制流格式”这条原则,有了这个名字,大家肯定都
会想到我们是需要把名称和对应的变量进行联系的,对的!
     后面的两个函数 nvram_set_ulong、nvram_get_ulong是为了方便操作而设立的,因为NVRAM中的参数一般的大小都不会超过4个字节
因此通过这两个函数可以很容易的设定NVRAM中各个变量的值。这里需要说明的是此函数应该自动依据NVRAM中变量的字节长度来截取
输入参数unsigned long value的值。有了这两个函数,你也可以根据实际上情况,增加nvram_set_float等方便操作的函数。好了,有了
上面的操作,下面该想分区对象了,想想为了满足上面的应用,分区对象应该需要具有哪些信息呢,现在给出我的设计如下:
  • #define NVRAM_FLAGSINFO_VALID                   0xeaf5dca5
  • #define NVRAM_PARAM_TYPE_UVALUE                 0
  • #define NVRAM_PARAM_TYPE_SVALUE                 1
  • #define NVRAM_PARAM_TYPE_STRING                 2
  • #define NVRAM_PARAM_ATTR_SUPER                  0x00
  • #define NVRAM_PARAM_ATTR_USER                   0x01
  • struct nvram_hdr {
  •     uint32_t flags;
  •     uint32_t verify_crc;
  •     uint32_t len;
  •     uint32_t data[1];
  • };
  • struct nvram_param_info {
  •     const char *name;
  •     uint32_t    offset;
  •     uint16_t    n_byte;
  •     uint8_t     type;
  •     uint8_t     attribute;
  • };
  • /* NVRAM分区信息 */
  • struct nvram_partition {
  •     char *name;                 /* 名称 */
  •     struct nvram_chip *chip;    /* 分区属于哪个NVRAM设备 */
  •     unsigned long offset;       /* 分区在NVRAM设备中的偏移量 */
  •     unsigned long size;         /* 分区的大小 */
  •     char *cache_image;
  •     char *cache_addr;           /* 分区缓冲区首地址 */
  •     unsigned long total_cache_size;/* 分区有效数据的长度,单位(字节)*/
  •     struct nvram_param_info *tbl;/* 分区中变量名称与地址映射表 */
  •     unsigned int tbl_size;
  •     OS_MUTEX *locker;           /* 分区访问互斥锁 */
  • };

[color=rgb(51, 102, 153) !important]复制代码

    关于这些结构体成员我就不再解释了,因为稍后你会看到代码,我想在源码里的使用你应该更容易理解!
但这里的一个成员我还是要说下:就是 struct nvram_chip *chip;    /* 分区属于哪个NVRAM设备 */
正如注释的那样,此变量是NVRAM与底层驱动的抽象,也即是和硬件相关联系的关键,有了个抽象我们就可以先不
用管硬件是什么器件了,就可以直接写上层的应用了,因此这个底层的抽象接口我们一定要设计好,其设计的原则是
什么呢还是那句老话:面象接口编程原则XXXXXXXXXX,这里就不再重复了!具体如下所示:
  • struct nvram_chip {
  •     char *name;
  •     unsigned long size;
  •     unsigned int  slave_addr;
  •     unsigned int  page_size;
  •     unsigned int  n_page;
  •     void         *page_buf;
  •     int (*write)(struct nvram_chip *thiz, unsigned long offset_addr,
  •                     const char *buf, int size);
  •     int (*read)(struct nvram_chip *thiz, unsigned long offset_addr,
  •                     char *buf, int size);
  • };

[color=rgb(51, 102, 153) !important]复制代码
    由于NVRAM一般是针对EEPROM这种设备也作用,因此对于底层的接口中对于EEPROM的属性部分的内容较多,如果你想在NOR或是其它
的一些存储设备上使用NVRAM,那这里你还需要加上这些设备所需要的特殊数据部分,而就目前而言我们不作更复杂的假设了,不然又得
绕进了。这里的NVRAM底层又重新抽象出了一种接口而没有直接使用struct dev_24lcxx的一个原因就是希望NVRAM更加的抽象而不紧紧针对
EEPROM,比如可能你的EEPROM设备是一个远程设备,即不要CPU板上,这个是有可能的,小编的上一个项目就是如此,需要通过CAN总去设置
下位机的EEPROM,那么只要虚拟化一个struct nvram_chip即可,把write read等实现通过CAN总线发送即可。因此有了这个我们自己抽象
的接口,我们就能很方便的把我们的参数放到任何地方,:-)。


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
有些网友提出来看起来比较费力,一部分原因可能是C语言还不够熟悉,当然我写的也比较粗,
如果因为编程语言不熟悉,就只能多看看书了,这里不可能对C语法的东西作说明,如果这样
那真的需要写的太多了,然而为了能更好帮助大家理解,对于以上的结构体我还是简单做下介绍:
struct nvram_hdr 是放在每个NVRAM分区的开始位置,也标识一个NVRAM设备
  • struct nvram_hdr {
  •     uint32_t flags;         /* 是一个标识符,表示EEPROM是否有效如果是一个已经初始化的EEPROM则其值应该是0xeaf5dca5 */
  •     uint32_t verify_crc;    /* 对EEPROM中的所有数据进行CRC32处理,也是通过这个CRC来检查EEPROM中的数据是否正确,
  •                                当然如果你的项目对EEPROM的数据的可靠性与完整性要求比较高,这里你也可以加上更复杂
  •                                的校验算法,如MD5等 */
  •     uint32_t len;           /* 表示整个NVRAM中有效数据长度 */
  •     uint32_t data[1];       /* 暂时没有使用,只作为NVRAM中头部信息与用户数据的分隔 */
  • };

[color=rgb(51, 102, 153) !important]复制代码

    在上面已经提到了,我们的接口函数使用字符串作为参数来对NVRAM中的变量进行访问,因此我们需要把字符串转化成对应的变量地址,
而下面的这个结构体struct nvram_param_info的作用就是把变量的地址与其名称进行映射。这种通过字符串查找来寻变量地址的方式
需要一定量的查找与比较,因此变量的名称应当尽量短,当然你也可以通过更先进的查找技术来解决这个问题,比如HASH查找等。
  • struct nvram_param_info {
  •     const char *name;       /* 变量的名字,是一个字符串 */
  •     uint32_t    offset;     /* 此变量在NVRAM设备的偏移量,即此变量的具体地址 */
  •     uint16_t    n_byte;     /* 此变量所占用的字节数,即变量的大小 */
  •     uint8_t     type;       /* 变量的类型,目前只支持有符号整形,无符号整形,
  •                                字符串,当然你也可以根据实际情况增加自己的类型及处理函数 */
  •     uint8_t     attribute;  /* 此变量的属性,主要的作用是,你可以为你的NVRAM中的变量设置不同的属性加以区分
  •                                一般我会把变量分为两种属性,一个是用户变量, 一个是系统变量,它们的属性不同,则当
  •                                用户需要恢复出厂时,则可以根据此属性来决定此变量是否需要被恢复,当然你也可以添加其它
  •                                的属性,比如写一次属性,只读属性等等 */
  • };

[color=rgb(51, 102, 153) !important]复制代码

     有了这个结构体struct nvram_param_info,我们就可以把相应的变量与其名称对应起来了,那是如何做的呢?
  • struct nvram_sysparam {
  •     struct nvram_hdr hdr;
  •     uint32_t    type;               /* 设备类型   */
  •     uint32_t    rev;                /* 设备版本号 */
  •     char        sn[32];             /* 设备序列号 */
  •     char        name[32];           /* 设备名称   */
  •     /* 以下是用户定义数据类型 */
  •     uint32_t    reserve32_1;
  •     uint16_t    reserve16_1;
  •     uint8_t     reserve08_1;
  • };

[color=rgb(51, 102, 153) !important]复制代码
     有了这个结构体,那我们下一步就是要把此结构体中的变量地址和其名称映射起来,这里说明下,一般我会直接
使用变量名作为其名称,那我们就需要定义如下数组:
static const struct nvram_param_info param_system_map_tbl[] = {
    {.name = "type", .offset = plat_offsetof(struct nvram_sysparam, type), ...},
   
};
     实在不好意思,写到这儿我就不想写了,太麻烦了,因此作为懒人的我肯定不会手动去一个一个去设置了。
于是就有了下面的宏定义:
  • #define NVRAM_EXPORT_PARAM_UVALUE(type, param, attr)  \
  •     {#param, plat_offsetof(type, param), sizeof(((type *)0)->param), NVRAM_PARAM_TYPE_UVALUE, attr}
  • #define NVRAM_EXPORT_PARAM_SVALUE(type, param, attr)  \
  •     {#param, plat_offsetof(type, param), sizeof(((type *)0)->param), NVRAM_PARAM_TYPE_SVALUE, attr}
  • #define NVRAM_EXPORT_PARAM_STRING(type, param, attr)  \
  •     {#param, plat_offsetof(type, param), sizeof(((type *)0)->param), NVRAM_PARAM_TYPE_STRING, attr}


[color=rgb(51, 102, 153) !important]复制代码
     有了上面的宏定义那我们刚才的初始化就相对来说就比较容易了:
  • static const struct nvram_param_info param_system_map_tbl[] = {
  •     NVRAM_EXPORT_PARAM_UVALUE(struct nvram_sysparam, type               ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_UVALUE(struct nvram_sysparam, rev                ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_STRING(struct nvram_sysparam, sn                 ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_STRING(struct nvram_sysparam, name               ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_UVALUE(struct nvram_sysparam, reserve32_1        ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_UVALUE(struct nvram_sysparam, reserve16_1        ,NVRAM_PARAM_ATTR_SUPER),
  •     NVRAM_EXPORT_PARAM_UVALUE(struct nvram_sysparam, reserve08_1        ,NVRAM_PARAM_ATTR_SUPER),
  • };

[color=rgb(51, 102, 153) !important]复制代码
     当然这也不是最省心的方案,还有一种就是利用编译器及链接器对段(SECTION)控制来处理,但这种方法需要
对程序的编译链接具有一定的了解,而本文主要目的是程序的模块化及可复用性,因此这种方法这里就不提及了!
通过以上的展示大家应该明白一点上述的结构及成员的作用了吧,
     接下来我们就该该说NVRAM的驱动接口 struct nvram_chip 了,还记得前面我们已经实现了一个抽象的设备了吧
对的,我们已经有了一个EEPROM的抽象设备,现在是时个使用它了,我们把此设备引用过来而且我们还需要实现两个
struct nvram_chip的函数,read 与 write,具体的代码如下所示:
  • extern const struct dev_24lcxx dev_ee24lc512;
  • static OS_MUTEX ee24lc512_lock;
  • static int eep_24lc512_write(struct nvram_chip *thiz, unsigned long offset_addr, const char *buf, int size)
  • {
  •     OS_ERR os_err;
  •     int r;
  •     if (!thiz) {
  •         return -1;
  •     }
  •     OSMutexPend(&ee24lc512_lock, 0, OS_OPT_PEND_BLOCKING, 0, &os_err);
  •     r = dev_24lcxx_write(&dev_ee24lc512, offset_addr, buf, size);
  •     OSMutexPost(&ee24lc512_lock, OS_OPT_POST_NONE, &os_err);
  •     return r;
  • }
  • static int eep_24lc512_read(struct nvram_chip *thiz, unsigned long offset_addr, char *buf, int size)
  • {
  •     int r;
  •     OS_ERR os_err;
  •     if (!thiz) {
  •         return -1;
  •     }
  •     OSMutexPend(&ee24lc512_lock, 0, OS_OPT_PEND_BLOCKING, 0, &os_err);
  •     r = dev_24lcxx_read(&dev_ee24lc512, offset_addr, buf, size);
  •     OSMutexPost(&ee24lc512_lock, OS_OPT_POST_NONE, &os_err);
  •     return r;
  • }

[color=rgb(51, 102, 153) !important]复制代码

    有了上面的两个操作函数,接下来我们就可以定义struct nvram_chip这个抽象的设备了,具体如下所示:
  • #define EE_ADDR             0x00
  • #define EE_CMD_RD           0xA1
  • #define EE_CMD_WR           0xA0
  • #define EE_PAGESIZE         128
  • #define EE_PAGENUM          512
  • const struct nvram_chip nvram_ee24lc512 = {
  •     .name       = "ee24lc512",
  •     .size       = EE_PAGESIZE * EE_PAGENUM,
  •     .slave_addr = EE_ADDR,
  •     .page_size  = EE_PAGESIZE,
  •     .n_page     = EE_PAGENUM,
  •     .page_buf   = 0,
  •     .write      = &eep_24lc512_write,
  •     .read       = &eep_24lc512_read,
  • };

[color=rgb(51, 102, 153) !important]复制代码
      从上面的代码中我们可以看出,如果哪天你的板子把EEPROM换成FRAM了,或是其它的存储介质,那么,你只需要重新
构建一个struct nvram_chip并实现其驱动,上层的所有逻辑不用更改,看到了吧,这就是面像接口的魅力。现在万事俱备只欠
东风了,我们东风是什么呢,就是我们最终要构建的NVRAM设备!

不错的文件,值得收藏

谢谢小编分享,学习中。

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top