感谢大家有兴趣关注这个主题,我就尝试写一点系统的解析吧。
(本文提到的论坛特指:)
刚好今天翻看关于"串口通信" 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中的一样,这个串口通信也许在有些应用中并不适合,比如非要使用带地址位的桢结构(用于多机通信)等,但我觉得那些也不是必须的,我更倾向在高层协议里处理多机地址,那样其实更灵活。
以上的代码,基本上满足我的设计思想,具有容易移植,业务逻辑无关等特点。请大家拍砖。
阅读(2532) | 评论(1) | 转发(0) |