您现在的位置是:首页 >技术杂谈 >硬件 TCP/IP 协议栈网站首页技术杂谈
硬件 TCP/IP 协议栈
全硬件的TCP/IP 协议栈简介
全硬件TCP/IP 协议栈芯片采用了“TCP/IP Offload Platform”技术,囊括了TCP/IP 协议栈全部的四层结构,独立于MCU 运作,信息的进栈/出栈,封包/解包等网络数据处理全部在全硬件TCP/IP 网络芯片中进行,高速硬件化TCP/IP 协议处理卸载掉了MCU 对于Ethernet 庞大数据处理的负载,从而,使MCU 保持高效运转且实现高速实际网络传输。同时,这也避免了MCU受到网络攻击的危险,网络攻击不会对MCU 中的主程序产生影响,增加了MCU 工作的安全性。大大优化了MCU 的网络功能,尤其对于不能支持OS 的8 bit & 16 bit MCU 的优化提升无疑是革命性的。工程师不需深入了解TCP/IP 协议,而且程序的烧写和移植比较方便,可以大大的缩短产品开发时间。本教程是针对正点原子战舰开发板板载的以太网芯片配套例程,这个以太网芯片采用了“TCP/IP Offload Platform”技术,实现了以太网通讯。
以太网接入单片机方案
在网络项目中,不同的MCU 实现网络接口通信的方式是有所不同的。根据网络接口通信方式的不同归结为两类方案:第一类是传统的软件TCP/IP 协议栈方案;第二类为硬件TCP/IP
协议栈方案。这两种接入方案的介绍如下所示:
- 传统的软件TCP/IP 协议栈以太网接入方案
这种方案由MCU(MAC)+PHY(芯片)实现以太网物理连接,例如:正点原子的探索者、
阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案,该方案的连接示意图如下
图所示:
可以看到,这种方案就是让MCU 内嵌了一个MAC 内核,该内核相当于TCP/IP 协议栈的数据链路层;板载的PHY 芯片相当于TCP/IP 协议栈的物理层;而lwIP 协议栈实现了应用层、传输层和网络层功能。至于lwIP 相关的知识,请读者观看正点原子的《lwIP 开发指南》文档。
接下来笔者带大家来了解一下传统软件TCP/IP 协议栈方案的优缺点,如下所示:优点:
①移植性:可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。
②可造性:可在TCP/IP 协议栈的基础上添加和删除相关功能。
③可扩展性:可扩展到其他领域的应用及开发。
缺点:
①内存方面分析:传统的TCP/IP 方案是移植一个lwIP 的TCP/IP 协议(RAM 50K+,ROM80K+),造成主控可用内存减小。
②从代码量分析:移植lwIP 可能需要的代码量超过40KB,对于有些主控芯片内存匮乏来说无疑是一个严重的问题。
③从运行性能方面分析:由于软件TCP/IP 协议栈方案在通信时候是不断地访问中断机制,造成线程无法运行,如果多线程运行,会使MCU 的工作效率大大降低。
④从安全性方面分析:软件协议栈会很容易遭受网络攻击,造成单片机瘫痪。
2. 硬件TCP/IP 协议栈以太网接入方案
所谓全硬件TCP/IP 协议栈是将传统的软件协议TCP/IP 协议栈用硬件化的逻辑门电路来实现。芯片内部完成TCP、UDP、ICMP 等多种应用层协议,并且实现了物理层以太网控制(MAC+PHY)、内存管理等功能,完成了一整套硬件化的以太网解决方案。该方案的连接示
意图如下图所示:
可以看到,MCU 可以不具备内嵌的MAC 控制器而实现以太网连接,这种方式可减少程序员对TCP/IP 协议的了解,甚至弥补了网络协议安全性不足的短板。全硬件TCP/IP 协议栈方案的优缺点,如下所示:
优点:
①从代码量方面来看:相比于传统的接入已经大大减少了代码量。
②从运行方面来看:极大的减少了中断次数,让单片机更好的完成其他线程的工作。
③从安全性方面来看:硬件化的逻辑门电路来处理TCP/IP 协议是不可被攻击的,也就是说网络攻击和病毒对它无效,这也充分弥补了网络协议安全性不足的短板。
缺点:
①从可扩展性来看:虽然该芯片内部使用逻辑门电路来实现应用层和物理层协议,但是它具有功能局限性,例如给TCP/IP 协议栈添加一个协议,这样它无法快速添加了。
②从收发速率来看:全硬件TCP/IP 协议栈芯片都是采用并口、SPI 以及IIC 等通讯接口来收发数据,这些数据会受通信接口的速率而影响。
总的来说:全硬件TCP / IP 协议栈简化传统的软件TCP / IP 协议栈,卸载了MCU 用于处理TCP / IP 这部分的线程,节约MCU 内部ROM 等硬件资源,工程师只需进行简单的套接字编程和少量的寄存器操作即可方便地进行嵌入式以太网上层应用开发,减少产品开发周期,降低开发成本。
以太网接口芯片CH395Q 简介
CH395Q 是南京沁恒微电子推出的一款高性能以太网芯片,正点原子战舰板载的以太网芯片是以南京沁恒微电子股份有限公司的CH395Q 以太网协议栈管理芯片为核心,该芯片自带了10/100M 以太网介质传输层(MAC)和物理层(PHY),并完全兼容IEEE802.3 10/100M 协议,
内置了UDP、TCP 等以太网协议栈的固件,用于单片机系统进行以太网通讯。它支持间接并行总线和高速SPI 接口2 种方式与主机进行通信。其内部还集成了以太网数据链路层(MAC)和10Base -T / 100Base -T 以太网物理层(PHY),支持自动协商(10 / 100-基于全双工/半双工)。
与传统软件协议栈不同,CH395Q 内嵌的8 个独立硬件套接字可以进行8 路独立通信,该8 路socket 的通信效率互不影响,使用起来十分方便。
CH395Q 以太网协议栈管理芯片的整体框图如下图所示:
可以看出,CH395Q 芯片具备三种通信接口,它内嵌TCP/IP 协议栈并且在其基础上实现了各层间的子协议。本教程的配套例程是使用SPI 通信接口,所以MCU 和CH395Q 芯片交互的引脚数量只有SCS、SCK、MOSI、MISO、RTS 以及INT#。下面笔者使用一个示意图来总结本
小节的内容如下图所示:
从上图可以看出,CH395Q 以太网芯片类似于网卡,我们可通过普通的接口发送数据至CH395Q 以太网芯片当中,这些数据经过以太网芯片处理发送至网络当中。
CH395Q 特性参数
CH395Q 的各项基本参数,如下表所示:
CH 395Q 模块的各项电气参数,如下表所示:
以太网接口芯片CH395Q 命令简介
CH395Q 芯片通过SPI 接口与外接控制器进行数据通讯,外接控制器需要通过命令控制CH395Q 芯片和与CH395Q 芯片进行数据交互,CH395Q 芯片支持的命令有很多,具体的命令和命令详述请见CH395Q 芯片的手册《CH395DS1.PDF》,本文仅介绍几个常用的命令,如下
表所示:
1、CMD_GET_IC_VER——获取芯片及固件版本
该命令用于获取CH395 的芯片及固件版本。该命令会返回1 个字节的版本号数据,其中位7 为0、位6 为1、位5~0 为版本号。
2、CMD_RESET_ALL——执行硬件复位
该命令使CH395 芯片执行硬件复位。通常情况下,硬件复位在50ms 时间之内完成。
3、CMD_CHECK_EXIST——测试通讯接口和工作方式
该命令用于测试通讯接口和工作状态。该命令需要1 个字节的任意数据,如果CH395 正常工作且通讯接口无误,那么将会返回1 个字节的数据,返回的值为输入数据按位取反的结果。
4、CMD_GET_GLOB_INT_STATUS_ALL——获取全局中断状态
该命令用于获取全局中断状态。该命令发送完成时会返回2 个字节的全局中断状态,其定义如下表所示:
GINT_STAT_UNREACH:不可达中断。当CH395 芯片收到ICMP 不可达中断报文后,将不可达IP 数据包的IP 地址、端口、协议类型保存到不可达信息表中,然后产生此中断。
GINT_STAT_IP_CONFLI:IP 冲突中断。当CH395 芯片检测到自身IP 地址和同一网段内的其他网络设备IP 地址相同时,会产生此中断。
GINT_STAT_PHY_CHANGE:PHY 变化中断。当CH395 芯片的PHY 连接变化时产生此中断。
GINT_STAT_DHCP 和GINT_STAT_PPPOE:DHCP 中断和PPPOE 中断共用此中断源。
如果外部主控使能了CH395 的DHCP 功能或PPPOE 功能,CH395 将会产生此中断。
GINT_STAT_SOCK0~GINT_STAT_SOCK7:Socket 中断。当Socket 有中断事件时,CH395芯片会产生此中断。
此命令执行完毕后,CH395 芯片会自动将INT 引脚置为高电平并清除全局中断。
5、CMD_SET_MAC_ADDR——设置MAC 地址
该命令用于设置ATK-MO395Q 模块的MAC 地址。该命令需要输入6 个字节的MAC 地址数据(MAC 地址低字节在前),CH395 芯片会将该MAC 地址保存到内部的EEPROM 中,该命令需要100ms 的执行时间。
注意:CH395 芯片出厂时已经烧录了由IEEE 分配的MAC 地址,如非必要请勿设置MAC地址。
6、CMD_SET_IP_ADDR——设置IP 地址
该命令用于设置ATK-MO395Q 模块的IP 地址。该命令需要输入4 个字节的IP 地址数据
(IP 地址低字节在前)。
7、CMD_SET_GWIP_ADDR——设置网关IP 地址
该命令用于设置ATK-MO395Q 模块的网关IP 地址。该命令需要输入4 个字节的网关IP 地址(网关IP 地址低字节在前)。
8、CMD_SET_MASK_ADDR——设置子网掩码
该命令用于设置ATK-MO395Q 模块的子网掩码。该命令需要输入4 字节的子网掩码数据(子网掩码低字节在前)。
9、CMD_GET_PHY_STATUS——获取PHY 的状态
该命令用于获取ATK-MO395Q 模块的PHY 状态。该命令会返回1 个字节的PHY 状态码数据,其定义如下表所示:
10、CMD_INIT_CH395——初始化CH395 芯片
该命令用于初始化CH395 芯片,初始化的内容包括MAC、PHY 和TCP/IP 协议栈,该命令约需要350ms 的执行时间。
11、CMD_GET_CMD_STATUS——获取命令执行状态
该命令用于获取命令的执行状态。该命令发送完成时会返回1 个字节的状态码数据,其定义如下表所示:
若外部控制器收到CH395_ERR_BUSY 的返回值,表示CH395 正在执行命令,外部主控应延时2ms 以上再获取命令的执行状态。
12、CMD_GET_INT_STATUS_SN——获取Socket 的中断状态
该命令用于获取Socket 的中断状态。该命令需要输入1 个字节的Socket 标号,该命令会返回1 个字节的Socket 中断状态码,其定义如下所示:
SINT_STAT_SENDBUF_FREE:发送缓冲区非空中断。外部控制器向Socket 发送换从去写入数据后,需等待该中断产生,才能再次向Socket 发送缓冲区写入数据。
SINT_STAT_SEND_OK:发送成功中断。当数据包被成功发送后,会产生此中断。外部控制器向Socket 的发送缓冲区写入数据后,CH395 可能会将数据封装成若干个数据包,每成功发送一个数据包,都会产生一次该中断。
SINT_STAT_RECV:接收缓冲区非空中断。当Socket 接收到数据时,会产生该中断。
SINT_STAT_CONNECT:TCP 连接中断。该中断仅在TCP 模式下有效,表明TCP 连接成功,外部控制器必须等待该中断产生才能进行TCP 数据传输。
SINT_STAT_DESCONNECT:TCP 连接断开中断。该中断仅在TCP 模式下有效,表明TCP连接断开。
SINT_STAT_TIM_OUT:超时中断。在TCP 模式下,TCP 连接、断开、发送数据等过程中出现超时,则会产生此中断;IPRAW 和UDP 模式下,发送数据失败也会产生此中断。
13、CMD_SET_IP_ADDR_SN——设置Socket 的目的IP 地址
该命令用于设置Socket 的目的IP 地址。该命令需要输入1 个字节的Socket 标号和4 个字节的目的IP 地址数据(目的IP 地址低字节在前)。
14、CMD_SET_DES_PORT_SN——设置Socket 的目的端口
该命令用于设置Socket 的目的端口。该命令需要输入1 个字节的Socket 标号和2 个字节的目的端口号数据(目的端口号低字节在前)。
15、CMD_SET_SOUR_PORT_SN——设置Socket 的源端口
该命令用于设置Socket 的原端口。该命令需要输入1 个字节的Socket 标号和2 个字节的源端口号数据(源端口号低字节在前)。
16、CMD_SET_PROTO_TYPE_SN——设置Socket 的工作模式
该命令用于设置Socket 的工作模式。该命令需要输入1 个字节的Socket 标号和1 个字节的工作模式码,其定义如下所示:
17、CMD_OPEN_SOCKET_SN——打开Socket
该命令用于打开Socket。该命令需要输入1 个字节的Socket 标号。
18、CMD_TCP_LISTEN_SN——启动Socket 监听
该命令用于使能Socket 进入监听模式(即TCP Server 模式),该命令仅在TCP 模式下有效。该命令需要输入1 个字节的Socket 标号。
19、CMD_TCP_CONNECT_SN——启动Socket 连接
该命令用于使能Socket 进入连接模式(即TCP Client 模式),该命令仅在TCP 模式下有效。该命令需要输入1 个字节的Socket 标号。
20、CMD_TCP_DISCONNECT_SN——断开Socket 的TCP 连接
该命令用于断开Socket 当前的TCP 连接,该命令仅在TCP 模式下有效。该命令需要输入1 个字节的Socket 标号。
21、CMD_WRITE_SEND_BUF_SN——向Socket 发送缓冲区写数据
该命令用于向Socket 的发送缓冲区写入数据。该命令需要输入1 个字节的Socket 标号、2个字节的待写入数据长度(低字节在前)和若干个字节的数据流。
22、CMD_GET_RECV_LEN_SN——获取Socket 接收数据长度
该命令用于获取当前接收缓冲区的有效数据长度。该命令需要输入1 个字节的Socket 标号,该命令会返回2 个字节的有效数据长度数据(低字节在前)。
23、CMD_READ_RECV_BUF_SN——从Socket 接收缓冲区接收数据
该命令用于从Socket 接收缓冲区读取数据。该命令需要输入1 个字节的Socket 标号和2 个字节的读取长度(低字节在前),该命令会返回若干个字节的数据流。
24、CMD_CLOSE_SOCKET_SN——关闭Socket
该命令用于关闭Socket。该命令需要输入1 个字节的Socket 标号。
25、CMD_DHCP_ENABLE——启动(停止)DHCP
该命令用于启动或停止DHCP。该命令需要输入1 个字节的DHCP 使能码,其定义如下表所示:
26、CMD_GET_DHCP_STATUS——获取DHCP 状态
该命令用于获取DHCP 的状态。该命令发送完成时会返回1 个字节的DHCP 状态码,其定义如下表所示:
27、CMD_GET_IP_INF——获取IP、MASK、DNS 等信息
该命令用于获取IP 地址、网关IP 地址、子网掩码、DNS 等信息。该命令会一次返回20 个字节数据,分别为4 个字节IP 地址、4 个字节网关IP 地址、4 个字节子网掩码、4 个字节DNS 服务器1 地址、4 个字节DNS 服务器2 地址。
以太网接口芯片CH395Q 寄存器配置与使用
CH395Q 以太网芯片是使用命令来配置以太网环境,通过通讯接口把配置命令发送至CH395Q 芯片当中,CH395Q 以太网芯片会根据接收的配置命令使能相应的功能。沁恒微电子为CH395Q 以太网芯片配置了一套驱动文件,这些文件定义了配置命令和配置函数,同时,它还提
供了例程源码方便用户学习。该驱动文件下载流程如下所示:
①打开沁恒微电子官方网址。
②在官方网站搜索CH395Q,查找完成之后下载CH395Q 使用文档和例程源码。
下载完成之后我们可以得到以下压缩包。
通过对CH395EVT.ZIP 进行解压可得到以下文件和文件夹,如下图所示:
从上图中,笔者重点讲解EXAMPUB 路径下的CH395CMD.C/H 和CH395INC.H 文件,这些文件就是定义了CH395Q 支持的配置命令和配置函数。配置命令是在CH395INC.H 文件下定义的,而配置函数(CH395CMD.C 文件下定义)就是对配置命令进行封装的,然后通过通讯接口函数发送至CH395Q 以太网芯片当中。CH395CMD.H 文件主要声明CH395CMD.C 文件下的配置函数,提供外部文件使用。
笔者为了兼容正点原子最新的代码格式规范,我们把CH395CMD.C/H 和CH395INC.H 文件进行了修改,主要对这些文件内的变量、函数及注释格式做了全新的改编。
根据上述内容的要求,笔者把CH395CMD.C/H 文件名修改为ch395cmd.c/h,而CH395INC.H文件名修改为ch395inc.h 文件(寄存器命令定义文件),下面我们分别地讲解这些文件定义的内容。
- 文件ch395inc.h
该文件定义了CH395Q 以太网芯片支持的配置命令,我们可以根据它提供的命令来配置以太网芯片的功能,这些配置命令如下源码所示:
#define CMD01_GET_IC_VER 0x01 /* 获取芯片以及固件版本号*/
#define CMD31_SET_BAUDRATE 0x02 /* 串口方式*/
#define CMD00_ENTER_SLEEP 0x03 /* 进入睡眠状态*/
#define CMD00_RESET_ALL 0x05 /* 执行硬件复位*/
#define CMD11_CHECK_EXIST 0x06 /* 测试通讯接口以及工作状态*/
#define CMD02_GET_GLOB_INT_STATUS_ALL 0x19 /* 获取全局中断状态*/
#define CMD10_SET_PHY 0x20 /* 设置PHY,自动协商*/
#define CMD60_SET_MAC_ADDR 0x21 /* 设置MAC地址*/
#define CMD40_SET_IP_ADDR 0x22 /* 设置IP地址*/
#define CMD40_SET_GWIP_ADDR 0x23 /* 设置网关IP地址*/
#define CMD40_SET_MASK_ADDR 0x24 /* 设置子网掩码*/
#define CMD90_SET_MAC_FILT 0x25 /* 设置MAC过滤*/
#define CMD01_GET_PHY_STATUS 0x26 /* 获取PHY当前状态*/
#define CMD0W_INIT_CH395 0x27 /* 初始化CH395 */
#define CMD08_GET_UNREACH_IPPORT 0x28 /* 获取不可达信息*/
#define CMD01_GET_GLOB_INT_STATUS 0x29 /* 获取全局中断状态*/
#define CMD10_SET_RETRAN_COUNT 0x2A /* 重试次数*/
#define CMD20_SET_RETRAN_PERIOD 0x2B /* 重试周期*/
#define CMD01_GET_CMD_STATUS 0x2C /* 获取命令执行状态*/
#define CMD06_GET_REMOT_IPP_SN 0x2D /* 获取远端的端口以及IP地址*/
#define CMD10_CLEAR_RECV_BUF_SN 0x2E /* 清除接收缓冲区*/
#define CMD12_GET_SOCKET_STATUS_SN 0x2F /* 获取socket n状态*/
#define CMD11_GET_INT_STATUS_SN 0x30 /* 获取socket n的中断状态*/
#define CMD50_SET_IP_ADDR_SN 0x31 /* 设置socket n的目的IP地址*/
#define CMD30_SET_DES_PORT_SN 0x32 /* 设置socket n的目的端口*/
#define CMD30_SET_SOUR_PORT_SN 0x33 /* 设置socket n的源端口*/
#define CMD20_SET_PROTO_TYPE_SN 0x34 /* 设置socket n的协议类型*/
#define CMD1W_OPEN_SOCKET_SN 0x35 /* 打开socket n */
#define CMD1W_TCP_LISTEN_SN 0x36 /* socket n监听*/
#define CMD1W_TCP_CONNECT_SN 0x37 /* socket n连接*/
#define CMD1W_TCP_DISNCONNECT_SN 0x38 /* socket n断开连接,*/
#define CMD30_WRITE_SEND_BUF_SN 0x39 /* 向socket n缓冲区写入数据*/
#define CMD12_GET_RECV_LEN_SN 0x3B /* 获取socket n接收数据的长度*/
#define CMD30_READ_RECV_BUF_SN 0x3C /* 读取socket n接收缓冲区数据*/
#define CMD1W_CLOSE_SOCKET_SN 0x3D /* 关闭socket n */
#define CMD20_SET_IPRAW_PRO_SN 0x3E /* 在IP RAW下*/
#define CMD01_PING_ENABLE 0x3F /* 开启/关闭PING */
#define CMD06_GET_MAC_ADDR 0x40 /* 获取MAC地址*/
#define CMD10_DHCP_ENABLE 0x41 /* DHCP使能*/
#define CMD01_GET_DHCP_STATUS 0x42 /* 获取DHCP状态*/
#define CMD014_GET_IP_INF 0x43 /* IP,子网掩码,网关*/
#define CMD00_PPPOE_SET_USER_NAME 0x44 /* 设置PPPOE用户名*/
#define CMD00_PPPOE_SET_PASSWORD 0x45 /* 设置密码*/
#define CMD10_PPPOE_ENABLE 0x46 /* PPPOE使能*/
#define CMD01_GET_PPPOE_STATUS 0x47 /* 获取pppoe状态*/
#define CMD20_SET_TCP_MSS 0x50 /* 设置TCP MSS */
#define CMD20_SET_TTL 0x51 /* 设置TTL,TTL最大值为128 */
#define CMD30_SET_RECV_BUF 0x52 /* 设置SOCKET接收缓冲区*/
#define CMD30_SET_SEND_BUF 0x53 /* 设置SOCKET发送缓冲区*/
#define CMD10_SET_MAC_RECV_BUF 0x54 /* 设置MAC接收缓冲区*/
#define CMD40_SET_FUN_PARA 0x55 /* 设置功能参数*/
#define CMD40_SET_KEEP_LIVE_IDLE 0x56 /* 设置KEEPLIVE空闲*
#define CMD40_SET_KEEP_LIVE_INTVL 0x57 /* 设置间隔时间*/
#define CMD10_SET_KEEP_LIVE_CNT 0x58 /* 重试次数*/
#define CMD20_SET_KEEP_LIVE_SN 0X59 /* 设置socket nkeeplive功能*/
#define CMD00_EEPROM_ERASE 0xE9 /* 擦除EEPROM*/
#define CMD30_EEPROM_WRITE 0xEA /* 写EEPROM */
#define CMD30_EEPROM_READ 0xEB /* 读EEPROM */
#define CMD10_READ_GPIO_REG 0xEC /* 读GPIO寄存器*/
#define CMD20_WRITE_GPIO_REG 0xED /* 写GPIO寄存器*/
相关命令描述请看本章节的“以太网接口芯片CH395Q 命令简介”小节。
2. 文件ch395cmd.c该文件的函数是根据CH395Q 以太网配置命令编写的,下面笔者挑几个重要的函数来讲解,如下源码所示:
(1) 函数ch395_cmd_reset
该函数的作用是硬件复位CH395Q 以太网芯片,如下源码所示:
/**
* @brief 复位ch395芯片
* @param 无
* @retval 无
*/
void ch395_cmd_reset(void)
{
ch395_write_cmd(CMD00_RESET_ALL);
ch395_scs_hign;
}
此函数通过发送CMD00_RESET_ALL 命令硬件复位CH395Q 以太网芯片。
(2) 函数ch395_cmd_check_exist
该函数的作用是测试MUC 与CH395Q 以太网芯片是否正常通讯,如下源码所示:
/**
* @brief 测试命令,用于测试硬件以及接口通讯,
* @param 1字节测试数据
* @retval 硬件ok,返回testdata按位取反
*/
uint8_t ch395_cmd_check_exist(uint8_t testdata)
{
uint8_t i;
ch395_write_cmd(CMD11_CHECK_EXIST);
ch395_write_data(testdata);
i = ch395_read_data();
ch395_scs_hign;
return i;
}
此函数把CMD11_CHECK_EXIST 命令和测试数据发送至CH395Q 以太网芯片中,发送完成之后接收CH395Q 以太网芯片返回的数据,如果MCU 接收的数据是发送数据的反码,则表示通信正常。
3. 文件ch395cmd.h
声明ch395cmd.c 文件下的配置函数,这些声明函数如下源码所示:
#ifndef __CH395CMD_H__
#define __CH395CMD_H__
#include "./BSP/CH395Q/ch395inc.h"
#include "./SYSTEM/sys/sys.h"
/* 复位*/
void ch395_cmd_reset(void);
/* 睡眠*/
void ch395_cmd_sleep(void);
/* 获取芯片及固件版本号*/
uint8_t ch395_cmd_get_ver(void);
/* 测试命令*/
uint8_t ch395_cmd_check_exist(uint8_t testdata);
/* 设置phy状态*/
void ch395_cmd_set_phy(uint8_t phystat);
/* 获取phy状态*/
uint8_t ch395_cmd_get_phy_status(void);
/*********************************省略部分函数*********************************/
#endif
该文件非常简单,它主要声明ch395cmd.c 文件下的配置函数,提供给其他文件调用。
至此,笔者已经讲解完了官方提供的ch395cmd.c/h 文件和ch395inc.h 文件。接下来笔者将讲解官方提供的另外一个重要文件,它们的文件名为CH395.C/H 文件,这些文件在EXAMEXAMx(x:0~15)文件夹找到,是官方提供给用户的例程源码,主要作用是调用ch395cmd.c
文件下的配置函数初始化CH395Q 以太网芯片和配置以太网环境,例如:UDP、TCP、ICMP 等协议。这里笔者会和前面一样对它们进行代码格式修改,首先我们把CH395.C/H 文件名修改为ch395.c/h 文件,接着在ch395.c 文件下封装了ch395_write_cmd 、ch395_read_data 以及
ch395_write_data 函数,这些函数都是调用了SPI1 接口函数发送数据和命令。ch395.c/h 的文件结构,如下所示:
- 文件ch395.c
(1) 声明头文件
#include "./BSP/CH395Q/ch395.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/SPI/spi.h"
(2) 定义网络配置结构体
struct ch395q_t g_ch395q_sta;
该结构体主要保存网络相关的参数及回调函数。
(3) CH395Q 引脚初始化
/**
* @brief ch395_gpio初始化
* @param 无
* @retval 无
*/
void ch395_gpio_init( void )
{
GPIO_InitTypeDef gpio_init_struct;
CH395_SCS_GPIO_CLK_ENABLE(); /* 使能SCS时钟*/
CH395_INT_GPIO_CLK_ENABLE(); /* 使能INT时钟*/
CH395_RST_GPIO_CLK_ENABLE(); /* 使能RST时钟*/
/* SCS */
gpio_init_struct.Pin = CH395_SCS_GPIO_PIN;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推拉输出*/
HAL_GPIO_Init( CH395_SCS_GPIO_PORT, &gpio_init_struct );
/* 初始化中断引脚*/
gpio_init_struct.Pin = CH395_INT_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入*/
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
HAL_GPIO_Init( CH395_INT_GPIO_PORT, &gpio_init_struct );
gpio_init_struct.Pin = CH395_RST_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 输出*/
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
HAL_GPIO_Init( CH395_RST_GPIO_PORT, &gpio_init_struct );
HAL_GPIO_WritePin(CH395_RST_GPIO_PORT, CH395_RST_GPIO_PIN, GPIO_PIN_SET);
delay_ms(20);
}
可以看出,上述的源码只初始化了片选、中断和复位引脚,而SPI 接口我们会在spi.c 驱动文件中定义。
(4) 通信函数
/**
* @brief 硬件SPI输出且输入8个位数据
* @param d:将要送入到ch395的数据
* @retval 无
*/
uint8_t ch395_read_write_byte( uint8_t data )
{
uint8_t rxdata;
rxdata = spi1_read_write_byte(data); /* SPI写入一个CH395Q数据并返回一个数据*/
return rxdata; /* 返回收到的数据*/
}
/**
* @brief 向ch395写命令
* @param 将要写入ch395的命令码
* @retval 无
*/
void ch395_write_cmd( uint8_t mcmd )
{
ch395_scs_hign; /* 防止CS原来为低,先将CS置高*/
ch395_scs_low; /* 命令开始,CS拉低*/
ch395_read_write_byte(mcmd); /* SPI发送命令码*/
delay_ms(2); /* 必要延时,延时1.5uS确保读写周期不小于1.5uS */
}
/**
* @brief 向ch395写数据
* @param 将要写入ch395的数据
* @retval 无
*/
void ch395_write_data( uint8_t mdata )
{
ch395_read_write_byte(mdata); /* SPI发送数据*/
}
/**
* @brief 从ch395读数据
* @param 无
* @retval 返回读取的数据
*/
uint8_t ch395_read_data( void )
{
uint8_t i;
i = ch395_read_write_byte(0xff); /* SPI读数据*/
return i;
}
这些函数最终还是调用了spi1_read_write_byte 函数发送和读取数据。如果读者想了解SPI协议,请参考正点原子的《STM32F103 战舰开发指南》第三十六章节的内容。
(5) 保活定时器设置函数
/**
* @brief ch395_keeplive_set 保活定时器参数设置
* @param 无
* @retval 无
*/
void ch395_keeplive_set(void)
{
ch395_keeplive_cnt(DEF_KEEP_LIVE_CNT);
ch395_keeplive_idle(DEF_KEEP_LIVE_IDLE);
ch395_keeplive_intvl(DEF_KEEP_LIVE_PERIOD);
}
此函数用于TCP 协议的客户端,超出规定的时间就会触发中止连接事件。
(6) 设置socket 参数
/**
* @brief ch395 socket配置
* @param ch395_sokect:Socket配置信息
* @retval 无
*/
uint8_t ch395q_socket_config(ch395_socket * ch395_sokect)
{
if (ch395_sokect == NULL)
{
return 0;
}
if (g_ch395q_sta.dhcp_status == DHCP_UP) /* DHCP获取成功状态*/
{
ch395_sokect->net_info.ip[0] = g_ch395q_sta.ipinf_buf[0];
ch395_sokect->net_info.ip[1] = g_ch395q_sta.ipinf_buf[1];
ch395_sokect->net_info.ip[2] = g_ch395q_sta.ipinf_buf[2];
ch395_sokect->net_info.ip[3] = g_ch395q_sta.ipinf_buf[3];
ch395_sokect->net_info.gwip[0] = g_ch395q_sta.ipinf_buf[4];
ch395_sokect->net_info.gwip[1] = g_ch395q_sta.ipinf_buf[5];
ch395_sokect->net_info.gwip[2] = g_ch395q_sta.ipinf_buf[6];
ch395_sokect->net_info.gwip[3] = g_ch395q_sta.ipinf_buf[7];
ch395_sokect->net_info.mask[0] = g_ch395q_sta.ipinf_buf[8];
ch395_sokect->net_info.mask[1] = g_ch395q_sta.ipinf_buf[9];
ch395_sokect->net_info.mask[2] = g_ch395q_sta.ipinf_buf[10];
ch395_sokect->net_info.mask[3] = g_ch395q_sta.ipinf_buf[11];
ch395_sokect->net_info.dns1[0] = g_ch395q_sta.ipinf_buf[12];
ch395_sokect->net_info.dns1[1] = g_ch395q_sta.ipinf_buf[13];
ch395_sokect->net_info.dns1[2] = g_ch395q_sta.ipinf_buf[14];
ch395_sokect->net_info.dns1[3] = g_ch395q_sta.ipinf_buf[15];
ch395_sokect->net_info.dns2[0] = g_ch395q_sta.ipinf_buf[16];
ch395_sokect->net_info.dns2[1] = g_ch395q_sta.ipinf_buf[17];
ch395_sokect->net_info.dns2[2] = g_ch395q_sta.ipinf_buf[18];
ch395_sokect->net_info.dns2[3] = g_ch395q_sta.ipinf_buf[19];
}
else /* DHCP获取失败状态,设置静态IP地址信息*/
{
/* 设置CH395的IP地址*/
ch395_cmd_set_ipaddr(ch395_sokect->net_config.ipaddr);
/* 设置网关地址*/
ch395_cmd_set_gw_ipaddr(ch395_sokect->net_config.gwipaddr);
/* 设置子网掩码,默认为255.255.255.0*/
ch395_cmd_set_maskaddr(ch395_sokect->net_config.maskaddr);
ch395_cmd_init();
delay_ms(100);
}
ch395_cmd_set_macaddr(ch395_sokect->net_config.macaddr); /* 设置MAC地址*/
memcpy(&g_ch395q_sta.socket[ch395_sokect->socket_index].config,
ch395_sokect, sizeof(ch395_socket));
switch(ch395_sokect->proto)
{
case CH395Q_SOCKET_UDP:
/* socket 为UDP模式*/
/* 设置socket 0目标IP地址*/
ch395_set_socket_desip(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].des_ip);
/* 设置socket 0协议类型*/
ch395_set_socket_prot_type(ch395_sokect->socket_index,
PROTO_TYPE_UDP);
/* 设置socket 0目的端口*/
ch395_set_socket_desport(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].des_port);
/* 设置socket 0源端口*/
ch395_set_socket_sourport(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].sour_port);
/* 检查是否成功*/
g_ch395q_sta.ch395_error
(ch395_open_socket(ch395_sokect->socket_index));
break;
case CH395Q_SOCKET_TCP_CLIENT:
/* socket 为TCPClient模式*/
ch395_keeplive_set(); /* 保活设置*/
/* 设置socket 0目标IP地址*/
ch395_set_socket_desip(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].des_ip);
/* 设置socket 0协议类型*/
ch395_set_socket_prot_type(ch395_sokect->socket_index,
PROTO_TYPE_TCP);
/* 设置socket 0目的端口*/
ch395_set_socket_desport(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].des_port);
/* 设置socket 0源端口*/
ch395_set_socket_sourport(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].sour_port);
/* 检查sokect是否打开成功*/
g_ch395q_sta.ch395_error
(ch395_open_socket(ch395_sokect->socket_index));
/* 检查tcp连接是否成功*/
g_ch395q_sta.ch395_error
(ch395_tcp_connect(ch395_sokect->socket_index));
break;
case CH395Q_SOCKET_TCP_SERVER:
/* socket 为TCPServer模式*/
/* 设置socket 0目标IP地址*/
ch395_set_socket_desip(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].des_ip);
/* 设置socket 0协议类型*/
ch395_set_socket_prot_type(ch395_sokect->socket_index,
PROTO_TYPE_TCP);
/* 设置socket 0源端口*/
ch395_set_socket_sourport(ch395_sokect->socket_index,
ch395_sokect[ch395_sokect->socket_index].sour_port);
/* 检查sokect是否打开成功*/
g_ch395q_sta.ch395_error
(ch395_open_socket(ch395_sokect->socket_index));
/* 监听tcp连接*/
g_ch395q_sta.ch395_error
(ch395_tcp_listen(ch395_sokect->socket_index));
break;
}
return 1;
}
通过传入的控制块判断协议的类型,程序根据这个协议的类型执行相应的代码。
(7) 检测错误函数
/**
* @brief 调试使用,显示错误代码,并停机
* @param ierror 检测命令
* @retval 无
*/
void ch395_error(uint8_t ierror)
{
if (ierror == CMD_ERR_SUCCESS)
{
return; /* 操作成功*/
}
#if CH395_DEBUG
printf("Error: %02X
", (uint16_t)ierror); /* 显示错误*/
#endif
while ( 1 )
{
delay_ms(200);
delay_ms(200);
}
}
这个函数没有什么好讲解的,主要用来判断传入的形参是否为CMD_ERR_SUCCESS,若
传入的形参不为CMD_ERR_SUCCESS,则程序会在while 语句内运行。
(8) 检测PHY 状态函数
/**
* @brief CH395 PHY状态
* @param phy_status:PHY状态值
* @retval 无
*/
void ch395_phy_status(uint8_t phy_status)
{
switch (phy_status)
{
case PHY_DISCONN:
printf("PHY DISCONN
");
break;
case PHY_10M_FLL:
printf("PHY 10M_FLL
");
break;
case PHY_10M_HALF:
printf("PHY 10M_HALF
");
break;
case PHY_100M_FLL:
printf("PHY 100M_FLL
");
break;
case PHY_100M_HALF:
printf("PHY 100M_HALF
");
break;
default:
printf("PHY AUTO
");
break;
}
}
此函数根据传入的形参判断PHY 处于那种状态,并以串口输出。
(9) 设置TCPClient 保活定时参数
/**
* @brief ch395_tcp初始化
* @param 无
* @retval 无
*/
void ch395_hardware_init(void)
{
uint8_t i;
ch395_gpio_init();
spi1_init();
g_ch395q_sta.ch395_error = ch395_error;
g_ch395q_sta.ch395_phy_cb = ch395_phy_status;
g_ch395q_sta.ch395_reconnection = ch395_reconnection;
g_ch395q_sta.dhcp_status = DHCP_STA;
i = ch395_cmd_check_exist(0x65); /* 测试命令,用于测试硬件以及接口通讯*/
if (i != 0x9a)
{
g_ch395q_sta.ch395_error(i); /* ch395q检测错误*/
}
ch395_cmd_reset(); /* 对ch395q复位*/
delay_ms(100); /* 这里必须等待100以上延时*/
g_ch395q_sta.ch395_error(ch395_cmd_init()); /* 初始化ch395q命令*/
do
{
g_ch395q_sta.phy_status = ch395_cmd_get_phy_status()/* 获取PHY状态*/
g_ch395q_sta.ch395_phy_cb(g_ch395q_sta.phy_status); /* 判断双工和网速模式*/
}
while(g_ch395q_sta.phy_status == PHY_DISCONN);
g_ch395q_sta.version = ch395_cmd_get_ver(); /* 获取版本*/
printf("CH395VER : %2x
", (uint16_t)g_ch395q_sta.version);
i = ch395_dhcp_enable(1); /* 开启DHCP */
g_ch395q_sta.ch395_error(i); /* ch395q检测错误*/
delay_ms(1000); /* ch395q初始化延时*/
}
此函数对CH395Q 以太网芯片初始化和相关测试,测试完成之后开启DHCP。
(10) CH395Q 全局中断
/**
* @brief CH395 socket 中断,在全局中断中被调用
* @param sockindex (0~7)
* @retval 无
*/
void ch395_socket_interrupt(uint8_t sockindex)
{
uint8_t sock_int_socket;
uint16_t rx_len = 0;
/* 获取socket 的中断状态*/
sock_int_socket = ch395_get_socket_int(sockindex);
/* 发送缓冲区空闲,可以继续写入要发送的数据*/
if (sock_int_socket & SINT_STAT_SENBUF_FREE)
{
}
if (sock_int_socket & SINT_STAT_SEND_OK) /* 发送完成中断*/
{
}
if (sock_int_socket & SINT_STAT_RECV) /* 接收中断*/
{
/* 获取当前缓冲区内数据长度*/
g_ch395q_sta.socket[sockindex].config.recv.size
= ch395_get_recv_length(sockindex);
rx_len = g_ch395q_sta.socket[sockindex].config.recv.size;
/* 读取数据*/
ch395_get_recv_data(sockindex, rx_len,
g_ch395q_sta.socket[sockindex].config.recv.buf);
g_ch395q_sta.socket[sockindex].config.recv.buf[rx_len] = '