Chinaunix首页 | 论坛 | 博客
  • 博客访问: 485908
  • 博文数量: 55
  • 博客积分: 1867
  • 博客等级: 上尉
  • 技术积分: 587
  • 用 户 组: 普通用户
  • 注册时间: 2006-12-29 01:33
文章分类

全部博文(55)

文章存档

2013年(1)

2012年(2)

2011年(16)

2010年(11)

2009年(5)

2008年(10)

2007年(8)

2006年(2)

分类: C/C++

2007-07-03 00:34:17

感谢大家有兴趣关注这个主题,我就尝试写一点系统的解析吧。
(本文提到的论坛特指:)

刚好今天翻看关于"串口通信" RS485方面的主题,看到好多人都贡献了自己成功的串口通信代码,但这些代码也许算法上是不错的,但感觉上系统的结构和移植性等就可以有仁者见仁,智者见智的说辞了。
我先从串口通信的设计部分开始吧,这部分之所以先开始,一是我有了上面所说的感觉,另外呢我调单片机程序基本上都是靠在串口上print信息来调的。

以下是我关于串口设计的思想:
1,基本上一个通信口的程序,因该属于驱动一级的,也就是说它不应该涉及你具体应用的业务逻辑,它只是负责接收数据,或发送你提交的数据。而具体数据的处理由应用层次的代码来解决。
2,对于多任务系统(前后台系统也如此),有可能会有多个任务企图占用串口作输出,如何处理这种竞争。
3,采用面向对象(OO)的设计方法,提高代码的利用率。
4,代码的可重入性,这对多任务来说是必须的。

具体设计:
基于以上4条,我其实最先构筑的是一个Queue对象,而不是串口通信方面的东西,这个数据结构很重要,有着很广泛的用途,比如可以用来实现消息队列,用来处理键盘(红外)来的按键序列等。当然我不会考虑想真正操作系统那样的Queue,我这里实现的只是一个通用的基于字节 FIFO。请看我的bfifo.h文件(修改过,为方便理解去掉了多任务的同步机制)

/**
 * Copyright (c) 2006-2008 iWESUN Inf.
 * All rights reserved.
 *
 * File: bfifo.h
 * Author: Hu Dong 
 * Create: Jun 22, 2007
 */


#ifndef __BYTEFIFO_H__
#define __BYTEFIFO_H__

#include "common.h"

#define FIFO_IS_EMPTY 1
#define FIFO_IS_FULLY 2

#ifndef __ASSEMBLER__
/* These only work in C program */

typedef struct ByteFifo {
  __volatile__ unsigned char  status;
  unsigned char  mxsize;                 // The max capability
  __volatile__ unsigned char  readps;    // The Read pointer
  __volatile__ unsigned char  writps;    // The Write pointer
  unsigned char *buffer;
} ByteFifo;

/**
 * A macro to ease the declaration of ByeFifo definition.
 * USAGE: BYTEFIFO(uart_tx, 16);
 *
 * @param name, The name of the ByteFifo
 * @param mxsize, The max capability of the Fifo
 */
#define BYTEFIFO(name, mxsize)                \
  unsigned char name##_fifo[mxsize];          \
  ByteFifo name = {                           \
    FIFO_IS_EMPTY, mxsize, 0, 0,              \
    &name##_fifo[0]                           \
  }

/**
 * Test whether the fifo is fully
 *
 * @param ByteFifo*, The pointer to the ByteFifo
 * @return bool, TRUE: fully, FALSE: not fully
 */
INTERFACE bool is_fully(ByteFifo*);

/**
 * Test whether the fifo is empty
 *
 * @param ByteFifo*, The pointer to the ByteFifo
 * @return bool, TRUE: empty, FALSE: not empty
 */
INTERFACE bool is_empty(ByteFifo*);

/**
 * Get a byte from byte fifo
 * Return -1 if fifo is empty
 *
 * @param ByteFifo*, The pointer to the fifo
 * @return int, the data. -1 means fifo is empty
 */
INTERFACE int get_byte(ByteFifo*);

/**
 * Put a byte to byte fifo
 * Return -1 if fifo is fully
 *
 * @param ByteFifo*, The pointer to the fifo
 * @param unsigned char, the byte need to put
 * @return int, the byte had put into fifo, -1 means fifo is fully
 */
INTERFACE int put_byte(ByteFifo*, unsigned char);

/**
 * Clear byte fifo
 * @param ByteFifo*, The pointer to the fifo
 */
INTERFACE void clear_fifo(ByteFifo*);

#endif /* !__ASSEMBLER__ */


#endif /* __BYTEFIFO_H__ */

以上基本上描绘了一个FIFO具有的功能,从方法的签名上可以看出代码是能够重入的,也就是说在系统可以在需要的地方创建很多个FIFO,而它们都可以共享同样的代码。
多说几句,看了论坛上好多人的代码,好像都不是很重视头文件(有人拍砖吗?),其实头文件表明的就是体现你的设计思想,记得有一次听一个搞Java的老外说,他们如何验收一个软件的设计,主要看人家的接口定义如何。而我理解C/C++的头文件就是一个对象的接口定义。

再看一下这个FIFO的具体实现:(修改过,为方便理解去掉了多任务的同步机制)

/**
 * Copyright (c) 2006-2008 iWESUN Inf.
 * All rights reserved.
 *
 * File: bfifo.c
 * Author: Hu Dong
 * Create: Jun 22, 2007
 *
 */

#include "bfifo.h"

/**
 * Test whether the fifo is fully
 *
 * @param ByteFifo*, The pointer to the ByteFifo
 * @return bool, TRUE: fully, FALSE: not fully
 */
bool is_fully(ByteFifo* pFifo){
  return (pFifo->status & FIFO_IS_FULLY) ? TRUE : FALSE;
}

/**
 * Test whether the fifo is empty
 *
 * @param ByteFifo*, The pointer to the ByteFifo
 * @return bool, TRUE: empty, FALSE: not empty
 */
bool is_empty(ByteFifo* pFifo){
  return (pFifo->status & FIFO_IS_EMPTY) ? TRUE : FALSE;
}

/**
 * Get a byte from byte fifo
 * Return -1 if fifo is empty
 *
 * @param ByteFifo*, The pointer to the fifo
 * @return int, the data. -1 means fifo is empty
 */
int get_byte(ByteFifo* pFifo){

  unsigned char _writps = pFifo->writps;
  unsigned char _readps = pFifo->readps;
  unsigned char _status = pFifo->status;

  if (_status & FIFO_IS_EMPTY) return -1;

  int b = *(pFifo->buffer+_readps);
 
  _readps++;
  _readps = (_readps >= pFifo->mxsize) ? 0 : _readps;

  if (_readps == _writps){
    _status |= FIFO_IS_EMPTY;
  }else{
    _status &= ~FIFO_IS_FULLY;
  }

  pFifo->readps = _readps;
  pFifo->status = _status;

  return b;
}

/**
 * Put a byte to byte fifo
 * Return -1 if fifo is fully
 *
 * @param ByteFifo*, The pointer to the fifo
 * @param unsigned char, the byte need to put
 * @return int, the byte had put into fifo, -1 means fifo is fully
 */
int put_byte(ByteFifo* pFifo, unsigned char b){

  unsigned char _writps = pFifo->writps;
  unsigned char _readps = pFifo->readps;
  unsigned char _status = pFifo->status;

  if (_status & FIFO_IS_FULLY) return -1;

  *(pFifo->buffer+_writps) = b;

  _writps++;
  _writps = (_writps >= pFifo->mxsize) ? 0 : _writps;
 
  if (_writps == _readps){
    _status |= FIFO_IS_FULLY;
  }else{
    _status &= ~FIFO_IS_EMPTY;
  }

  pFifo->writps = _writps;
  pFifo->status = _status;

  return (int) b;
}

/**
 * Clear byte fifo
 * @param ByteFifo*, The pointer to the fifo
 */
void clear_fifo(ByteFifo* pFifo){
  pFifo->status = FIFO_IS_EMPTY;
  pFifo->writps = 0;
  pFifo->status = 0;
}

熟悉算法的人,可以看出这是一个通用字节循环队列的实现。这个队列的实现完全是可移植的,其实这些底层的结构性代码,我一般都是在PC上调试的,因为它们根本和单片机没有任何关系。

好了,到了这一步有了基础,看看如何在串口通信中使用这个FIFO,来做一个业务逻辑无关的通用串口程序,还是先看头文件uart.h(修改过,为方便理解去掉了多任务的同步机制),毕竟是体现设计思想的东西,哈哈

/**
 * Copyright (c) 2006-2008 iWESUN Inf.
 * All rights reserved.
 *
 * File: uart.h
 * Author: Hu Dong 
 * Create: Jun 22, 2007
 */

#ifndef __UART_H__
#define __UART_H__

#include "bfifo.h"  // 看好,引入那个FIFO了

#define UART_TXBUF_SIZE 16  // 发送FIFO的大小
#define UART_RXBUF_SIZE 16  // 接收FIFO的大小

#define UART_RECVDATA 1
#define UART_SENDDATA 2
#define UART_RX_FULLY 4  // Buffer is fully
#define UART_TX_EMPTY 8  // Buffer is empty

#ifndef __ASSEMBLER__
/* These only work in C program */

/**
 * Uart will callback this method when uart status had changed
 *
 * @param unsigned char, The status of transfer
 */
typedef void (*uart_hook)(unsigned char);  // CallBack,也就是钩子,比如可以用来干收到数据,或发送数据时闪闪灯啊什么的。

/* 什么什么控制块,受DOS时代东西的影响,哈哈*/
typedef struct UartControlBlock {
  __volatile__ unsigned char status;
  uart_hook hook;
  ByteFifo* txbuf;  // 定义发送FIFO
  ByteFifo* rxbuf;  // 定义接收FIFO
} UARTCB;

/**
 * Initialize UART for Rx and Tx
 * You should invoke this method to initializ UART
 * Currently, this method just supported 8bit data, 1bit stop and none check
 *
 */
INTERFACE void init_uart(uart_hook);

/**
 * Put a char to UART Tx queue
 *
 * @param char, the char need put to UART
 * @return 0 is successful, other values are failed
 */
INTERFACE int uart_putchar(char b);

/**
 * Get a char from UART Rx queue
 *
 * @return the char, if equas to -1, there are some errors occur.
 */
INTERFACE int uart_getchar(void);

/*用来打印memory中的字符串,不是必须的,可以设计到别的地方*/
INTERFACE void uart_print_memstr(const unsigned char*);

/*用来打印FLASH中的字符串,不是必须的,可以设计到别的地方*/
INTERFACE void uart_print_pgmstr(const prog_uchar*);

/* 这一段宏定义,sbi, cbi都是宏定义,根据具体开发环境可以再定义
 * 我在Mega系列gcc中的定义是:(定义在arch.h文件中)
 *
 * #define sbi(P, b) __asm__ __volatile__ ("sbi %0,%1" : :"I"(_SFR_IO_ADDR(P)), "I"(b))
 * #define cbi(P, b) __asm__ __volatile__ ("cbi %0,%1" : :"I"(_SFR_IO_ADDR(P)), "I"(b))
 *
 * RS485_DDR,RS485_PORT, RS485_DEPIN, RS485_REPIN都定义在具体应用hardware.h中,
 * 比如,我的一个具体应用中
 * #define __RS485__ 1
 * #define RS485_DDR DDRD
 * #define RS485_PORT PORTD
 * #define RS485_DEPIN PD2
 * #define RS485_REPIN PD2
 */
#define enable_rs485_send()  sbi(RS485_PORT,RS485_DEPIN)
#define enable_rs485_recv()  cbi(RS485_PORT,RS485_REPIN)
#define disable_rs485_send() cbi(RS485_PORT,RS485_DEPIN)
#define disable_rs485_recv() sbi(RS485_PORT,RS485_REPIN)


#endif /* ! __ASSEMBLER__ */

#endif /* __UART_H__ */

很简单吧,看头文件还看不出我是要中断方式实现收发,还是查询方式。这就对了,底层实现是无所谓的,但头文件是很重要的。
又多说几句,我不知道大家在设计程序时,是不是也是先写头文件,反正我是这样的,有时候为了方法的名字和签名也要斟酌好半天,当然,还有注释,其实这就是我理解的设计。
有了uart.h,再来看uart.c吧,同样也是去掉了同步机制的代码。
大家都知道,中断方式更高级,毕竟少占用CPU时间,所以我的实现肯定是要用中断方式的。尤其是多任务系统,查询方式根本就是笨笨。

/**
 * Copyright (c) 2006-2008 iWESUN Inf.
 * All rights reserved.
 *
 * File: uart.c
 * Author: Hu Dong 
 * Create: Jun 22, 2007
 */

#include "uart.h"

#if __UART__ == 1 // 有些系统不用串口通信,可以把代码屏蔽掉,以减少代码大小,我不知论坛上有多少人使用这种方法。

BYTEFIFO(rxbuf, UART_RXBUF_SIZE); // 创建接收FIFO的实例
BYTEFIFO(txbuf, UART_TXBUF_SIZE); // 创建发送FIFO的实例
UARTCB uart = { UART_TX_EMPTY, NULL, &txbuf, &rxbuf}; // 创建那个控制块

void _print(unsigned char);

/**
 * Initialize UART for Rx and Tx
 * You should invoke this method to initializ UART
 * Currently, this method just supported 8bit data, 1bit stop and none check
 *
 */
void init_uart(uart_hook hook){
  /* 这里又多说几句,看到好多人的hard code,很不舒服,要是时钟变了,波特率变了,
   * 怎么办?
   * 我还是隆重介绍我的方法,我通常在具体应用hardware.h里定义
   *
   * #ifndef F_CPU
   * #define F_CPU 8000000
   * #endif
   *
   * #define __UART__ 1
   * #define BAUDRATE 9600
   * #define UART_UBRR F_CPU/16/BAUDRATE-1  
   *
   * 以上的宏定义,在通常情况下(我还没有发现例外)是自适应你选定的时钟和波特率的
   */

  UBRRH = HI8(UART_UBRR);
  UBRRL = LO8(UART_UBRR);
  UCSRB = _BV(RXCIE) | _BV(TXCIE) | _BV(RXEN) | _BV(TXEN);
  UCSRC = 0x86; // Async, 8 bits data, 1 bit stop, None check

  uart.hook = hook;

  #if __RS485__ == 1
  RS485_DDR |= (_BV(RS485_DEPIN) | _BV(RS485_REPIN));
  disable_rs485_send();
  enable_rs485_recv();
  #endif
}

/**
 * Put a char to UART Tx queue
 *
 * @param char, the char need put to UART
 * @return 0 is successful, -1 is failed
 */
int uart_putchar(char b){
  int ret = 0;

  into_critical();  // 其实就是 cli
 
  /*
   * 我靠以下代码,来可能会引发发送中断
   */
  unsigned char status = UCSRA;
  if ( is_empty(uart.txbuf) && (status & _BV(UDRE))){
    // Fifo is empty and UDRE, so send byte directly
    UDR = b;
  }else{
    uart.status &= ~UART_TX_EMPTY;
    ret = put_byte(uart.txbuf, b); // 正在发送,多余的放到FIFO里,中断程序会自己取
  }

  exit_critical(); // 其实就是 sei

  return ret;
}

/**
 * Get a char from UART Rx queue
 *
 * @return the char, if equas to -1, there are some errors occur.
 */
int uart_getchar(void){
  into_critical();

  int ret = get_byte(uart.rxbuf); // 接收更简单了,直接从FIFO读,读到-1没有数据
  uart.status &= ~(UART_RECVDATA | UART_RX_FULLY);

  exit_critical();

  return ret;
}

void uart_print_memstr(const unsigned char* p){
  #if __RS485__ == 1
  disable_rs485_recv();
  enable_rs485_send();
  #endif

  unsigned char b = *(p++);
  while(b != 0){
    _print(b);
    b = *(p++);
  }

  #if __RS485__ == 1
  _delay_ms(1);
  while(!(is_empty(&txbuf)))wdt_reset();
  disable_rs485_send();
  enable_rs485_recv();
  #endif
}

void uart_print_pgmstr(const prog_uchar* p){
  #if __RS485__ == 1
  disable_rs485_recv();
  enable_rs485_send();
  #endif

  unsigned char b = pgm_read_byte(p++);
  while(b != 0){
    _print(b);
    b = pgm_read_byte(p++);
  }

  #if __RS485__ == 1
  _delay_ms(1);
  while(!(is_empty(&txbuf)))wdt_reset();
  disable_rs485_send();
  enable_rs485_recv();
  #endif
}

void _print(unsigned char b){
  int err = uart_putchar(b);
  while(err == -1){
    wdt_reset();
    err = uart_putchar(b);
  }
}

/**
 * Rx complete interrupt handler
 */
void NAKED SIG_UART_RECV(void);
void SIG_UART_RECV(void){
  into_critical();

  prologue(); // 可以理解成把所有寄存器保存起来
 
  if (!(uart.status & UART_RECVDATA)){
    uart.status |= UART_RECVDATA;
    if (uart.hook != NULL) uart.hook(uart.status); // 收到数据CallBack一下
  }
 
  unsigned char tmp = UCSRA;
  unsigned char b = UDR;
  if (!(tmp & (_BV(FE) | _BV(DOR)))){
    int err = put_byte(uart.rxbuf, b);  // 把收到的数据放到FIFO中
    if (err == -1){
      uart.status |= UART_RX_FULLY;
      if (uart.hook != NULL) uart.hook(uart.status); // FIFO满了,CallBack一下
    }
  }
 
  epilogue(); // 可以理解成恢复所有的寄存器,开中断
}

/**
 * Tx data register empty interrupt handler
 */
void NAKED SIG_UART_TRANS(void);
void SIG_UART_TRANS(void){
  into_critical();

  prologue();
 
  int b = get_byte(uart.txbuf); // 发送寄存器空了,从FIFO中取下一个数据来发送
  if (b > 0){
    UDR = b;
    if (!(uart.status & UART_SENDDATA)){
      uart.status |= UART_SENDDATA;
      if (uart.hook != NULL) uart.hook(uart.status); // 发送数据,CallBack一下
    }
  }else{
    uart.status &= ~UART_SENDDATA;
    uart.status |= UART_TX_EMPTY;
    if (uart.hook != NULL) uart.hook(uart.status); // 没有数据了,CallBack一下
  }

  epilogue();
}

#endif

展示了4段代码,基本上和我这个AvrcX中的一样,这个串口通信也许在有些应用中并不适合,比如非要使用带地址位的桢结构(用于多机通信)等,但我觉得那些也不是必须的,我更倾向在高层协议里处理多机地址,那样其实更灵活。
以上的代码,基本上满足我的设计思想,具有容易移植,业务逻辑无关等特点。请大家拍砖。
阅读(2571) | 评论(1) | 转发(0) |
0

上一篇:AvrcX 中的一个bug

下一篇:000100的全面分析

给主人留下些什么吧!~~