基于STM32的轻量级Web服务器设计
一、前言
1.1 开发背景
本项目的目的是构建一个基于STM32F103ZET6微控制器的嵌入式Web服务器,以满足远程监控和控制嵌入式设备的需求。随着物联网技术的快速发展,远程监控和控制嵌入式设备变得越来越重要。本项目通过选择STM32F103ZET6作为主控芯片,结合ENC28J60网卡实现网络通信,并移植UIP协议栈来构建轻量级的Web服务器。项目还集成了DS18B20温度传感器、LED灯模块和高电平触发的有源蜂鸣器,以实现远程监控和控制STM32设备端的功能,如LED灯和蜂鸣器的控制,以及设备端温度和RTC时间的显示。这种设计使得用户能够通过浏览器访问服务器,实时查看和控制嵌入式设备,为物联网应用提供了一种灵活、高效的解决方案。
1.2 实现的功能
(1)网络通信搭建:通过STM32F103ZET6微控制器与ENC28J60以太网控制器的集成,利用SPI接口实现数据传输,成功移植UIP轻量级TCP/IP协议栈,从而在嵌入式平台上搭建起一个功能完备的Web服务器。
(2)网页服务:在STM32内部存储一个简易的网页文件,该网页设计用于用户界面展示及交互。当用户使用任何标准的Web浏览器访问此服务器的IP地址时,即可加载并显示该网页内容。
(3)远程控制功能:
- LED灯控制:网页上设有控制按钮或界面元素,允许用户远程开关连接到STM32的LED灯模块,实现远程照明控制演示。
- 蜂鸣器控制:同样通过网页界面,用户能激活或关闭STM32连接的高电平触发的有源蜂鸣器,完成远程报警或信号提示功能的测试。
(4)环境监测与显示:
- 温度监控:集成DS18B20数字温度传感器,周期性采集环境温度数据,并通过Web界面实时显示给用户,提供基本的环境监测能力。
- 实时时钟显示:利用STM32内置的RTC(实时时钟)模块,获取并准确显示当前的时间信息,增强系统的实用性和用户交互体验。
项目不仅实现了从硬件选型、网络配置到软件开发的全过程,还展示了物联网技术在实际应用中的一个小而完整的案例,即通过简单的Web界面远程监控和控制物理设备,体现了STM32平台在物联网领域的灵活性与强大功能。
1.3 硬件模块组成
(1)主控制器模块:
- STM32F103ZET6微控制器:作为系统的核心处理器,负责运行控制程序、管理外设通信、处理网络数据包及执行用户指令。它拥有丰富的外设资源,如SPI、USART、I2C等,支持高速运算和低功耗操作,是实现项目功能的基础。
(2)网络通信模块:
- ENC28J60以太网控制器:通过SPI接口与STM32连接,提供物理层和数据链路层的网络功能,实现与外部网络的连接。它是项目中实现Web服务器功能的关键组件,支持以太网数据包的收发,使嵌入式设备能够接入互联网。
(3)温度监测模块:
- DS18B20数字温度传感器:通过单总线(One-Wire)接口与STM32通信,用于精确测量环境温度。该传感器具有体积小、精度高、直接数字输出等特点,非常适合嵌入式系统中的温度监控应用。
(4)输出控制模块:
- LED灯模块:作为基本的输出设备,通过GPIO(通用输入输出端口)直接由STM32控制,用于响应用户的控制命令,如点亮或熄灭,以直观展示控制效果。
- 有源蜂鸣器:通过高电平触发的方式连接至STM32的一个GPIO引脚,根据控制信号产生声音,实现报警或状态反馈功能。
(5)电源模块:为确保所有硬件组件稳定工作,需要一个合适的电源供应模块,为STM32微控制器、ENC28J60网卡、传感器及外围电路提供稳定的电压和电流。
1.4 ENC28J60网卡介绍
ENC28J60是一款集成MAC(Media Access Control,媒体访问控制)和10BASE-T PHY(物理层)的以太网控制器,特别适合于嵌入式系统和微控制器应用。
以下是ENC28J60网卡的一些关键特性与介绍:
(1)接口类型:它使用SPI(Serial Peripheral Interface,串行外设接口)作为与外部微控制器通信的主要方式,这使得它能够以较少的引脚数(通常为4或5条线)与诸如STM32、Arduino等微控制器连接,降低了硬件设计的复杂度。
(2)兼容性:ENC28J60兼容IEEE 802.3标准,这意味着它可以无缝地融入标准以太网网络环境中。它支持10Mbps的传输速率,适用于不需要高速网络连接的应用场景。
(3)集成功能:除了MAC和PHY层之外,ENC28J60还集成了其他一些功能,如缓冲区管理、DMA(Direct Memory Access,直接内存访问)支持、以及对多种网络帧类型的支持,包括广播、多播和单播。
(4)网络功能:能够实现完整的以太网数据包的发送和接收,支持IP、TCP、UDP等网络协议栈。开发者通常会结合LwIP(Lightweight IP)这样的轻量级TCP/IP协议栈来实现网络通信。
(5)物理层特性:支持10BASE-T标准,可以通过RJ45接口连接双绞线(UTP),支持自动极性和交叉检测,可工作在全双工或半双工模式下。
(6)功耗与封装:ENC28J60设计考虑到了低功耗应用的需求,适合电池供电设备。它通常采用SSOP(Shrink Small Outline Package)或QFN(Quad Flat No-Leads)等小型封装形式,便于在空间受限的设计中使用。
(7)灵活性与应用:由于其SPI接口和相对较低的成本,ENC28J60被广泛应用于各种嵌入式项目中,如物联网设备、智能家居、工业控制、远程监控系统等。
ENC28J60以其集成度高、接口灵活、成本效益好等特点,成为了许多嵌入式系统设计中实现网络连接的优选解决方案。
1.5 UIP协议栈
UIP(Micro IP)协议栈是一种专门为资源受限的嵌入式系统设计的轻量级TCP/IP协议栈。它最初由Adam Dunkels在SICS(瑞典计算机科学研究所)开发,目的是使即便是8位微控制器也能轻松实现网络通信,而不必负担全尺寸TCP/IP协议栈的开销。下面是UIP协议栈的详细介绍:
【1】目标与特点
-
轻量化设计:UIP的核心目标是在保持TCP/IP协议核心功能的同时,尽可能减小代码大小和内存占用。其代码量通常仅为几千字节,RAM消耗最低可达几百字节,这使得它非常适合于微控制器等资源有限的环境。
-
事件驱动编程:UIP采用了事件驱动的编程模型,而不是基于多线程或中断驱动的方法。这种设计减少了对内存的需求,并且简化了程序逻辑,使得协议栈更加易于理解和维护。
-
协议支持:尽管精简,UIP仍实现了网络层(IP)、网络互联层(ARP)、传输层(TCP)和部分应用层(如ICMP ping)的基本功能。它还支持UDP,尽管可能不如TCP那样全面。对于不常用的TCP/IP特性,如窗口缩放、时间戳等,则被省略以减少资源消耗。
-
无操作系统依赖:UIP设计为可以在“裸机”环境下运行,即不需要操作系统的支持,这使得它非常灵活,可以部署在各种嵌入式平台上。
【2】核心组件
- IP层:负责数据包的路由和分片重组,实现基本的网络层功能。
- ARP层:地址解析协议,用于将IP地址转换为MAC地址,确保数据包能够正确送达物理网络层。
- ICMP层:因特网控制消息协议,主要用于网络诊断,如ping命令的实现。
- TCP层:传输控制协议,用于建立可靠的、面向连接的数据传输服务,尽管在UIP中进行了大幅简化以适应嵌入式环境。
- 内存管理:由于嵌入式系统内存资源有限,UIP实现了高效的内存块管理和缓冲区分配机制。
【3】应用与优势
- 应用广泛:UIP被广泛应用于物联网设备、智能家居、传感器网络、嵌入式Web服务器等场景。
- 易于移植:由于其代码结构清晰、依赖少,UIP很容易被移植到不同的微控制器平台上。
- 资源效率:在保证基本网络功能的同时,极大地节省了系统资源,使得低成本、低功耗的设备也能实现网络连接。
1.6 添加UIP协议栈实现创建WEB服务器步骤
1.7 ENC28J60添加UIP协议栈实现创建WEB客户端
1.8 ENC28J60移植UIP协议并编写服务器测试示例
1.9 ENC28J60移植UIP协议并编写客户端测试示例
二、硬件设计
2.1 接线说明
【1】ENC28J60接线
【2】LED灯接线
【3】蜂鸣器接线
【4】DS18B20接线
2.2 代码与服务器交互的控制
这是判断网页下发的请求。
2.3 修改网页的页面
这个是写好的网页模版:
代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WEB服务器设计</title>
<style>
.LED{
margin-left: 10px;
}
.font{
font-size:30px;
width:130px;
height:60px;
}
.LED:hover{
background: red;
}
.LED value{
background: blue;
}
</style>
</head>
<body>
<div style="border:1px solid black;text-align: center;width:600px;height:500px;margin:auto;">
<h1>基于STM32的WEB服务器设计</h1>
<h1>微信公众号:DS小龙哥嵌入式技术资讯</h1>
<h1>这是基于ENC28J60+UIP协议栈设计的WEB服务器</h1>
<div class="paren_LED ">
<button class="LED font" v="on1" num = "1">LED1</button>
<button class="LED font" v="on2" num = "2">LED2</button>
<button class="LED font" v="on3" num = "3">LED3</button>
<button class="LED font" v="on4" num = "4">LED4</button>
<button class="LED font" v="on5" num = "5" style="display: block;margin-left:23px;margin-top:5px;">蜂鸣器</button>
</div>
<div style="text-align: left;">
<span class="font" style="margin-left:22px">温度:</span><input style="font-size: 30px" value="23℃" id="wendu"/>
</div>
<div style="text-align: left;">
<span class="font" style="margin-left:22px">时间:</span><input style="font-size: 30px" value="1s" id="time"/>
</div>
</div>
<script>
var LED = document.getElementsByClassName("paren_LED")[0];
var wendu = document.getElementById("wendu");
var time = document.getElementById("time");
var xhr = new XMLHttpRequest();
var xhr2 = new XMLHttpRequest();
//控制灯的按钮
LED.onclick = function(e){
var status = e.target.getAttribute("v");
var paren_LED = LED.children;
xhr.open("GET","test?data="+status,true);
//xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send();
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status==200){
//获取后台的数据
var data = xhr.responseText;
var sta = e.target.getAttribute("num");
e.target.setAttribute("v",data+sta);
for(var i=0;i<paren_LED.length;i++){
var str = paren_LED[i].getAttribute("v");
var st = str.substring(0,str.length-1);
if("off"==st){
paren_LED[i].style.background="blue";
}else if("on"==st){
paren_LED[i].style.background="";
}else if("Beep_on"==st){
paren_LED[i].style.background="";
}else if("Beep_off"==st){
paren_LED[i].style.background="blue";
}
}
}
}
}
//定时器
var wd = function(){
xhr2.open("GET","test?data=temp",true);
xhr2.send();
xhr2.onreadystatechange = function () {
var data = xhr2.responseText.split("&");
wendu.value= data[0]+"℃";
time.value = data[1]+"s";
}
};
setInterval("wd()",1000);
</script>
</body>
</html>
将这个文件转为C语言数组放到单片机代码工程里就行了。
如何转换? 用winhex
这个工具。
替换这个数组就行了:
2.4 设置网卡地址
因为板子是静态IP,为了方便通信,ENC28J60通过网线直接与电脑网口连接。 设置固定的IP地址。
2.5 程序运行效果
【1】下载程序
【2】串口看效果
【3】ping板子IP地址
【4】打开网页看效果
2.6 ENC28J60驱动代码
#include "delay.h"
#include <stdio.h>
#include "enc28j60.h"
/*
以下是ENC28J60驱动移植接口:
MISO--->PA6----主机输入
MOSI--->PA7----主机输出
SCLK--->PA5----时钟信号
CS----->PA4----片选
RESET-->PG15---复位
*/
#define ENC28J60_CS PAout(4) //ENC28J60片选信号
#define ENC28J60_RST PGout(15) //ENC28J60复位信号
#define ENC28J60_MOSI PAout(7) //输出
#define ENC28J60_MISO PAin(6) //输入
#define ENC28J60_SCLK PAout(5) //时钟线
static u8 ENC28J60BANK;
static u32 NextPacketPtr;
/*
函数功能:底层SPI接口收发一个字节
说 明:模拟SPI时序,ENC28J60时钟线空闲电平为低电平,在第一个下降沿采集数据
*/
u8 ENC28J60_SPI_ReadWriteOneByte(u8 tx_data)
{
u16 cnt=0;
while((SPI1->SR&1<<1)==0) //等待发送区空--等待发送缓冲为空
{
cnt++;
if(cnt>=65530)return 0; //超时退出 u16=2个字节
}
SPI1->DR=tx_data; //发送一个byte
cnt=0;
while((SPI1->SR&1<<0)==0) //等待接收完一个byte
{
cnt++;
if(cnt>=65530)return 0; //超时退出
}
return SPI1->DR; //返回收到的数据
}
/*
函数功能:复位ENC28J60,包括SPI初始化/IO初始化等
MISO--->PA6----主机输入
MOSI--->PA7----主机输出
SCLK--->PA5----时钟信号
CS----->PA4----片选
RESET-->PG15---复位
*/
void ENC28J60_Reset(void)
{
/*开启时钟*/
RCC->APB2ENR|=1<<12; //开启SPI1时钟
RCC->APB2ENR|=1<<2; //PA
GPIOA->CRL&=0X0000FFFF; //清除寄存器
GPIOA->CRL|=0XB8B30000;
GPIOA->ODR|=0XF<<4; // 上拉--输出高电平
GPIOA->ODR&=~(1<<5);
RCC->APB2ENR|=1<<8; //2 3 4 5 6 7 8
GPIOG->CRH&=0x0FFFFFFF;
GPIOG->CRH|=0x30000000;
/*SPI2基本配置*/
SPI1->CR1=0X0; //清空寄存器
SPI1->CR1|=0<<15; //选择“双线双向”模式
SPI1->CR1|=0<<11; //使用8位数据帧格式进行发送/接收;
SPI1->CR1|=0<<10; //全双工(发送和接收);
SPI1->CR1|=1<<9; //启用软件从设备管理
SPI1->CR1|=1<<8; //NSS
SPI1->CR1|=0<<7; //帧格式,先发送高位
SPI1->CR1|=0x1<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。
SPI1->CR1|=1<<2; //配置为主设备
SPI1->CR1|=1<<1; //空闲状态时, SCK保持高电平。
SPI1->CR1|=1<<0; //数据采样从第二个时钟边沿开始。
SPI1->CR1|=1<<6; //开启SPI设备。
//针对ENC28J60的特点(SCK空闲为低电平)修改SPI的设置
SPI1->CR1&=~(1<<6); //SPI设备失能
SPI1->CR1&=~(1<<1); //空闲模式下SCK为0 CPOL=0
SPI1->CR1&=~(1<<0); //数据采样从第1个时间边沿开始,CPHA=0
SPI1->CR1|=1<<6; //SPI设备使能
ENC28J60_RST=0; //复位ENC28J60
DelayMs(10);
ENC28J60_RST=1; //复位结束
DelayMs(10);
}
/*
函数功能:读取ENC28J60寄存器(带操作码)
参 数:op:操作码
addr:寄存器地址/参数
返 回 值:读到的数据
*/
u8 ENC28J60_Read_Op(u8 op,u8 addr)
{
u8 dat=0;
ENC28J60_CS=0;
dat=op|(addr&ADDR_MASK);
ENC28J60_SPI_ReadWriteOneByte(dat);
dat=ENC28J60_SPI_ReadWriteOneByte(0xFF);
//如果是读取MAC/MII寄存器,则第二次读到的数据才是正确的,见手册29页
if(addr&0x80)dat=ENC28J60_SPI_ReadWriteOneByte(0xFF);
ENC28J60_CS=1;
return dat;
}
/*
函数功能:读取ENC28J60寄存器(带操作码)
参 数:
op:操作码
addr:寄存器地址
data:参数
*/
void ENC28J60_Write_Op(u8 op,u8 addr,u8 data)
{
u8 dat = 0;
ENC28J60_CS=0;
dat=op|(addr&ADDR_MASK);
ENC28J60_SPI_ReadWriteOneByte(dat);
ENC28J60_SPI_ReadWriteOneByte(data);
ENC28J60_CS=1;
}
/*
函数功能:读取ENC28J60接收缓存数据
参 数:
len:要读取的数据长度
data:输出数据缓存区(末尾自动添加结束符)
*/
void ENC28J60_Read_Buf(u32 len,u8* data)
{
ENC28J60_CS=0;
ENC28J60_SPI_ReadWriteOneByte(ENC28J60_READ_BUF_MEM);
while(len)
{
len--;
*data=(u8)ENC28J60_SPI_ReadWriteOneByte(0);
data++;
}
*data='\0';
ENC28J60_CS=1;
}
/*
函数功能:向ENC28J60写发送缓存数据
参 数:
len:要写入的数据长度
data:数据缓存区
*/
void ENC28J60_Write_Buf(u32 len,u8* data)
{
ENC28J60_CS=0;
ENC28J60_SPI_ReadWriteOneByte(ENC28J60_WRITE_BUF_MEM);
while(len)
{
len--;
ENC28J60_SPI_ReadWriteOneByte(*data);
data++;
}
ENC28J60_CS=1;
}
/*
函数功能:设置ENC28J60寄存器Bank
参 数:
ban:要设置的bank
*/
void ENC28J60_Set_Bank(u8 bank)
{
if((bank&BANK_MASK)!=ENC28J60BANK)//和当前bank不一致的时候,才设置
{
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,ECON1,(ECON1_BSEL1|ECON1_BSEL0));
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON1,(bank&BANK_MASK)>>5);
ENC28J60BANK=(bank&BANK_MASK);
}
}
/*
函数功能:读取ENC28J60指定寄存器
参 数:addr:寄存器地址
返 回 值:读到的数据
*/
u8 ENC28J60_Read(u8 addr)
{
ENC28J60_Set_Bank(addr);//设置BANK
return ENC28J60_Read_Op(ENC28J60_READ_CTRL_REG,addr);
}
/*
函数功能:向ENC28J60指定寄存器写数据
参 数:
addr:寄存器地址
data:要写入的数据
*/
void ENC28J60_Write(u8 addr,u8 data)
{
ENC28J60_Set_Bank(addr);
ENC28J60_Write_Op(ENC28J60_WRITE_CTRL_REG,addr,data);
}
/*
函数功能:向ENC28J60的PHY寄存器写入数据
参 数:
addr:寄存器地址
data:要写入的数据
*/
void ENC28J60_PHY_Write(u8 addr,u32 data)
{
u16 retry=0;
ENC28J60_Write(MIREGADR,addr); //设置PHY寄存器地址
ENC28J60_Write(MIWRL,data); //写入数据
ENC28J60_Write(MIWRH,data>>8);
while((ENC28J60_Read(MISTAT)&MISTAT_BUSY)&&retry<0XFFF)retry++;//等待写入PHY结束
}
/*
函数功能:初始化ENC28J60
参 数:macaddr:MAC地址
返 回 值:
0,初始化成功;
1,初始化失败;
*/
u8 ENC28J60_Init(u8* macaddr)
{
u16 retry=0;
ENC28J60_Reset(); //复位底层引脚接口
ENC28J60_Write_Op(ENC28J60_SOFT_RESET,0,ENC28J60_SOFT_RESET);//软件复位
while(!(ENC28J60_Read(ESTAT)&ESTAT_CLKRDY)&&retry<500)//等待时钟稳定
{
retry++;
DelayMs(1);
};
if(retry>=500)return 1;//ENC28J60初始化失败
// do bank 0 stuff
// initialize receive buffer
// 16-bit transfers,must write low byte first
// set receive buffer start address 设置接收缓冲区地址 8K字节容量
NextPacketPtr=RXSTART_INIT;
// Rx start
//接收缓冲器由一个硬件管理的循环FIFO 缓冲器构成。
//寄存器对ERXSTH:ERXSTL 和ERXNDH:ERXNDL 作
//为指针,定义缓冲器的容量和其在存储器中的位置。
//ERXST和ERXND指向的字节均包含在FIFO缓冲器内。
//当从以太网接口接收数据字节时,这些字节被顺序写入
//接收缓冲器。 但是当写入由ERXND 指向的存储单元
//后,硬件会自动将接收的下一字节写入由ERXST 指向
//的存储单元。 因此接收硬件将不会写入FIFO 以外的单
//元。
//设置接收起始字节
ENC28J60_Write(ERXSTL,RXSTART_INIT&0xFF);
ENC28J60_Write(ERXSTH,RXSTART_INIT>>8);
//ERXWRPTH:ERXWRPTL 寄存器定义硬件向FIFO 中
//的哪个位置写入其接收到的字节。 指针是只读的,在成
//功接收到一个数据包后,硬件会自动更新指针。 指针可
//用于判断FIFO 内剩余空间的大小 8K-1500。
//设置接收读指针字节
ENC28J60_Write(ERXRDPTL,RXSTART_INIT&0xFF);
ENC28J60_Write(ERXRDPTH,RXSTART_INIT>>8);
//设置接收结束字节
ENC28J60_Write(ERXNDL,RXSTOP_INIT&0xFF);
ENC28J60_Write(ERXNDH,RXSTOP_INIT>>8);
//设置发送起始字节
ENC28J60_Write(ETXSTL,TXSTART_INIT&0xFF);
ENC28J60_Write(ETXSTH,TXSTART_INIT>>8);
//设置发送结束字节
ENC28J60_Write(ETXNDL,TXSTOP_INIT&0xFF);
ENC28J60_Write(ETXNDH,TXSTOP_INIT>>8);
// do bank 1 stuff,packet filter:
// For broadcast packets we allow only ARP packtets
// All other packets should be unicast only for our mac (MAADR)
//
// The pattern to match on is therefore
// Type ETH.DST
// ARP BROADCAST
// 06 08 -- ff ff ff ff ff ff -> ip checksum for theses bytes=f7f9
// in binary these poitions are:11 0000 0011 1111
// This is hex 303F->EPMM0=0x3f,EPMM1=0x30
//接收过滤器
//UCEN:单播过滤器使能位
//当ANDOR = 1 时:
//1 = 目标地址与本地MAC 地址不匹配的数据包将被丢弃
//0 = 禁止过滤器
//当ANDOR = 0 时:
//1 = 目标地址与本地MAC 地址匹配的数据包会被接受
//0 = 禁止过滤器
//CRCEN:后过滤器CRC 校验使能位
//1 = 所有CRC 无效的数据包都将被丢弃
//0 = 不考虑CRC 是否有效
//PMEN:格式匹配过滤器使能位
//当ANDOR = 1 时:
//1 = 数据包必须符合格式匹配条件,否则将被丢弃
//0 = 禁止过滤器
//当ANDOR = 0 时:
//1 = 符合格式匹配条件的数据包将被接受
//0 = 禁止过滤器
ENC28J60_Write(ERXFCON,ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN);
ENC28J60_Write(EPMM0,0x3f);
ENC28J60_Write(EPMM1,0x30);
ENC28J60_Write(EPMCSL,0xf9);
ENC28J60_Write(EPMCSH,0xf7);
// do bank 2 stuff
// enable MAC receive
//bit 0 MARXEN:MAC 接收使能位
//1 = 允许MAC 接收数据包
//0 = 禁止数据包接收
//bit 3 TXPAUS:暂停控制帧发送使能位
//1 = 允许MAC 发送暂停控制帧(用于全双工模式下的流量控制)
//0 = 禁止暂停帧发送
//bit 2 RXPAUS:暂停控制帧接收使能位
//1 = 当接收到暂停控制帧时,禁止发送(正常操作)
//0 = 忽略接收到的暂停控制帧
ENC28J60_Write(MACON1,MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);
// bring MAC out of reset
//将MACON2 中的MARST 位清零,使MAC 退出复位状态。
ENC28J60_Write(MACON2,0x00);
// enable automatic padding to 60bytes and CRC operations
//bit 7-5 PADCFG2:PACDFG0:自动填充和CRC 配置位
//111 = 用0 填充所有短帧至64 字节长,并追加一个有效的CRC
//110 = 不自动填充短帧
//101 = MAC 自动检测具有8100h 类型字段的VLAN 协议帧,并自动填充到64 字节长。如果不
//是VLAN 帧,则填充至60 字节长。填充后还要追加一个有效的CRC
//100 = 不自动填充短帧
//011 = 用0 填充所有短帧至64 字节长,并追加一个有效的CRC
//010 = 不自动填充短帧
//001 = 用0 填充所有短帧至60 字节长,并追加一个有效的CRC
//000 = 不自动填充短帧
//bit 4 TXCRCEN:发送CRC 使能位
//1 = 不管PADCFG如何,MAC都会在发送帧的末尾追加一个有效的CRC。 如果PADCFG规定要
//追加有效的CRC,则必须将TXCRCEN 置1。
//0 = MAC不会追加CRC。 检查最后4 个字节,如果不是有效的CRC 则报告给发送状态向量。
//bit 0 FULDPX:MAC 全双工使能位
//1 = MAC工作在全双工模式下。 PHCON1.PDPXMD 位必须置1。
//0 = MAC工作在半双工模式下。 PHCON1.PDPXMD 位必须清零。
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,MACON3,MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN|MACON3_FULDPX);
// set inter-frame gap (non-back-to-back)
//配置非背对背包间间隔寄存器的低字节
//MAIPGL。 大多数应用使用12h 编程该寄存器。
//如果使用半双工模式,应编程非背对背包间间隔
//寄存器的高字节MAIPGH。 大多数应用使用0Ch
//编程该寄存器。
ENC28J60_Write(MAIPGL,0x12);
ENC28J60_Write(MAIPGH,0x0C);
// set inter-frame gap (back-to-back)
//配置背对背包间间隔寄存器MABBIPG。当使用
//全双工模式时,大多数应用使用15h 编程该寄存
//器,而使用半双工模式时则使用12h 进行编程。
ENC28J60_Write(MABBIPG,0x15);
// Set the maximum packet size which the controller will accept
// Do not send packets longer than MAX_FRAMELEN:
// 最大帧长度 1500
ENC28J60_Write(MAMXFLL,MAX_FRAMELEN&0xFF);
ENC28J60_Write(MAMXFLH,MAX_FRAMELEN>>8);
// do bank 3 stuff
// write MAC address
// NOTE: MAC address in ENC28J60 is byte-backward
//设置MAC地址
ENC28J60_Write(MAADR5,macaddr[0]);
ENC28J60_Write(MAADR4,macaddr[1]);
ENC28J60_Write(MAADR3,macaddr[2]);
ENC28J60_Write(MAADR2,macaddr[3]);
ENC28J60_Write(MAADR1,macaddr[4]);
ENC28J60_Write(MAADR0,macaddr[5]);
//配置PHY为全双工 LEDB为拉电流
ENC28J60_PHY_Write(PHCON1,PHCON1_PDPXMD);
// no loopback of transmitted frames 禁止环回
//HDLDIS:PHY 半双工环回禁止位
//当PHCON1.PDPXMD = 1 或PHCON1.PLOOPBK = 1 时:
//此位可被忽略。
//当PHCON1.PDPXMD = 0 且PHCON1.PLOOPBK = 0 时:
//1 = 要发送的数据仅通过双绞线接口发出
//0 = 要发送的数据会环回到MAC 并通过双绞线接口发出
ENC28J60_PHY_Write(PHCON2,PHCON2_HDLDIS);
// switch to bank 0
//ECON1 寄存器
//寄存器3-1 所示为ECON1 寄存器,它用于控制
//ENC28J60 的主要功能。 ECON1 中包含接收使能、发
//送请求、DMA 控制和存储区选择位。
ENC28J60_Set_Bank(ECON1);
// enable interrutps
//EIE: 以太网中断允许寄存器
//bit 7 INTIE: 全局INT 中断允许位
//1 = 允许中断事件驱动INT 引脚
//0 = 禁止所有INT 引脚的活动(引脚始终被驱动为高电平)
//bit 6 PKTIE: 接收数据包待处理中断允许位
//1 = 允许接收数据包待处理中断
//0 = 禁止接收数据包待处理中断
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,EIE,EIE_INTIE|EIE_PKTIE);
// enable packet reception
//bit 2 RXEN:接收使能位
//1 = 通过当前过滤器的数据包将被写入接收缓冲器
//0 = 忽略所有接收的数据包
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON1,ECON1_RXEN);
if(ENC28J60_Read(MAADR5)== macaddr[0])return 0;//初始化成功
else return 1;
}
/*
函数功能:读取EREVID
参 数:
*/
u8 ENC28J60_Get_EREVID(void)
{
//在EREVID 内也存储了版本信息。 EREVID 是一个只读控
//制寄存器,包含一个5 位标识符,用来标识器件特定硅片
//的版本号
return ENC28J60_Read(EREVID);
}
/*
函数功能:通过ENC28J60发送数据包到网络
参 数:
len :数据包大小
packet:数据包
*/
void ENC28J60_Packet_Send(u32 len,u8* packet)
{
//设置发送缓冲区地址写指针入口
ENC28J60_Write(EWRPTL,TXSTART_INIT&0xFF);
ENC28J60_Write(EWRPTH,TXSTART_INIT>>8);
//设置TXND指针,以对应给定的数据包大小
ENC28J60_Write(ETXNDL,(TXSTART_INIT+len)&0xFF);
ENC28J60_Write(ETXNDH,(TXSTART_INIT+len)>>8);
//写每包控制字节(0x00表示使用macon3的设置)
ENC28J60_Write_Op(ENC28J60_WRITE_BUF_MEM,0,0x00);
//复制数据包到发送缓冲区
//printf("len:%d\r\n",len); //监视发送数据长度
ENC28J60_Write_Buf(len,packet);
//发送数据到网络
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON1,ECON1_TXRTS);
//复位发送逻辑的问题。参见Rev. B4 Silicon Errata point 12.
if((ENC28J60_Read(EIR)&EIR_TXERIF))ENC28J60_Write_Op(ENC28J60_BIT_FIELD_CLR,ECON1,ECON1_TXRTS);
}
/*
函数功能:从网络获取一个数据包内容
函数参数:
maxlen:数据包最大允许接收长度
packet:数据包缓存区
返 回 值:收到的数据包长度(字节)
*/
u32 ENC28J60_Packet_Receive(u32 maxlen,u8* packet)
{
u32 rxstat;
u32 len;
if(ENC28J60_Read(EPKTCNT)==0)return 0; //是否收到数据包?
//设置接收缓冲器读指针
ENC28J60_Write(ERDPTL,(NextPacketPtr));
ENC28J60_Write(ERDPTH,(NextPacketPtr)>>8);
// 读下一个包的指针
NextPacketPtr=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0);
NextPacketPtr|=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0)<<8;
//读包的长度
len=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0);
len|=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0)<<8;
len-=4; //去掉CRC计数
//读取接收状态
rxstat=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0);
rxstat|=ENC28J60_Read_Op(ENC28J60_READ_BUF_MEM,0)<<8;
//限制接收长度
if (len>maxlen-1)len=maxlen-1;
//检查CRC和符号错误
// ERXFCON.CRCEN为默认设置,一般我们不需要检查.
if((rxstat&0x80)==0)len=0;//无效
else ENC28J60_Read_Buf(len,packet);//从接收缓冲器中复制数据包
//RX读指针移动到下一个接收到的数据包的开始位置
//并释放我们刚才读出过的内存
ENC28J60_Write(ERXRDPTL,(NextPacketPtr));
ENC28J60_Write(ERXRDPTH,(NextPacketPtr)>>8);
//递减数据包计数器标志我们已经得到了这个包
ENC28J60_Write_Op(ENC28J60_BIT_FIELD_SET,ECON2,ECON2_PKTDEC);
return(len);
}
- 点赞
- 收藏
- 关注作者
评论(0)