Embedded Ethernet: Part 2

Part 1
Part 3

Here is the code to write / read from the ENC chip over SPI. This layer sits below the MAC layer (Part 3) and utilized functions from the SPI library (Part 1).

ENC28J60.h

//ENC28J60.h

#ifndef H_ENC28J60
#define H_ENC28J60

#include

#include "globals.h"
#include "static.h"
#include "controlboard.h"

#include "ENCDefs.h"

#include "protocol/IPSuite.h"

#define ERXST		(0x1000)	// Receive Buffer Start
#define ERXND		(0x1FFF)	// Receive Buffer End
#define MAMXFL		(0x05EE)	// Maximum Frame Length (in bytes)
#define BBIPG		(0x15)	// Back to Back Inter-packet Gap
#define TX_BASEADDR	(0x0100)	// Base Address for Packet Development

// Write Buffer Memory States
#define WBM_OFF	(0)
#define WBM_ON		(1)

// Read Buffer Memory States
#define RBM_OFF		(0)
#define RBM_ON		(1)

// Pointer Types
#define TX_ST		(0)
#define TX_ND		(1)
#define RD			(2)
#define WR			(3)

void ENC_Enable(void);

unsigned char ENC_ReadControlRegister(unsigned int, unsigned char);	

unsigned char ENC_ReadControlRegisterETH(unsigned char);	

unsigned char ENC_ReadControlRegisterMACMII(unsigned char);	

unsigned int  ENC_ReadControlRegisterPHY(unsigned char);

void ENC_RBM_Open(void);

void ENC_RBM_Close(void);

unsigned char ENC_ReadBufferMemory(void);

void ENC_WriteControlRegister(unsigned int, unsigned char);

void ENC_BFS(unsigned int, unsigned char);

void ENC_BFC(unsigned int, unsigned char);

void ENC_WBM_Open(void);

void ENC_WBM_Close(void);

void ENC_WriteBufferMemory(unsigned char);

void ENC_SoftReset(void);

void ENC_SetBank(unsigned char);

void ENC_Init(unsigned char);

void ENC_InitMAC(unsigned char);

void ENC_InitPHY(unsigned char);

unsigned char rtoc(REG);

void ENC_SetTxPointer(unsigned char, unsigned int);

void ENC_HandleInterrupts(void);

void ENC_TxPrepare(void);

void ENC_TxSend(void);

void ENC_HandleRx(unsigned char);

unsigned char ENC_RxBufferSize(void);

#endif

ENC28J60.c

//ENC28J60.c

#include "ENC28J60.h"

unsigned char WBM_STATE;
unsigned char RBM_STATE;

unsigned int TxEnd;			// Needed for Transmit Counter

unsigned char rx_buffer[4][2048];
unsigned char rx_buffer_wr;
unsigned char rx_buffer_rd;

extern unsigned char HW_ADDRESS[6];

//! Enables the ENC Module (asserts the /RST line)
void ENC_Enable(void)
{
	// Raise CS
	E_CS = 1;

	// Enable Ethernet
	E_RST = 1;
}

//! Reads a Control Register from ENC Module
/*!
 \param loc register location
 \param bankset explicitly change bank before read (0=no, 1=yes (recommended))
 \return the byte stored in the register
 */
unsigned char ENC_ReadControlRegister(unsigned int loc, unsigned char bankset)
{
	unsigned char bank, addr, byte;

	// Pull Bank From Address
	bank = ((loc >> 8) & 0x0F);

	// Trim Address
	addr = (loc & 0xFF);

	// Check for out of range
	if(addr > 0x1F){ return; }

	if(bankset)
	{
		ENC_SetBank(bank);
	}

	//printf("B:0x%02x, A:0x%02x\r\n", bank, addr);
	//return;

	byte = -1;

	if((bank == 0) || (bank == 1))
	{
		byte = ENC_ReadControlRegisterETH(addr);
	}
	else if(bank == 2)
	{
		byte = ENC_ReadControlRegisterMACMII(addr);
	}
	else if((bank == 3) && (addr < 0x06))
	{
		byte = ENC_ReadControlRegisterMACMII(addr);
	}
	else if((bank == 3) && (addr >= 0x06))
	{
		byte = ENC_ReadControlRegisterETH(addr);
	}
	else
	{
		printf("Failed Address Parsing.\r\n");
	}

	#ifdef DEBUG_ENC
	printf("ENC: [%02x] = %02x\r\n", addr, byte);
	#endif

	return byte;
}

//! Reads a Control Register from ENC Module's ETH Registers (should not be called explicitly.)
/*!
 \param loc register location
 \return the byte stored in the register
 \sa ENC_ReadControlRegister()
 */
unsigned char ENC_ReadControlRegisterETH(unsigned char address)
{
	unsigned char byte;

	E_CS = 0;

	SPI_Put(2, address);
	byte = SPI_Get(2);

	E_CS = 1;

	return byte;
}

//! Reads a Control Register from ENC Module's MAC or MII Registers (should not be called explicitly.)
/*!
 \param loc register location
 \return the byte stored in the register
 \sa ENC_ReadControlRegister()
 */
unsigned char ENC_ReadControlRegisterMACMII(unsigned char address)
{
	unsigned char dummy, byte;

	E_CS = 0;

	SPI_Put(2, address);
	dummy = SPI_Get(2);
	byte = SPI_Get(2);

	E_CS = 1;

	return byte;
}

//! Reads a Control Register from ENC Module's PHY Register
/*!
 \param loc register location
 \return the value (16-bits) stored in the register
 */
unsigned int ENC_ReadControlRegisterPHY(unsigned char addr)
{
	unsigned char byte, temp, hb, lb;
	unsigned int i, val;

	// Check for out of bounds
	if(addr > 0x14){ return; }

	// 1. Write the address of the PHY register to read from into the MIREGADR register.
	ENC_WriteControlRegister(MIREGADR, addr);

	// 2. Set the MICMD.MIIRD bit. The read operation begins and the MISTAT.BUSY bit is set.
	ENC_BFS(MICMD, MICMD_MIIRD);

	// 3. Wait 10.24 µs. Poll the MISTAT.BUSY bit to be certain that the operation is complete.
	//    While busy, the host controller should not start any MIISCAN operations or write to the MIWRH register.
	//    When the MAC has obtained the register contents, the BUSY bit will clear itself.
	do{
		for(i = 0; i<1024; i++){ ; }
		temp = ENC_ReadControlRegister(MISTAT, 1);
	} while ((temp & 0x01) == 0x01);

	// 4. Clear the MICMD.MIIRD bit.
	ENC_BFC(MICMD, 0x01);

	// 5. Read the desired data from the MIRDL and MIRDH registers. The order that these bytes are
	//    accessed is unimportant.
	hb = ENC_ReadControlRegister(MIRDH, 1);
	lb = ENC_ReadControlRegister(MIRDL, 1);

	val = hb;
	val = (val << 8);
	val+= lb;

	#ifdef DEBUG_ENC
	printf("ENC: [PHY 0x%02x] = %04x\r\n", addr, val);
	#endif

	return val;
}

//! Begins the process of a multi-byte read from the read-buffer
/*!
 \sa ENC_RBM_Close()
 */
void ENC_RBM_Open(void)
{
	E_CS = 0;

	SPI_Put(2, 0x3A);

	RBM_STATE = RBM_ON;
	return;
}

//! Ends the process of a multi-byte read from the read-buffer
/*!
 \sa ENC_RBM_Open()
 */
void ENC_RBM_Close(void)
{
	E_CS = 1;
	RBM_STATE = RBM_OFF;
	return;
}

//! Reads a single byte from read-buffer (register location where read pointer is)
/*!
 \return byte byte value read (value at buffer read pointer)
 */
unsigned char ENC_ReadBufferMemory(void)
{
	unsigned char byte;
	if(RBM_STATE != RBM_ON){ printf("Trying a RBM Read when RBM_STATE != RBM_ON!\r\n"); return ;}

	byte = SPI_Get(2);

	#ifdef DEBUG_ENC
	printf("ENC: [BM] = %02x\r\n", byte);
	#endif

	return byte;
}

//! Writes a byte to a control register location
/*!
 \param loc 16-bit register location (bits 11-8 are bank, 7-0 are register)
 \param byte 8-bit value to be written
 */
void ENC_WriteControlRegister(unsigned int loc, unsigned char byte)
{
	unsigned char opcode = 0x02;
	unsigned char bank, addr;

	// Pull Bank From Address
	bank = ((loc >> 8) & 0x0F);

	// Trim Address
	addr = (loc & 0xFF);

	ENC_SetBank(bank);

	if(addr > 0x1F){ return; }

	E_CS = 0;

	SPI_Put(2, ((opcode << 5) | addr));
	SPI_Put(2, byte);

	E_CS = 1;

	#ifdef DEBUG_ENC
	printf("ENC: [%02x] <= %02x\r\n", addr, byte);
	#endif

	return;
}

//! Sets certain bits in a register
/*!
 byte is not the bitval, but rather a mask
(ie: not 0, 1, 2 but rather 0x01, 0x02, 0x04)
 \param loc 16-bit register location (bits 11-8 are bank, 7-0 are register)
 \param byte 8-bit mask to be set
 */
void ENC_BFS(unsigned int loc, unsigned char byte)
{
	unsigned char opcode = 0x04;
	unsigned char bank, addr;

	// Pull Bank From Address
	bank = ((loc >> 8) & 0x0F);

	// Trim Address
	addr = (loc & 0xFF);

	if(addr > 0x1F){ return; }

	// Prevent Recursive Lock
	if(addr <= 0x1A){
		ENC_SetBank(bank);
	}

	E_CS = 0;

	SPI_Put(2, ((opcode << 5) | addr));
	SPI_Put(2, byte);

	E_CS = 1;

	#ifdef DEBUG_ENC
	printf("ENC: [%02x] <| %02x\r\n", addr, byte);
	#endif

}

//! Clears certain bits in a register
/*!
 byte is not the bitval, but rather a mask
(ie: not 0, 1, 2 but rather 0x01, 0x02, 0x04)
 \param loc 16-bit register location (bits 11-8 are bank, 7-0 are register)
 \param byte 8-bit mask to be set
 */
void ENC_BFC(unsigned int loc, unsigned char byte)
{
	unsigned char opcode = 0x05;
	unsigned char bank, addr;

	// Pull Bank From Address
	bank = ((loc >> 8) & 0x0F);

	// Trim Address
	addr = (loc & 0xFF);

	if(addr > 0x1F){ return; }

	// Prevent Recursive Lock
	if(addr <= 0x1A){
		ENC_SetBank(bank);
	}

	E_CS = 0;

	SPI_Put(2, ((opcode << 5) | addr));
	SPI_Put(2, byte);

	E_CS = 1;

	#ifdef DEBUG_ENC
	printf("ENC: [%02x] <: %02x\r\n", addr, byte);
	#endif
}

//! Starts the process of a multi-byte write to the write-buffer
/*!
 \sa ENC_RBM_Close()
 */
void ENC_WBM_Open(void)
{
	E_CS = 0;
	SPI_Put(2, 0x7A);
	WBM_STATE = WBM_ON;
	return;
}

//! Ends the process of a multi-byte write to the write-buffer
/*!
 \sa ENC_RBM_Open()
 */
void ENC_WBM_Close(void)
{
	E_CS = 1;
	WBM_STATE = WBM_OFF;
	return;
}

//! Writes a single byte to the write buffer (register location where write pointer is)
/*!
 \return byte byte to be written (value at buffer write pointer)
 */
void ENC_WriteBufferMemory(unsigned char byte)
{
	if(WBM_STATE != WBM_ON){ printf("Trying a WBM Write when WBM_STATE != WBM_ON!\r\n"); return ;}

	SPI_Put(2, byte);

	// Advance End Counter
	TxEnd++;

	#ifdef DEBUG_ENC
	printf("ENC: [BM] <= %02x\r\n", byte);
	#endif

	return;
}

//! Issues a reset command to the ENC Module
void ENC_SoftReset(void)
{
	E_CS = 0;

	SPI_Put(2, 0xFF);

	SPI_Get(2);

	E_CS = 1;

	#ifdef DEBUG_ENC
	printf("ENC: Soft Reset Issued\r\n");
	#endif

	return;
}

//! Changes the ENC module's current bank
/*!
 \param byte bank to switch to (only used bottom 2 bits)
 */
void ENC_SetBank(unsigned char byte)
{
	unsigned char bank = (byte & 0x03);

	ENC_BFC(ECON1, 0x03);
	ENC_BFS(ECON1, bank);

	#ifdef DEBUG_ENC
	printf("ENC: Bank Switched to %1d\r\n", bank);
	#endif

	return;
}

//! Initializes ENC Module
/*!
 \param echo prints spew as it goes along (0=no, 1=yes)
 */
void ENC_Init(unsigned char echo)
{
	unsigned char byte;
	unsigned char byteval;
	REG RXF;

	if(echo){ printf("Initializing ENC Controller...\r\n"); }

	ENC_SoftReset();
	if(echo){ printf("  Controller Reset.\r\n"); }

	// 6.4 OST Timer
	if(echo){ printf("  Waiting for CLKOK..."); }
	do{
		byte = ENC_ReadControlRegister(ESTAT, 1);
	} while((byte & 0x01) != 0x01);
	if(echo){ printf(" OK\r\n"); }

	// 6.1 Receive Buffer
	ENC_WriteControlRegister(ERXSTL, make8(ERXST,0));
	ENC_WriteControlRegister(ERXSTH, make8(ERXST,1));
	ENC_WriteControlRegister(ERXNDL, make8(ERXND,0));
	ENC_WriteControlRegister(ERXNDH, make8(ERXND,1));
	if(echo){ printf("  Receive Buffer Initialized [0x%04x:0x%04x]\r\n", ERXST, ERXND); }

	ENC_WriteControlRegister(ERXRDPTL, make8(ERXST,0));
	ENC_WriteControlRegister(ERXRDPTH, make8(ERXST,1));
	if(echo){ printf("  Receive Read Pointer Initialized [@0x%04x]\r\n", ERXST); }

	// 6.2 Transmit Buffer
	// No initialization necessary

	// 6.3 Receive Filters
	/*
	RXF.ERXFCONbits.ANDOR = 1;	// Packets will be rejected unless all enabled filters accept the packet (1)
	RXF.ERXFCONbits.UCEN = 1;	// Packets not having a dest. addr. matching the local MAC will be discarded (1)
	RXF.ERXFCONbits.CRCEN = 1;	// Enable CRC Filter (1)
	RXF.ERXFCONbits.PMEN = 0;	// Pattern Match Filter Disabled (0)
	RXF.ERXFCONbits.MPEN = 0;	// Magic Packet Filter Disabled (0)
	RXF.ERXFCONbits.HTEN = 0;	// Hast Table Filter Disabled (0)
	RXF.ERXFCONbits.MCEN = 0;	// Multicast Filter Disabled (0)
	RXF.ERXFCONbits.BCEN = 0;	// Broadcast Filter Disabled (0)
	*/

	ENC_WriteControlRegister(ERXFCON, RXF.Val);
	if(echo){ printf("  Receive Filters Configured [0x%02x]\r\n", RXF.Val); }

	// 6.5 MAC Initialization Settings
	ENC_InitMAC(echo);

	// 6.6 PHY Initialization Settings
	ENC_InitPHY(echo);

	ENC_BFS(EIE, EIE_PKTIE | EIE_INTIE);
	if(echo){ printf("  Packet Receive Interrupts Enabled.\r\n"); }

	ENC_BFS(ECON1, ECON1_RXEN);
	if(echo){ printf("  Packet Receiver Enabled.\r\n"); }

	rx_buffer_rd = 0;
	rx_buffer_wr = 0;
	if(echo){ printf("  Receive Buffers Enabled.\r\n"); }

	if(echo){ printf("ENC Controller Initialization Complete.\r\n"); }

	return;
}

//! Initializes ENC Module's MAC
/*!
 Should be called from ENC_Init()
 \param echo prints spew as it goes along (0=no, 1=yes)
 \sa ENC_Init()
 */
void ENC_InitMAC(unsigned char echo)
{
	unsigned char i;

	if(echo){ printf("  Begin MAC Initialization...\r\n"); }

	// 1
	ENC_BFS(MACON1, MACON1_MARXEN | MACON1_TXPAUS | MACON1_RXPAUS);

	// 2
	// Pad to 64 bytes, Enable Add. of CRC, Enable Full Duplex
	ENC_BFS(MACON3, MACON3_PADCFG0 | MACON3_TXCRCEN | MACON3_FULDPX);

	// 3
	// Leave MACON4 as default (0x00)

	// 4
	ENC_WriteControlRegister(MAMXFLL, make8(MAMXFL, 0));
	ENC_WriteControlRegister(MAMXFLH, make8(MAMXFL, 1));
	if(echo){ printf("    MAC Max Frame Length Set [0x%04x]\r\n", MAMXFL); }

	// 5
	ENC_WriteControlRegister(MABBIPG, BBIPG);
	if(echo){ printf("    MAC Back-to-back Inter-packet Gap Set [0x%02x]\r\n", BBIPG); }

	// 6
	ENC_WriteControlRegister(MAIPGL, 0x12);

	// 7

	// 8

	// 9
	ENC_WriteControlRegister(MAADR1, HW_ADDRESS[0]);
	ENC_WriteControlRegister(MAADR2, HW_ADDRESS[1]);
	ENC_WriteControlRegister(MAADR3, HW_ADDRESS[2]);
	ENC_WriteControlRegister(MAADR4, HW_ADDRESS[3]);
	ENC_WriteControlRegister(MAADR5, HW_ADDRESS[4]);
	ENC_WriteControlRegister(MAADR6, HW_ADDRESS[5]);
	if(echo)
	{
		printf("    MAC Address Programmed: ");
		for(i=0; i<6; i++)
		{
			printf("%02x", HW_ADDRESS[i]);
			if(i != 5){ printf("-"); }
		}
		printf("\r\n");
	}

	if(echo){ printf("  MAC Initialization Complete.\r\n"); }

	return;
}

//! Initializes ENC Module's PHY
/*!
 Should be called from ENC_Init()
 \param echo prints spew as it goes along (0=no, 1=yes)
 \sa ENC_Init()
 */
void ENC_InitPHY(unsigned char echo)
{
	if(echo){ printf("  Begin PHY Initialization...\r\n"); }

	if(echo){ printf("  PHY Initialization Complete.\r\n"); }
	return;
}

//! Sets a ENC Module Tx Pointer
/*!
 \param type which pointer to set (see .h files for type enumeration)
 \param loc 16-bit register address to set it to
 */
void ENC_SetTxPointer(unsigned char type, unsigned int loc)
{
	if(type == TX_ST)
	{
		ENC_WriteControlRegister(ETXSTL, make8(loc, 0));
		ENC_WriteControlRegister(ETXSTH, make8(loc, 1));
	}
	else if(type == TX_ND)
	{
		ENC_WriteControlRegister(ETXNDL, make8(loc, 0));
		ENC_WriteControlRegister(ETXNDH, make8(loc, 1));
	}
	else if(type == RD)
	{
		ENC_WriteControlRegister(ERDPTL, make8(loc, 0));
		ENC_WriteControlRegister(ERDPTH, make8(loc, 1));
	}
	else if(type == WR)
	{
		ENC_WriteControlRegister(EWRPTL, make8(loc, 0));
		ENC_WriteControlRegister(EWRPTH, make8(loc, 1));
	}

	else
	{
		printf("Error: Incorrect Pointer Type!\r\n");
	}
}

//! ISR for interrupts received from ENC
void ENC_HandleInterrupts(void)
{
	unsigned char flags, PKTCNT;
	flags = ENC_ReadControlRegister(EIR, 1);

	#ifdef DEBUG_ISR
	printf("Interrupt Flags: 0x%02x\r\n", flags);
	#endif

	if(flags)
	{
		// Disable Global IE while handling
		ENC_BFC(EIE, EIE_INTIE);

		if(flags & (1<<6))
		{
			do{
				PKTCNT = ENC_ReadControlRegister(EPKTCNT, 1);
				if(PKTCNT)
				{
					#ifdef DEBUG_ISR
					printf("- PKTIF\r\n");
					#endif

					ENC_BFC(EIR, EIR_PKTIF);
					ENC_HandleRx(PKTCNT);

					// Have any more packets arrived?
					PKTCNT = ENC_ReadControlRegister(EPKTCNT, 1);
				}
			} while(PKTCNT > 0);
		}

		if(flags & (1<<3))
		{
			#ifdef DEBUG_ISR
			printf("- TXIF\r\n");
			#endif

			ENC_BFC(EIR, EIR_TXIF);
		}

		// Re-enable Global IE
		ENC_BFS(EIE, EIE_INTIE);
	}
}

//! Prepares the ENC Module to accept an incoming packet
/*!
 Sets Pointers Correctly, Resets Length Counter, Opens WBM
 */
void ENC_TxPrepare(void)
{
	unsigned int TxStart;
	TxStart = TX_BASEADDR;

	// Set Tranmit Start Pointer
	ENC_SetTxPointer(TX_ST, TxStart);

	#ifdef DEBUG_TXPTR
	printf("[TX_ST = %04x]\r\n", TxStart);
	#endif

	// Set WBM Pointer
	ENC_SetTxPointer(WR, TxStart);

	TxEnd = TxStart;

	// Open WBM
	ENC_WBM_Open();

	// Packet Control Byte
	ENC_WriteBufferMemory(0x00);
}

//! Sends a packet that was loaded through ENC_TxPrepare and ENC_WBM
/*!
 Closes WBM, Sets Pointer, Configures Interrupts, Initiates Transfer
 */
void ENC_TxSend(void)
{
	// Close WBM
	ENC_WBM_Close();

	// Set Transmit End Pointer
	ENC_SetTxPointer(TX_ND, --TxEnd);

	#ifdef DEBUG_TXPTR
	printf("[TX_ND = %04x]\r\n", TxEnd);
	#endif

	// Configure Interrupts
	if(1)
	{
		ENC_BFC(EIR, EIR_TXIF);
		ENC_BFS(EIE, EIE_TXIE | EIE_INTIE);
	}

	// Begin Transmission
	ENC_BFS(ECON1, ECON1_TXRTS);
}

//! Prepares the ENC Module to accept an incoming packet
/*!
 Called from ISR, Handles an RX interrupt
 \param PKTCNT PKTCNT register value (# of unread packets in buffer)
 */
void ENC_HandleRx(unsigned char PKTCNT)
{
	unsigned char RDHB, RDLB, NPHB, NPLB;
	unsigned int RXRD, RXNP;
	unsigned int i;

	unsigned char byte;

	RDLB = ENC_ReadControlRegister(ERXRDPTL, 1);
	RDHB = ENC_ReadControlRegister(ERXRDPTH, 1);
	RXRD = form16(RDHB, RDLB);

	// Set Read Pointer
	ENC_WriteControlRegister(ERDPTL, RDLB);
	ENC_WriteControlRegister(ERDPTH, RDHB);

	ENC_RBM_Open();

	// Find out where next pointer is
	NPLB = ENC_ReadBufferMemory();
	NPHB = ENC_ReadBufferMemory();
	RXNP = form16(NPHB, NPLB);

	#ifdef DEBUG_RAWRX
	printf("Packet Rec'd [0x%04x:0x%04x]\r\n", RXRD, RXNP);
	#endif

	i = 0;
	while((RXRD + i + 2) < RXNP)
	{
		byte = ENC_ReadBufferMemory();
		rx_buffer[rx_buffer_wr][i+2] = byte;

		#ifdef DEBUG_RAWRX
		printf("%02x", byte);
		if(((i) % 16) == 15){ printf("\r\n"); }
		else                { printf("  "); }
		#endif

		i++;
	}

	#ifdef DEBUG_RAWRX
	printf("\r\n");
	#endif

	#ifdef DEBUG_RXRING
	printf("Stored to Buffer %d\r\n", rx_buffer_wr);
	#endif

	// Load Length
	rx_buffer[rx_buffer_wr][0] = make8(i,1);
	rx_buffer[rx_buffer_wr][1] = make8(i,0);

	// Advance rx_buffer_wr
	rx_buffer_wr++;
	if(rx_buffer_wr >= 4){ rx_buffer_wr = 0; }

	ENC_RBM_Close();

	// Decrement Packet Counter
	ENC_BFS(ECON2, ECON2_PKTDEC);

	// Advance Rx Read Pointer
	ENC_WriteControlRegister(ERXRDPTL, NPLB);
	ENC_WriteControlRegister(ERXRDPTH, NPHB);
}

//! Calculates and returns size of Rx ring buffer
/*!
 \return The value between 0(empty) and buffer-size-1, inclusive.
 */
unsigned char ENC_RxBufferSize(void)
{
	if(rx_buffer_wr == rx_buffer_rd)
	{
		return 0;
	}
	if(rx_buffer_wr > rx_buffer_rd)
	{
		return (rx_buffer_wr - rx_buffer_rd);
	}
	else
	{
		return (4 - rx_buffer_rd - rx_buffer_wr);
	}
}
Posted Wednesday, May 20th, 2009 under firmware, projects.

Leave a Reply