[讨论] 来一次简单的程序库之旅3 想知道怎么封装库,先看别人的库

辛昕   2013-6-26 00:18 楼主
最近实在有点忙,都忘了这事情。
我们这是电子论坛,本来,我应该以一款常见单片机一款常见ide为例子,做一个简单的演示,而不是我常用的mingw。
本来想让51的小斑竹——最近和他混得挺熟来着。
用vc6做一个例子,不过他最近忙考试,也是不可开交。

算了,先按照我原来的安排,这第三节是 以几个例子说明一些库的封装原则。

回复评论 (6)

想当贼,先让人偷上几回

标题说的很粗俗。
但事实上正是如此。

如果你没见过别人怎么封装库,你是不会知道应该怎么封装库,才更合理的。

好,我们先以一些简单的例子为例说明。

说到库,我们大多数人接触到的首先就是 C标准库。
但这个库过于复杂。不是我们能应付的。

我上传过 C标准库 的一本著作,而我自己也没看多少。实在太艰深。

我不敢轻言评论它。

我们以我们接触的一些简单的库为例子。

首先,第一个是 st的库。

我说过很多次,搞单片机的,基本上大多数人都不会不知道st的这个库,并且很可能像我一样,最初的编程风格受其深刻影响。
然而,这个库的质量确实不高。

我仅举其中一例。

在stm8s的 时钟模块 stm8s_clock.c里。
有一个可以设置 HSI(内部高频时钟)的分频比的函数。
  1. void CLK_HSIPrescalerConfig(CLK_Prescaler_TypeDef HSIPrescaler)
  2. {

  3.     /* check the parameters */
  4.     assert_param(IS_CLK_HSIPRESCALER_OK(HSIPrescaler));

  5.     /* Clear High speed internal clock prescaler */
  6.     CLK->CKDIVR &= (uint8_t)(~CLK_CKDIVR_HSIDIV);

  7.     /* Set High speed internal clock prescaler */
  8.     CLK->CKDIVR |= (uint8_t)HSIPrescaler;

  9. }
这里简单介绍一下stm8s的时钟系统,以及这个 CLKDIVR寄存器。
和很多新MCU一样,stm8s的时钟比较先进,因而略显复杂。
首先它的时钟分内部外部,高频低频。
这里只是对内部高频而言,我们不管这个。

另外,它的时钟是从时钟源出发,经过分频,得到 主时钟——这个主时钟的意思是,CPU本身的运作时钟以及可以给其他外设提供时钟。
而另一层复杂的地方在于,在这两个地方,又同时可以分别设置。

在一个程序里,我试图做这样一件事。
让主时钟四分频作为CPU时钟;
同时主时钟是由内部高频16M时钟四分频得到的。
因此,在我的设想里,最终我的CPU运行频率应该是1M。

然而,当我用另一个获取时钟频率的函数去查看时,我发现一件奇怪的事情。
频率是 4M。
点赞  2013-6-26 00:35
也是无意,我后来无意发现,如果我把这两句操作的顺序反过来写。
那么,就是1M。

——当然,现在我已经忘记了什么顺序是对的,什么顺序是错的。
但是,如果使用这个库,你两个顺序得到的结果是不一样的。

这是为什么呢?

我最后查这个源代码,查到 CKDIVR这个寄存器。

这个寄存器8位,却带了两个分频比信息,有2位是16M时钟分频到主时钟,有3位是主时钟分频到CPU时钟。

而偏偏。注意看这一句
  1. /* Clear High speed internal clock prescaler */
  2.     CLK->CKDIVR &= (uint8_t)(~CLK_CKDIVR_HSIDIV);

这句话,我亏他还敢那么明目张胆地注释道:

清除高速内部时钟的分频比——简直找死!
于是,我如果先设置这个分频比,在我第二次想再次设置CPU分频比时,就会原来设置好的时钟分频比清除掉!!

事实上,我们注意到,由于操作的是同一个寄存器。
由于可以设置两个分频比,那么也就是说,这一个函数,无论如何,是不可以同时完成两个分频比的设置的。

至少,它不可以只带一个形参。
如果分两个形参,那么,完全是可以实现的。

当然,最后我选择了分开两个函数。
因为这压根就是两件事情。
我很可能只设置其中一个,而不设置另一个,我为什么要让一个函数做两件本不想干的事情呢?
点赞  2013-6-26 00:40
在这件事情里,另有一个教训,那就是
对于一些带有复合功能设置的寄存器,不要轻易地去 清除一些其他不想干位的操作。

这里可以举出另一个例子。

这一次,是ti的 cc2530的 simpliciTi协议 中的初始化函数,同样是一个时钟设置函数。
  1. CLKCONCMD = (0x00 | OSC_32KHZ);            /* 32MHz XOSC */
  2.   while (CLKCONSTA != (0x00 | OSC_32KHZ));
  3. //CLKCONCMD &= (~0xC0);
  4.   //while (CLKCONSTA & 0xC0);
被注释掉的是我后来改的代码
没注释掉的是原代码

你也许就不难理解为什么我会突然发现,当我的程序一调用这个初始化以后,突然发现 定时器的溢出频率变得快了许多许多。

提示,那个CLKCONCMD和刚刚那个CKDIVR功能和结构极其相似。
点赞  2013-6-26 00:50
所以,教训1

不要随意覆盖全局变量

对于单片机而言,最常见的“全局变量”就是片上默认的寄存器。

因为这些该死的厂家似乎很缺钱,它们总是把几个功能糅合在一个寄存器上设置。
这个时候,如果我们轻易覆盖掉之前的不相关的位,那我们就会影响其他功能。

回到这个具体例子而言
最简单的解释就是
你应该用位操作,保证只操作了相关设置的位,而不要改变其他位的状态。
点赞  2013-6-26 00:52

今天网速真的还不如RP......

最近,被RF弄得有点晕头转向,更加忘记了这个帖子的存在。
好吧,现在让我来收拾一下这个帖子。

作为结束,我选择一个更加具有代表性,而且比之前说的这个 反复覆盖同一个寄存器 影响不同功能设置 更加严重得多的 一个问题。

还是Ti的 SimpliciTi协议。
嗯,我跟他没仇,真没仇,只是我用的多了,就发现了坑爹的事情更加多而已。

首先做个简单介绍——虽然我刚刚看了看前面的几个帖子,决定尽量减少文字描述,而用代码说话,不过接下来这个问题,因为涉及几乎整个协议栈,所以还是没办法了......
点赞  2013-6-30 16:52

天啊......居然发出去了,泪牛满面~~~

这个协议定义了三种设备 Access Point接入点 Extender Range中继 End Device设备
同时,它为了应付各色各样的应用情形,它总结了四种应用模型。

这个不是问题,问题在于,在它的库中,针对这四种不同的应用模型,这三种不同的设备,协议中居然用了宏来开关代码!

这意味着什么呢?
这意味着,假如你把这部分代码事先封装成库,然后在应用层里,根据不同的设备,应用,来打开宏,这个时候已经没办法改变编译好的宏了。

这里,我们应该有这样一种观点:
一个库,应该提供它所应该具备的所有功能,然后在应用层根据需要来选择。

我曾经试图解开这个麻烦,不过这种麻烦改起来比较麻烦。
因为库里,根据不同的设备或者应用模型,打开和关闭的代码有一部分是冲突的。
也就是说,我一旦打开了一个宏,就等于关了另一个宏的代码部分。
点赞  2013-6-30 16:53
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复