基于ERTOS的CAN总线驱动设计

出处:互联网 发布于:2019-11-07 13:45:58 | 96 次阅读

  C/OS-II 是美国人 Jean Labrosse 编写的一个免费的、源码公开的嵌入式实时内核。对于开发计算机嵌入式应用产品的技术人员来说是一个实用价值很高的实时嵌入式操作系统 ERTOS(Embedded Real Time Operation System)。
  要开发出完善的 ERTOS,就要在多任务的调度和对 I/O 设备操作的稳定性、协调性方面做出大量的工作,这也是我在开发 ERTOS 过程中深深体会到的重点所在。希望本文能对开发 ERTOS 的技术人员在多任务信息流和 I/O 驱动方面有所启迪。
  1 多任务信息流关键技术
  在讨论多任务信息流之前,先讨论一下多任务的工作状态。在μC/OS 中,每个任务都是无限循环的,每个任务都处在以下五种状态之一:休眠态、就绪态、运行态、挂起态和中断态,如图 1 所示。


  在多任务的调度和驱动程序的编写过程中,必然要涉及到公用代码段和共享存储区的保护问题。即使是原有的 C 函数,可重用性方面在没有得到理论和实践的验证情况下也需要对其进行保护。这样就需要合理的算法对公用代码段、共享存储区进行保护,避免操作系统在运行过程中产生重用性问题而导致运行结果不可预测。
  系统在开发过程中,既要考虑到减少系统的复杂程度,也要兼顾其稳定性与运行效率的要求。这就需要我们对各种算法进行合理的选择:在稳定性可以保障的情况下,选择相对简单,占用 CPU 时间少的算法;在稳定性不能保障的情况下,考虑选择周全的算法。只有这样才能使操作系统在一定的配置环境下达到最高的运行效率。
  接下来分别用 void CanSendMessageProcess(void *data)、void CanSendMessage(void *data)、void CanReceiveMessageProcess(void *data)和 void CanReceiveMessage(void *data)这四个任务来描述在采用消息队列、邮箱和信号量通信机制时的信息流的传递过程。
  (1)消息队列通信机制
  消息队列在初始化的时候,建立一个指定空间大小的数组,这个数组在使用的时候取得了环形缓冲区的概念。这个数组在运行期间不会被消除,这样就避免了重复建立数组的时候内存空间的泄漏问题。当一个任务向消息队列发送一个信息的时候,相应的指针加 1(OSQIn+1),队列满时(OSQEntries = OSQSize),OSQIn 则与 OSQOut 指向同一单元。如果在 OSQIn 指向的单元内插入新的指向消息的指针,就构成 FIFO(First-In-First-Out)队列。相反,如果在 OSQOut 指向单元的下一个单元插入新的指针,就构成 LIFO 队列(Last-In-First-Out)。在本实例中,我们定义 FIFO 队列。消息指针总是从 OSQOut 指向的单元取出。OSQStart 和 OSQEnd 定义了消息指针数组的头和尾,以便在 OSQIn 和 OSQOut 到达队列的边缘时,进行边界检查和必要的指针调整,实现其循环功能。
  消息队列数据结构如下:
  typedef struct os_q {
  struct os_q *OSQPtr; /* 在空闲队列控制块中链接所有的队列控制块*/
  void *OSQStart; /*指向消息队列的指针数组的起始地址的指针*/
  void *OSQEnd; /* 指向消息队列结束单元的下一个地址的指针*/
  void *OSQIn; /* 指向消息队列中插入下一条信息位置的指针*/
  void *OSQOut; /* 指向消息队列中下一个取出消息位置的指针*/
  INT16U OSQSize; /* 消息队列中总的单元数*/
  INT16U OSQEntries; /*消息队列中总的消息数量*/
  } OS_Q;
  图 2 为消息队列信息流的演示说明。
  ① CanSendMessageProcess 任务完成信息的计算工作以后,将要发送的信息送进消息队列 1。
  ② CanSendMessage 任务负责取得消息队列 1 里面的信息。
  ③ 通过 CAN 总线 I/O 端口将数据发送到总线上去。如果消息队列中没有信息,则该任务由运行状态进入等待状态,直到从消息队列中接收到信息为止。
  ④ CanReceiveMessage 任务负责读取总线上面的信息。
  ⑤ CanReceiveMessage 任务将读取到的信息送入消息队列 2。
  ⑥ CanReceiveMessageProcess 任务是从消息队列 2 中取出信息开始计算工作,如果消息队列为空的话,该任务进入等待状态。
  消息队列适用于一对一、一对多、多对多和多对一的关系。也就是说,消息队列可以作为一块共享的公共区域,为实施互斥,任务间需要同步;为了合作,进程间需要交换信息,这样也就实现了同步和通信。


  (2)邮箱通信机制
  邮箱的概念和管道(管线)有相似的定义,一个任务或者中断服务子程序向另一个任务发送一个指针型的变量,该指针指向一个包含了特定“消息”的数据结构。在源端的任务只能向邮箱写,在目的端的任务只能从邮箱读。邮箱传输流数据,即连续的字节串或流。因此,访问一个邮箱就像是访问一个顺序文件。邮箱可以用来通知一个事件的发生(发送一条信息),也可以用来共享某些资源,这样邮箱就被当成一个二值信号量。
  图 3 为邮箱信息流的演示说明。
  ① CanSendMessageProcess 任务将计算好的数据发送给 CanSendMessage 任务,然后进入就绪态等待应答信号。CanSendMessage 在接收的同时发送应答握手信号给 CanSendMessageProcess,确认信息接收完毕。
  ②CanSendMessage 任务将 CanSend MessageProcess 任务发送来的信息发送到 CAN 总线,发送结束后进入就绪态等待下一次传输工作。
  ③ CanReceiveMessage 任务接收来自总线的信息流,将接收到的信息发送到 Can  ReceiveMessageProcess 任务,进入就绪态等待应答信号。
  ④ CanReceiveMessageProcess 任务收到信息后发送应答握手信号。
  (3)信号量通信机制
  信号量(semaphore)是一种约定机制:两个或多个任务通过简单的信号进行合作,一个任务可以被迫在某一位置停止,直到它接收到一个特定的信号。在多任务内核中普遍将信号量用于:
  ◇ 标志某事件的发生;
  ◇ 控制共享资源的使用权(满足互斥条件);
  ◇ 使两个任务的行为同步。
  信号量主要实施三种操作:
  ◇ 一个信号量可以初始化为非负数;
  ◇ 等待(wait)操作使信号量减 1。如果值变成负数,则执行等待的任务被阻塞。
  ◇ 得到 CPU 使用权的任务 singal 操作使信号量加 1。如果值不是正数,则被等待操作阻塞的任务被解除阻塞。
  为了满足信息传递过程中实时高效的原则,在消息队列中部分地引入信号量的概念。也就是 CanSendMessageProcess 任务,把若干个字节的信息一次性地发送到消息队列,令信号量加 1 并由运行态进入等待挂起状态。在 CanSendMessage 任务获得信号量后进入就绪态,等待 CPU 的使用权进入运行态。进入运行态后,该任务使信号量减 1 并从消息队列中取出信息后通过 I/O 端口发送到 CAN 总线。CanReceiveMessage 任务和 CanReceive MessageProcess 任务执行与上面相反的操作。这个实例说明了信号量用于标志某事件的发生。(见图 2。)


  2 μC/OS-II 的中断处理
  μC/OS-II 中,中断服务程序一般用汇编语言来写。以下是中断服务程序的示意代码。
  用户中断服务程序:
  保存全部 CPU 寄存器;
  调用 OSIntEnter 或 OSIntNesting 直接加 1;
  执行用户代码做中断服务;
  调用 OSIntExit;
  恢复所有 CPU 寄存器;
  执行中断返回指令;
  这里μC/OS-II 提供了两个 ISR 与内核的接口函数:OSIntEnter 和 OSIntExit。OSIntEnter 通知μC/OS-II 内核,中断服务程序开始运行了。实际上,此函数做的工作是把一个全局变量 OSIntNesting 加 1。在 x86 等有累加指令的 CPU 中,可以用指令代替 OSIntEnter:
  INC BYTE PTR OSIntNesting
  此中断嵌套计数器可以确保所有中断处理完成后再作任务调度。另一个接口函数 OSIntExit 则通知内核,中断服务已结束。根据相应情况,返回被中断点(可能是一个任务或者被嵌套的中断服务程序)或由内核作任务调度。
  用户编写的 ISR 必须被安装到某一位置,以便中断发生后,CPU 根据相应的中断号运行准确的服务程序。许多实时操作系统都提供了安装、卸载中断服务程序的 API 接口函数,有些成熟的 RTOS 甚至对中断控制器的管理都有相应的 API 函数。但 μC/OS-II 内核没有提供类似的接口函数,需要用户在对应的 CPU 移植中自己实现。这些接口函数与具体的硬件环境有关,接下来 PC 体系下的中断处理对此有详细的说明。
  3 PC 体系下的中断
  X86 系列的处理器可支持 256 个中断,并用向量表的方法来关联每个中断和相应 ISR 的位置。在实模式下,中断向量表(IVT)存于内存的低端 1K。每个向量表条目占 4 字节,保存一个 ISR 的段地址和偏移信息。PC 系统使用两个级联的可编程中断控制器 82C59A。一个 82C59A 能连接 8 个硬件中断,编号为 IRQ0~IRQ7。 PC 总共可管理 15 个外部中断源,PC 的中断控制器如图 4 所示。(关于 82C59A 的详细使用可参见有关资料。)
  在μC/OS 下,CAN 总线 I/O 端口中断向量设置伪代码:
  void CanInitHW(UI segment,BYTE Irq0,BYTE Irq1){
  保存原有的中断向量
  保存掩码寄存器的值
  使 82C59A 的掩码寄存器(0x21)各位置 1,关闭中断输入
  关闭 CPU 中断
  设置新的中断向量
  正在服务的中断禁止再次响应服务(假定当前服务中断是 IRQ5)
  开 CPU 中断
  清除 82C59A 的掩码寄存器(0X21、0XA1)各位,开启中断输入
  }
  4 信号量与缓冲队列支持下的 CAN 总线驱动
  前面介绍了μC/OS-II 内核下多任务调度的关键技术、中断与 PC 体系下中断的一般方法。又以 82C59A 的中断 5(IRQ5)、0x0D 中断向量为例,介绍了中断服务子程序的重新分配和响应 SJA1000 控制器收发的中断服务子程序。
  下面介绍信号量配合下的环形缓冲队列与中断处理程序之间的关系问题,这也是设备驱动部分的核心内容。
  ERTOS 的驱动程序与其它操作系统有所不同。比如 Windows、Unix、Solaris、Linux 等操作系统弱化了设备的概念,用户进程对设备的使用可以通过文件系统来完成。然而,在μC /OS-II 上开发 CAN 总线驱动程序没有那么严格,只要满足设备在连续的 CPU 时间上使用时不发生时间重叠就可以了。
  串行设备或者其它字符型设备都存在外设处理速度和 CPU 速度不匹配的问题,所以需要建立相应的缓冲区。向 CAN 口发送数据时,只要把数据写到缓冲区,然后由 SJA1000 控制器逐个取出往外发。从 CAN 口接收数据时,往往等收到若干个字节后才需要 CPU 进行处理,所以这些预收的数据可以先存于缓冲区。缓冲区可以设置收到若干个字节后再中断 CPU,这样避免了因为 CPU 的频繁中断而降低系统的实时性。
  在对缓冲区读写的过程中,经常会遇到想发送数据时,发送缓冲已满;想去读时,接收缓冲却是空的。对于用户程序端,可以采用查询工作方式,即放弃无法读写的操作,然后再频繁地去尝试这个操作直到成功,这样程序效率显然降低。如果引入读、写两个信号量分别对缓冲区两端的操作进行同步,问题将迎刃而解。用户任务想写但缓冲区满时,在信号量上睡眠,让 CPU 运行别的任务,待 ISR 从缓冲区读走数据后唤醒此睡眠的任务;类似地,用户任务想读但缓冲区空时,也可以在信号量上睡眠,待外部设备有数据来了再唤醒。由于μC/OS-II 的信号量提供了超时等待机制,CAN 口当然也具有超时读写能力。
  带缓冲和信号量的 CAN 口接收和发送部分见本刊网络补充版()。
  接口函数总结如下。
  void CanInitHW(UI segment,BYTE irq0,BYTE IRQ1)
  /*设置 SJA1000 控制器端口中断向量*/
  int canReleaseHW() /* 清除 SJA1000 控制器端口中断向量*/
  int canSendMsg( CANBYTE port, MSG_STRUCT msg)
  /* 向定制 SJA1000 控制器端口发送数据*/
  int canReceiveMsg( CANBYTE port, MSG_STRUCT msg_ptr)
  /*从定制 SJA1000 控制器端口接收数据
  int canConfig( CANBYTE port, CAN_STRUCT can)
  /*初始化和配置 SJA1000 控制器 */
  int canNormalRun( CANBYTE port )
  /*设置 SJA1000 正常(Normal)运行模式 */
  int canReset( CANBYTE port )
  /* SJA1000 控制器端口重新设置,缓冲区置位 0xff*/
  CANBYTE can0r( CANBYTE addr)
  /*读取 SJA1000 控制器端口 0 的定制寄存器的值 */
  CANBYTE can1r( CANBYTE addr)
  /*读取 SJA1000 控制器端口 1 的定制寄存器的值 */
  接收和发送数据缓冲区数据结构定义:
  typedef struct {
  INT16U RingBufRxCtr; /* 接收缓冲中字符数目 */
  OS_EVENT RingBufRxSem; /* 接收信号量 */
  INT8U RingBufRxInPtr; /* 接收缓冲中下一字符的写入位置 */
  INT8U RingBufRxOutPtr; /* 接收缓冲中下一待读出字符的位置 */
  INT8U RingBufRx[CAN_RX_BUF_SIZE]; /* 接收环形缓冲区*/
  INT16U RingBufTxCtr;
  /* 发送缓冲中字符数目 */
  OS_EVENT *RingBufTxSem; /* 发送信号量 */
  INT8U *RingBufTxInPtr;
  /* 发送缓冲中下一字符的写入位置 */
  INT8U *RingBufTxOutPtr;
  /* 发送缓冲中下一待读出字符的位置 */
  INT8U RingBufTx[CAN_TX_BUF_SIZE]; /* 发送环形缓冲区*/
  } CAN_RING_BUF;

0次
关键词:总线

版权与免责声明

凡本网注明“出处:维库电子市场网”的所有作品,版权均属于维库电子市场网,转载请必须注明维库电子市场网,http://www.dzsc.com,违反者本网将追究相关法律责任。

本网转载并注明自其它出处的作品,目的在于传递更多信息,并不代表本网赞同其观点或证实其内容的真实性,不承担此类作品侵权行为的直接责任及连带责任。其他媒体、网站或个人从本网转载时,必须保留本网注明的作品出处,并自负版权等法律责任。

如涉及作品内容、版权等问题,请在作品发表之日起一周内与本网联系,否则视为放弃相关权利。