转载一个网上资料---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设备!
|