[讨论] 只为uC而生,uS成长历程 9

辛昕   2013-8-7 21:30 楼主
昨晚我们已经基本阐明了 这样一个 以(字节间)超时作为判断一串数据是否接收完整 的 超时机制 的 完整思路 和 假设性分析。

为了避免过于冲突,今晚我仍然不会立马切到我之前上传的那份 我经过整理的已经变成一个足以成为第三方库 的 模块。

而是以 串口为例,从一个更加符合逻辑的过程来进行这个 模块功能的一步步实现的推演过程。

回复评论 (11)

第一步 定时器的实现

前面我们说了,这样一个定时器,我们只需要实现两个功能:
1.累加;
2.清零;

至于在什么地方累加,什么地方清零,我们一概不管。
我们只是提供这样一个函数接口,然后再去考虑在什么地方调用。


这就是Linux语境里经常说 的 策略 和 机制 相分离。

它说的 策略 是一种实现方式,就比如我们这里的函数定义(或说函数实现)——既是常说的
如何实现功能;

而 机制 是 如何调用这些功能。

这个我老是记错。
所以我只是简单地记成

"要做什么" 和 “怎么做”

很显然,我们在 考虑 怎么做的时候不去考虑 要做什么(或者在什么地方要做什么)

下面是代码:、
  1. static U32 Timer = 0;

  2. //超时定时器操作部分
  3. void runTimer(void)
  4. {
  5.      if(Timer < 0x00ffffff)
  6.         (Timer)++;
  7.      else
  8.         Timer = 0;
  9. }

  10. void resetTimer(void)
  11. {
  12.      Timer = 0;
  13. }

[ 本帖最后由 辛昕 于 2013-8-7 22:35 编辑 ]
点赞  2013-8-7 21:34
这里要补充一点

实际上,很多时候,考虑 怎么做 和 做什么 并不一定具备正常逻辑上的顺序问题。

按照惯常逻辑来说

当然是首先知道了要做什么(在哪里做什么,就是在那里调用),然后才去考虑具体怎么做。

这是一种基于自上而下的思维方式。

然而很多时候,面对具体的情况并非如此。

比如我最初在思考如何实现这个机制的时候
因为我更熟悉 串口收发。
而对于一方面要考虑接受,另一方面又要考虑如何判断超时的时候。
我并不知道具体要怎么做

所以我是先一点一点做完需要做的内容时(也就是大概像现在用的这个思路),我先分析,我肯定需要一个定时器,然后分析我需要什么

等我把这些实现好了
我再一点一点考虑怎么调用这些函数的。

这是一种自下而上的思维模式。

事实上,自上而下 自下而上这两种截然相反的思维方式并不矛盾。
很多时候它们是互为补充

如果你一条路走不通,你就要考虑先逆向行驶。
因为条条大路通罗马,反正最后你总是会走到一个汇合点的。

而编程最怕的其实是拖延。

转换思路,是战胜拖延的一个秘诀之一。
点赞  2013-8-7 21:39

然后是接收数据的部分

  1. static U8 RecvBuff[250];
  2. static U8 RecvLen = 0;

  3. int recvByte(,U8 Data)
  4. {
  5.      if(RecvLen + 1 >= 250)
  6.         return -1;
  7.      
  8.      RecvBuff[RecvLen] = Data;
  9.      RecvLen++;
  10.    
  11.      return 0;
  12. }
这是针对串口这种单字节缓冲的情况。

事实上,我们还会遇到 不定字长缓冲的情况。
所以我们其实还应当提供另外一个接口。
  1. int recvBuffer(U8 *buffer,U8 len)
  2. {
  3.      if(RecvLen + len >= 250)
  4.         len = 250 - RecvLen;
  5.      
  6.      if(String_Joint(RecvBuff,bufferRecvLen,len) != -1)
  7.      {
  8.         RecvLen += len;
  9.         return 0;
  10.      }
  11.      else
  12.         return -1;
  13. }
点赞  2013-8-7 21:41

最后一部分 超时判断

这里,我分成了两个函数实现

第一个实现纯粹的超时判断

第二个才是我们真正在应用程序中使用到的
判断超时时,把数据往外传递 的 函数接口。

这一部分是整个模块里唯一复杂的部分,但也并不复杂,具体我在注释中简单解释了。
  1. /*
  2.     这个判断和我们分析的时候略有一点距离,需要做一些介绍
  3.         这个函数是判断是否超时。它的方法是
  4.         接收到新数据与否 是靠 查询 接受长度是否发生变化来实现的。
  5.         很自然的,就要用一个静态值存储当前的接受长度;
  6. */
  7. int IsTimeout(U32 nTimeoutMs)  
  8. {   
  9.     static U8 CurrenLen = 0;
  10.        
  11.     if(CurrentLen != RecvLen)
  12.     {
  13.        CurrentLen = RecvLen;
  14.        resetTimer();
  15.     }
  16.    
  17.     if(Timer >= nTimeoutMs)
  18.     {
  19.        CurrentLen = 0;
  20.        return 1;
  21.     }

  22.     return 0;
  23. }

  24. // 这是带上了判断超时的接收函数;也是最终应用层调用的函数。
  25. int recvTimeoutData(U8 *Buf,U32 Timeout)
  26. {
  27.     int i = 0;
  28.     int DataAcount = 0;
  29.    
  30.     if(Buf == NULL)
  31.        return 0;
  32.    
  33.     if(RecvLen == 0)
  34.        return 0;
  35.    
  36.     if(IsTimeout(Timeout) == 0)
  37.        return 0;
  38.    
  39.     DataAcount = RecvLen;

  40.     for(i=0;i
  41.         Buf[i] = RecvBuff[i];
  42.    
  43.     RecvLen = 0;
  44.    
  45.     return DataAcount;
  46. }
点赞  2013-8-7 21:43

最后,我们以一个应用实例来说明

注意,包括上面的代码,我们没有特别强调它们放在什么模块。
因为在这个演示过程里,他们并不是重点,你可以随你喜欢地摆放。
  1. //==========================================
  2. // 使用举例

  3. // 定时器中断,此处我们不追究具体的单片机程序里,怎么表示它的中断函数,
  4. // 只以 TimerIsr表示,下面 串口中断 亦然,以 UartRecvIsr表示

  5. void TimerIsr(void)
  6. {
  7.      // other code;
  8.          runTimer(); // 让定时器均匀累加;
  9. }

  10. void UartRecvIsr(void)
  11. {
  12.      recvByte(SBUF);   // 原始数据(raw)
  13. }

  14. void main(void)
  15. {
  16.      // somewhere need to get datas get from uart...
  17.          U8  UartBuffer[250];  // 用以转存数据;
  18.          
  19.          if(recvTimeoutData(UartBuffer,10) == 1)
  20.          {
  21.              // 数据处理;
  22.          }
  23. }
点赞  2013-8-7 22:13
就前面的这个简单例子而言。

我们可以看到,这个超时机制 的确是一点也不复杂。

在我们写正式的程序以前,我们最后来考虑一个问题

——当然,对我来说,这并不是偶然或者我事先具有什么先见之明
我之所以会考虑到这个问题,完全是因为当时我的程序里还需要另一个类似的 超时机制。

当时我是在做2530的 无线透明传输。
我要完成的功能是

从串口收发数据,然后把收发的数据经过无线通道 收发 出去。
以此,实现两个2530模块通过无线通信,取代有线串口线通信。

我们不去关心无线通信是一个什么机制。
我们只知道
因为它是和串口配合使用,因此,串口具有的突然发起发送,另一方被动突发接收 的这个特征,在无线通信上也同样需要。

而最初的时候,我的做法是,另外重复实现了一次这个超时机制。

在我完成项目的功能之后,我开始考虑到
假如我还有其他类似需要超时机制的通信,那我岂不是要实现很多次,而且是重复地,实现 这个超时机制?

于是,我就开始思考如何把这个超时机制抽象出来。
点赞  2013-8-7 22:17
具体的想法,其实经历了一段探索。
而这其中最关键的一个思想就是

我要找出,每一个超时机制 到底由一些什么元素组成?
当然,它们必须是独立拥有的。

我们重新审视一下前面的实现过程。

最终我们发现,每一个超时机制至少都需要这些元素:

1.一个超时用的定时器;
2.一个接收缓冲;
3.当前接收长度 以及 实际接收长度;

还有什么其他吗?
我们看了一遍,没有,反过来考虑也是很明显的;

我们要超时接收,故而,肯定要设置一个独立缓冲存放数据;
此外超时机制,我们当然得每个超时机制独立有一个属于自己的 定时器

至于 当前接收长度 和 实际接收长度 也一样,它的特征是,每一个独立的超时机制 都需要这样单独一组静态变量,来记录其状态;

而在这些元素上的操作——也就是函数,全都是一样的。
所不同的也许只有 缓冲长度(或者叫 缓冲深度更加贴切)以及,字节间的超时时间。毕竟不同的通信机制有不一样的速度和实时性要求。
点赞  2013-8-7 22:22

一组数据就代表了一个独立的 超时机制

现在我们明确了一个思想

一组上面这样的数据就足以代表一个独立的超时机制。

因为不是单一数据,因此我们采用 结构体 来封装这个数据结构;
  1. typedef struct
  2. {
  3.     U32 Timer;
  4.     U8  RecvLen;
  5.     U8  CurrentLen;
  6.     U8  RecvBuff[250];   
  7. }TimeoutStr;
然后因为上面的操作都是一样的。
因此,我们可以想见,我们只需要实现一次这样的函数操作,就可以通用。

当然,在封装的时候我们考虑到另一个重要特征。
在数据上的操作是一致的,但是,使用的却是不同数据;

因此,我们需要传递参数。

而这些参数大多数都需要被操作它们的函数改变数值,因此,方便起见,我们以 地址传递参数。

这样一来。
上面的三个实现部分,我们就可以合成以下一个完整的模块。

(但是为了方便阅读代码,我仍然分成三个 代码块 展示。)
  1. //超时定时器操作部分
  2. void runTimer(U32 *Timer)
  3. {
  4.      if(*Timer < 0x00ffffff)
  5.         (*Timer)++;
  6.      else
  7.         *Timer = 0;
  8. }

  9. void resetTimer(U32 *Timer)
  10. {
  11.      *Timer = 0;
  12. }
  1. // 这组函数用于把接收到的数据放入接收缓冲
  2.    // 这个用于一串数据
  3. int recvBuffer(TimeoutStr *Str,U8 *buffer,U8 len)
  4. {
  5.      if(Str->RecvLen + len >= 250)
  6.         len = 250 - Str->RecvLen;
  7.      
  8.      if(String_Joint(Str->RecvBuff,buffer,Str->RecvLen,len) != -1)
  9.      {
  10.         Str->RecvLen += len;
  11.         return 0;
  12.      }
  13.      else
  14.         return -1;
  15. }

  16.    // 这个用于一个数据
  17. int recvByte(TimeoutStr *Str,U8 Data)
  18. {
  19.      if(Str->RecvLen + 1 >= 250)
  20.         return -1;
  21.      
  22.      Str->RecvBuff[Str->RecvLen] = Data;
  23.      (Str->RecvLen)++;
  24.    
  25.      return 0;
  26. }
  1. int IsTimeout(TimeoutStr *Str,U32 nTimeoutMs)  
  2. {   
  3.     if(Str->CurrentLen != Str->RecvLen)
  4.     {
  5.        Str->CurrentLen = Str->RecvLen;
  6.        resetTimer(&(Str->Timer));
  7.     }
  8.    
  9.     if(Str->Timer >= nTimeoutMs)
  10.     {
  11.        Str->CurrentLen = 0;
  12.        return 1;
  13.     }

  14.     return 0;
  15. }

  16. // 这是带上了判断超时的接收函数;也是最终应用层调用的函数。
  17. int recvTimeoutData(TimeoutStr *Str,U8 *Buf,U32 Timeout)
  18. {
  19.     int i = 0;
  20.     int DataAcount = 0;
  21.    
  22.     if(Buf == NULL)
  23.        return 0;
  24.    
  25.     if(Str->RecvLen == 0)
  26.        return 0;
  27.    
  28.     if(IsTimeout(Str,Timeout) == 0)
  29.        return 0;
  30.    
  31.     DataAcount = Str->RecvLen;

  32.     for(i=0;i
  33.         Buf[i] = Str->RecvBuff[i];
  34.    
  35.     Str->RecvLen = 0;
  36.    
  37.     return DataAcount;
  38. }
点赞  2013-8-7 22:26
这实质上就是我们在 第七天的时候上传的那组代码。

而现在如果你仍然感到突兀。
你可以一行一行对比我们前面实现的串口实例。

你会发现,它们相差的仅仅是传递了地址来操作而已。

于是乎,我们调用也是类似的。只是变得更加简单。
而且 有了一些面向 对象 的感觉了。

每个超时机制就是一个对象。
  1. static TimeoutStr UartRecvTimeout;

  2. void UartRecvTimeout_Initial(void)
  3. {
  4.      TimeoutStr_Initial(&UartRecvTimeout);
  5. }

  6. TimeoutStr* getUartRecvTimeout(void)
  7. {
  8.      return (&UartRecvTimeout);
  9. }
我们通过最后一个函数向外传递这个代表了一个独立(串口接收)超时机制的结构体,即传递了这个过程所需要的所有数据。

于是,我们前面那个 调用的举例,只需要改变一下接口就可以平滑移植。
点赞  2013-8-7 22:29

至此,这个通用结构的封装已经完毕

接下去,我们将把实际的单片机串口操作实现好,然后,融合这个实现好的模块,把它加入我们的uSCore里。

如前所说,我们这个简单的模块里,调用了一些我个人使用的公共函数,因此仍然无法直接使用。

因此,明晚,我将首先第一步完成我的公共函数库 的介绍。
并将它公开来。

虽然它们都只是一些非常简单的函数。但它们都是在我实际编程过程中根据需要封装起来的部分
——当然因为这项工作是大概不到几个月前才开始做的,所以数量不是太多,功能也不是十分完整
同样需要慢慢补充。


大家如果有需要,可以根据讲解使用,变成自己的实用函数库。

——注意,这些函数完全是脱离寄存器而独立存在的,因此除了确定和修改数据类型位长,你无需作任何改变!

好了,今晚就到这里了,晚安!

[ 本帖最后由 辛昕 于 2013-8-7 22:34 编辑 ]
点赞  2013-8-7 22:32
先留个名
点赞  2013-8-8 22:15
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复