网站建设大数据,网页设计培训班需要多久,wordpress 博客host,wordpress评论邮件插件目录 全硬件的TCP/IP 协议栈简介以太网接入单片机方案以太网接口芯片CH395Q 简介以太网接口芯片CH395Q 命令简介以太网接口芯片CH395Q 寄存器配置与使用移植CH395Q 源码 TCP_Client 实验TCPClient 配置流程TCPClient 实验硬件设计程序设计下载验证 WebServer 实验WebServer 简介… 目录 全硬件的TCP/IP 协议栈简介以太网接入单片机方案以太网接口芯片CH395Q 简介以太网接口芯片CH395Q 命令简介以太网接口芯片CH395Q 寄存器配置与使用移植CH395Q 源码 TCP_Client 实验TCPClient 配置流程TCPClient 实验硬件设计程序设计下载验证 WebServer 实验WebServer 简介WebServer 实验硬件设计软件设计下载验证 NTP 实时时间实验NTP 简介NTP 实验硬件设计软件设计下载验证 基于MQTT 协议连接OneNET 服务器移植 MQTT 协议配置OneNET 平台工程配置基于OneNET 平台MQTT 实验硬件设计软件设计下载验证 原子云平台连接原子云工作流程原子云连接实验硬件设计软件设计下载验证 全硬件的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 协议栈方案
1. 传统的软件TCP/IP 协议栈以太网接入方案
这种方案由MCUMACPHY芯片实现以太网物理连接例如正点原子的探索者、阿波罗、北极星以及电机开发板都是采用这类型的以太网接入方案该方案的连接示意图如下图所示 MCU 内嵌了一个MAC 内核该内核相当于TCP/IP 协议栈的数据链路层板载的PHY 芯片相当于TCP/IP 协议栈的物理层lwIP 协议栈实现了应用层、传输层和网络层功能。
传统软件TCP/IP 协议栈方案的优点
①移植性可在不同平台、不同编译环境的程序代码经过修改转移到自己的系统中运行。 ②可造性可在TCP/IP 协议栈的基础上添加和删除相关功能。 ③可扩展性可扩展到其他领域的应用及开发。
缺点
①内存方面分析传统的TCP/IP 方案是移植一个lwIP 的TCP/IP 协议RAM 50KROM80K造成主控可用内存减小。 ②从代码量分析移植lwIP 可能需要的代码量超过40KB对于有些主控芯片内存匮乏来说无疑是一个严重的问题。 ③从运行性能方面分析由于软件TCP/IP 协议栈方案在通信时候是不断地访问中断机制造成线程无法运行如果多线程运行会使MCU 的工作效率大大降低。 ④从安全性方面分析软件协议栈会很容易遭受网络攻击造成单片机瘫痪。
2. 硬件TCP/IP 协议栈以太网接入方案
所谓全硬件TCP/IP 协议栈是将传统的软件协议TCP/IP 协议栈用硬件化的逻辑门电路来实现。芯片内部完成TCP、UDP、ICMP 等多种应用层协议并且实现了物理层以太网控制MACPHY、内存管理等功能完成了一整套硬件化的以太网解决方案。该方案的连接示意图如下图所示 可以看到MCU 可以不具备内嵌的MAC 控制器而实现以太网连接这种方式可减少程序员对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_CONFLIIP 冲突中断。当CH395 芯片检测到自身IP 地址和同一网段内的其他网络设备IP 地址相同时会产生此中断。 GINT_STAT_PHY_CHANGEPHY 变化中断。当CH395 芯片的PHY 连接变化时产生此中断。 GINT_STAT_DHCP 和GINT_STAT_PPPOEDHCP 中断和PPPOE 中断共用此中断源。 如果外部主控使能了CH395 的DHCP 功能或PPPOE 功能CH395 将会产生此中断。 GINT_STAT_SOCK0~GINT_STAT_SOCK7Socket 中断。当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_CONNECTTCP 连接中断。该中断仅在TCP 模式下有效表明TCP 连接成功外部控制器必须等待该中断产生才能进行TCP 数据传输。 SINT_STAT_DESCONNECTTCP 连接断开中断。该中断仅在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 进行解压可得到以下文件和文件夹如下图所示 从上图中笔者重点讲解EXAM\PUB 路径下的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/hCH395INC.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 /* 设置TTLTTL最大值为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 命令简介”小节。
文件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 接收的数据是发送数据的反码则表示通信正常。
文件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 文件这些文件在EXAM\EXAMxx0~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_sokectSocket配置信息* 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_DEBUGprintf(Error: %02X\r\n, (uint16_t)ierror); /* 显示错误*/
#endifwhile ( 1 ){delay_ms(200);delay_ms(200);}
}这个函数没有什么好讲解的主要用来判断传入的形参是否为CMD_ERR_SUCCESS若 传入的形参不为CMD_ERR_SUCCESS则程序会在while 语句内运行。
(8) 检测PHY 状态函数
/*** brief CH395 PHY状态* param phy_statusPHY状态值* retval 无*/
void ch395_phy_status(uint8_t phy_status)
{switch (phy_status){case PHY_DISCONN:printf(PHY DISCONN\r\n);break;case PHY_10M_FLL:printf(PHY 10M_FLL\r\n);break;case PHY_10M_HALF:printf(PHY 10M_HALF\r\n);break;case PHY_100M_FLL:printf(PHY 100M_FLL\r\n);break;case PHY_100M_HALF:printf(PHY 100M_HALF\r\n);break;default:printf(PHY AUTO\r\n);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\r\n, (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] \0;printf(%s, g_ch395q_sta.socket[sockindex].config.recv.buf);}if (sock_int_socket SINT_STAT_CONNECT) /* 连接中断仅在TCP模式下有效*/{if (g_ch395q_sta.socket[sockindex].config.proto CH395Q_SOCKET_TCP_CLIENT){ch395_set_keeplive(sockindex,1); /* 打开KEEPALIVE保活定时器*/ch395_setttl_num(sockindex,60); /* 设置TTL */}}if (sock_int_socket SINT_STAT_DISCONNECT) /* 断开中断仅在TCP模式下有效*/{g_ch395q_sta.ch395_error(ch395_open_socket(g_ch395q_sta.socket[sockindex].config.socket_index));switch(g_ch395q_sta.socket[sockindex].config.proto){case CH395Q_SOCKET_TCP_CLIENT:g_ch395q_sta.ch395_error(ch395_tcp_connect(g_ch395q_sta.socket[sockindex].config.socket_index));break;case CH395Q_SOCKET_TCP_SERVER:g_ch395q_sta.ch395_error(ch395_tcp_listen(g_ch395q_sta.socket[sockindex].config.socket_index));break;default:break;}delay_ms(200); /* 延时200MS后再次重试没有必要过于频繁连接*/}if (sock_int_socket SINT_STAT_TIM_OUT) /* 超时中断仅在TCP模式下有效*/{if (g_ch395q_sta.socket[sockindex].config.proto CH395Q_SOCKET_TCP_CLIENT){delay_ms(200); /* 延时200MS后再次重试没有必要过于频繁连接*/g_ch395q_sta.ch395_error(ch395_open_socket(g_ch395q_sta.socket[sockindex].config.socket_index));g_ch395q_sta.ch395_error(ch395_tcp_connect(g_ch395q_sta.socket[sockindex].config.socket_index));}}
}根据sockindex 形参的数值来选择scoket 通道本教程的例程使用的是scoket0 通道所以该变量的数值为0。我们通过函数ch395_get_socket_int 获取PHY 当前状态并且根据状态执行相应的代码段。
(11) socket 中断
/*** brief CH395全局中断函数* param 无* retval 无*/
void ch395_interrupt_handler(void)
{uint16_t init_status;uint8_t i;init_status ch395_cmd_get_glob_int_status_all();/* 不可达中断读取不可达信息*/if (init_status GINT_STAT_UNREACH){ch395_cmd_get_unreachippt(g_ch395q_sta.ipinf_buf);}/* 产生IP冲突中断建议重新修改CH395的IP并初始化CH395 */if (init_status GINT_STAT_IP_CONFLI){}/* 产生PHY改变中断*/if (init_status GINT_STAT_PHY_CHANGE){g_ch395q_sta.phy_status ch395_cmd_get_phy_status(); /* 获取PHY状态*/}/* 处理DHCP中断*/if (init_status GINT_STAT_DHCP){i ch395_get_dhcp_status();switch (i){case DHCP_UP:ch395_get_ipinf(g_ch395q_sta.ipinf_buf);printf(IP:%02d.%02d.%02d.%02d\r\n,(uint16_t)g_ch395q_sta.ipinf_buf[0],(uint16_t)g_ch395q_sta.ipinf_buf[1],(uint16_t)g_ch395q_sta.ipinf_buf[2],(uint16_t)g_ch395q_sta.ipinf_buf[3]);printf(GWIP:%02d.%02d.%02d.%02d\r\n,(uint16_t)g_ch395q_sta.ipinf_buf[4],(uint16_t)g_ch395q_sta.ipinf_buf[5],(uint16_t)g_ch395q_sta.ipinf_buf[6],(uint16_t)g_ch395q_sta.ipinf_buf[7]);printf(Mask:%02d.%02d.%02d.%02d\r\n,(uint16_t)g_ch395q_sta.ipinf_buf[8],(uint16_t)g_ch395q_sta.ipinf_buf[9],(uint16_t)g_ch395q_sta.ipinf_buf[10],(uint16_t)g_ch395q_sta.ipinf_buf[11]);printf(DNS1:%02d.%02d.%02d.%02d\r\n,(uint16_t)g_ch395q_sta.ipinf_buf[12],(uint16_t)g_ch395q_sta.ipinf_buf[13],(uint16_t)g_ch395q_sta.ipinf_buf[14],(uint16_t)g_ch395q_sta.ipinf_buf[15]);printf(DNS2:%02d.%02d.%02d.%02d\r\n,(uint16_t)g_ch395q_sta.ipinf_buf[16],(uint16_t)g_ch395q_sta.ipinf_buf[17],(uint16_t)g_ch395q_sta.ipinf_buf[18],(uint16_t)g_ch395q_sta.ipinf_buf[19]);g_ch395q_sta.dhcp_status DHCP_UP;break;default:g_ch395q_sta.dhcp_status DHCP_DOWN;/* 设置默认IP地址信息*/printf(静态IP信息.....................................\r\n);break;}}if (init_status GINT_STAT_SOCK0){ch395_socket_interrupt(CH395Q_SOCKET_0); /* 处理socket 0中断*/}if (init_status GINT_STAT_SOCK1){ch395_socket_interrupt(CH395Q_SOCKET_1); /* 处理socket 1中断*/}if (init_status GINT_STAT_SOCK2){ch395_socket_interrupt(CH395Q_SOCKET_2); /* 处理socket 2中断*/}if (init_status GINT_STAT_SOCK3){ch395_socket_interrupt(CH395Q_SOCKET_3); /* 处理socket 3中断*/}{ch395_socket_interrupt(CH395Q_SOCKET_4); /* 处理socket 4中断*/}if (init_status GINT_STAT_SOCK5){ch395_socket_interrupt(CH395Q_SOCKET_5); /* 处理socket 5中断*/}if (init_status GINT_STAT_SOCK6){ch395_socket_interrupt(CH395Q_SOCKET_6); /* 处理socket 6中断*/}if (init_status GINT_STAT_SOCK7){ch395_socket_interrupt(CH395Q_SOCKET_7); /* 处理socket 7中断*/}
}此函数是根据INT#引脚的电平而调用如果INT#引脚的电平为低电平则CH395Q 触发了一个中断。它通过ch395_cmd_get_glob_int_status_all 函数获取全局中断状态根据这个中断状态执行相应的功能。
(12) CH395 全局管理函数
/**
* brief CH395全局管理函数
* param 无
* retval 无
*/
void ch395q_handler(void)
{if (ch395_int_pin_wire 0){ch395_interrupt_handler(); /* 中断处理函数*/}g_ch395q_sta.ch395_reconnection(); /* 检测PHY状态并重新连接*/
}可以看到此函数先判断INT#的电平如果为低电平则调用ch395_interrupt_handler 函数处理接着调用g_ch395q_sta.ch395_reconnection 函数检测PHY 状态若PHY 状态为断开则关闭socket 通道直到网线重新插入才打开socket 通道同时触发重连函数。
(13) 重接机制
/*** brief 检测PHY状态并重新连接* param 无* retval 无*/
void ch395_reconnection(void)
{for (uint8_t socket_index CH395Q_SOCKET_0 ;socket_index CH395Q_SOCKET_7 ; socket_index ){if (g_ch395q_sta.phy_status PHY_DISCONN (g_ch395q_sta.dhcp_status DHCP_UP|| g_ch395q_sta.dhcp_status DHCP_DOWN)){if (g_ch395q_sta.socket[socket_index].config.socket_enable CH395Q_ENABLE){ch395_close_socket(g_ch395q_sta.socket[socket_index].config.socket_index);/* ch395q检测错误*/g_ch395q_sta.ch395_error(ch395_dhcp_enable(0));g_ch395q_sta.socket[socket_index].config.socket_enable CH395Q_DISABLE;g_ch395q_sta.dhcp_status DHCP_STA;}}else{if (g_ch395q_sta.phy_status ! PHY_DISCONN g_ch395q_sta.socket[socket_index].config.socket_enable CH395Q_DISABLE){ch395_cmd_reset(); /* 对ch395q复位*/delay_ms(100); /* 这里必须等待100以上延时*/g_ch395q_sta.ch395_error(ch395_cmd_init()); /* 初始化ch395q命令*//* 开启DHCP */g_ch395q_sta.ch395_error(ch395_dhcp_enable(1));do{if (ch395_int_pin_wire 0){ch395_interrupt_handler();/* 中断处理函数*/}}while (g_ch395q_sta.dhcp_status DHCP_STA); /* 获取DHCP */switch(g_ch395q_sta.socket[socket_index].config.proto){case CH395Q_SOCKET_UDP:/* socket 为UDP模式*//* 设置socket 0目标IP地址*/ch395_set_socket_desip(socket_index,g_ch395q_sta.socket[socket_index].config.des_ip);/* 设置socket 0协议类型*/ch395_set_socket_prot_type(socket_index, PROTO_TYPE_UDP);/* 设置socket 0目的端口*/ch395_set_socket_desport(socket_index,g_ch395q_sta.socket[socket_index].config.des_port);/* 设置socket 0源端口*/ch395_set_socket_sourport(socket_index,g_ch395q_sta.socket[socket_index].config.sour_port);/* 检查是否成功*/g_ch395q_sta.ch395_error(ch395_open_socket(socket_index));break;case CH395Q_SOCKET_TCP_CLIENT:/* socket 为TCPClient模式*/ch395_keeplive_set(); /* 保活设置*//* 设置socket 0目标IP地址*/ch395_set_socket_desip(socket_index,g_ch395q_sta.socket[socket_index].config.des_ip);/* 设置socket 0协议类型*/ch395_set_socket_prot_type(socket_index, PROTO_TYPE_TCP);/* 设置socket 0目的端口*/ch395_set_socket_desport(socket_index,g_ch395q_sta.socket[socket_index].config.des_port);/* 设置socket 0源端口*/ch395_set_socket_sourport(socket_index,g_ch395q_sta.socket[socket_index].config.sour_port);/* 检查sokect是否打开成功*/g_ch395q_sta.ch395_error(ch395_open_socket(socket_index));/* 检查tcp连接是否成功*/g_ch395q_sta.ch395_error(ch395_tcp_connect(socket_index));break;case CH395Q_SOCKET_TCP_SERVER:/* socket 为TCPServer模式*//* 设置socket 0目标IP地址*/ch395_set_socket_desip(socket_index,g_ch395q_sta.socket[socket_index].config.des_ip);/* 设置socket 0协议类型*/ch395_set_socket_prot_type(socket_index, PROTO_TYPE_TCP);/* 设置socket 0源端口*/ch395_set_socket_sourport(socket_index,g_ch395q_sta.socket[socket_index].config.sour_port);/* 检查sokect是否打开成功*/g_ch395q_sta.ch395_error(ch395_open_socket(socket_index));/* 监听tcp连接*/g_ch395q_sta.ch395_error(ch395_tcp_listen(socket_index));break;}g_ch395q_sta.socket[0].config.socket_enable CH395Q_ENABLE;}}}
}此函数主要判断PHY 状态如果网线断开则等待网线重新插入那一刻触发重连机制。
文件ch395.h
#ifndef __CH395_H
#define __CH395_H
#include ./BSP/CH395Q/ch395inc.h
#include ./SYSTEM/sys/sys.h
#include ./SYSTEM/usart/usart.h
#include ./BSP/CH395Q/ch395inc.h
#include ./BSP/CH395Q/ch395cmd.h
#include ./SYSTEM/delay/delay.h
#include string.h
#include stdio.h
/*****************************************************************************/
/* 引脚定义*/
#define CH395_SCS_GPIO_PORT GPIOG
#define CH395_SCS_GPIO_PIN GPIO_PIN_9
#define CH395_SCS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0) /* PG口时钟使能*/
#define CH395_INT_GPIO_PORT GPIOG
#define CH395_INT_GPIO_PIN GPIO_PIN_6
#define CH395_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0) /* PG口时钟使能*/
#define CH395_RST_GPIO_PORT GPIOD
#define CH395_RST_GPIO_PIN GPIO_PIN_7
#define CH395_RST_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) /* PD口时钟使能*/
/*****************************************************************************/
/* SPI片选引脚输出低电平*/
#define ch395_scs_low HAL_GPIO_WritePin(GPIOG, GPIO_PIN_9, GPIO_PIN_RESET)
/* SPI片选引脚输出高电平*/
#define ch395_scs_hign HAL_GPIO_WritePin(GPIOG, GPIO_PIN_9, GPIO_PIN_SET)
/* 获取CH395的SPI数据输出引脚电平*/
#define ch395_sdo_pin HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_6)
/* 假定CH395的INT#引脚,如果未连接那么也可以通过查询兼做中断输出的SDO引脚状态实现*/
#define ch395_int_pin_wire HAL_GPIO_ReadPin(GPIOG,GPIO_PIN_6)
typedef struct ch395q_socket_t
{uint8_t socket_enable; /* Socket使能*/uint8_t socket_index; /* Socket标号*/uint8_t proto; /* Socket协议*/uint8_t des_ip[4]; /* 目的IP地址*/uint16_t des_port; /* 目的端口*/uint16_t sour_port; /* 源端口*/struct{uint8_t *buf; /* 缓冲空间*/uint32_t size; /* 缓冲空间大小*/} send; /* 发送缓冲*/struct{uint8_t *buf; /* 缓冲空间*/uint32_t size; /* 缓冲空间大小*/} recv; /* 接收缓冲*/struct{uint8_t ip[4]; /* IP地址*/uint8_t gwip[4]; /* 网关IP地址*/uint8_t mask[4]; /* 子网掩码*/uint8_t dns1[4]; /* DNS服务器1地址*/uint8_t dns2[4]; /* DNS服务器2地址*/} net_info; /* 网络信息*/struct{uint8_t ipaddr[4]; /* IP地址32bit*/uint8_t gwipaddr[4]; /* 网关地址32bit*/uint8_t maskaddr[4]; /* 子网掩码32bit*/uint8_t macaddr[6]; /* MAC地址48bit*/} net_config; /* 网络配置信息*/
} ch395_socket;
/* DHCP状态*/
enum DHCP
{DHCP_UP 0, /* DHCP获取成功状态*/DHCP_DOWN, /* DHCP获取失败状态*/DHCP_STA, /* DHCP开启状态*/
};
struct ch395q_t
{uint8_t version; /* 版本信息*/uint8_t phy_status; /* PHY状态*/uint8_t dhcp_status; /* DHCP状态*/uint8_t ipinf_buf[20]; /* 获取IP信息*/struct{ch395_socket config; /* 配置信息*/} socket[8]; /* Socket状态*/void (*ch395_error)(uint8_t i); /* ch395q错误检测函数*/void (*ch395_phy_cb)(uint8_t phy_status); /* ch395q phy状态回调函数*/void (*ch395_reconnection)(void); /* ch395q 重新连接函数*/
};
extern struct ch395q_t g_ch395q_sta;
/* CH395Q模块Socket标号定义*/
#define CH395Q_SOCKET_0 0 /* Socket 0 */
#define CH395Q_SOCKET_1 1 /* Socket 1 */
#define CH395Q_SOCKET_2 2 /* Socket 2 */
#define CH395Q_SOCKET_3 3 /* Socket 3 */
#define CH395Q_SOCKET_4 4 /* Socket 4 */
#define CH395Q_SOCKET_5 5 /* Socket 5 */
#define CH395Q_SOCKET_6 6 /* Socket 6 */
#define CH395Q_SOCKET_7 7 /* Socket 7 */
/* 使能定义*/
#define CH395Q_DISABLE 1 /* 禁用*/
#define CH395Q_ENABLE 2 /* 使能*/
/* CH395Q模块Socket协议类型定义*/
#define CH395Q_SOCKET_UDP 0 /* UDP */
#define CH395Q_SOCKET_TCP_CLIENT 1 /* TCP客户端*/
#define CH395Q_SOCKET_TCP_SERVER 2 /* TCP服务器*/
#define DEF_KEEP_LIVE_IDLE (15*1000) /* 空闲时间*/
/* 间隔为15秒发送一次KEEPLIVE数据包*/
#define DEF_KEEP_LIVE_PERIOD (15*1000)
#define DEF_KEEP_LIVE_CNT 200
uint8_t ch395_read_data(void ) ;
void ch395_write_cmd( uint8_t mcmd );
void ch395_write_data( uint8_t mdata );
void ch395q_handler(void);
void ch395_interrupt_handler(void);
void ch395_hardware_init(void);
uint8_t ch395q_socket_config(ch395_socket * ch395_sokect);
void ch395_reconnection(void);
#endif此函数主要声明ch395.c 中的函数和结构体这些函数和结构体可在其他文件中调用。至此ch395.c/h 文件介绍完成。
移植CH395Q 源码
移植前我们需要一个基础工程这里我们使用裸机例程中的内存管理实验作为基础工程我们在这个工程的基础上完成本章的CH395Q 移植。
首先我们把内存管理实验重命名为“网络实验1 CH395_DHCP 实验”工程然后在该工程的Drivers\BSP 文件夹下创建CH395Q 文件夹并在此文件夹添加ch395.c/h、ch395cmd.c/.h 和ch395inc.h 文件这些文件我们可在正点原子移植好的例程中获取如下图所示 可以看到CH395Q 文件夹下保存着与CH395Q 相关的源码接着我们在工程的Drivers/BSP分组添加上图的.c 文件如下图所示 前面笔者也讲到本教程配套的例程使用的是SPI 接口通讯所以我们需要添加SPI 驱动文件这些文件可在战舰“实验24 SPI 实验”下获取并且把它复制到本实验的Drivers\BSP 路径下同时在工程的Drivers/BSP 分组上添加该驱动文件如下图所示 值得注意的是该驱动文件的SPI 协议是使用SPI2 接口实现的而正点原子战舰开发板板载的CH395Q 使用的是SPI1 接口通讯所以我们把该驱动接口修改为SPI1 接口修改过程很简单这里笔者无需讲解了大家请参考移植好的例程。
本教程是以标准例程-HAL 库版本的内存管理实验工程为基础工程所以内存管理实验工程的工程名为“MALLOC”为了规范工程笔者建议将工程的目标名修改为“CH395Q”或根据读者的实际场景进行修改修改如下图所示 到了这里我们已经移植完成编译工程如果出现下图的错误则添加HAL 驱动文件。 上图中的错误很容易解决我们在工程的Drivers/STM32F1xx_HAL_Driver 分组上添加Drivers\STM32F1xx_HAL_Driver\Src 路径下的stm32f1xx_hal_spi.c 文件即可解决。再一次编译就不会出现错误和警告了。
为了工程整洁性笔者把Drivers\BSP 路径下未使用的驱动文件一并删除了如下图所示 为了代码整体简洁性我们在工程上创建ch395_demo.c/h 文件并把它们保存在User\APP路径下同时在工程的User 分组下添加ch395_demo.c 文件。
至此我们已经移植完毕下面我们来测试一下网络是否正常。首先我们在main.c 文件中编写测试代码该测试代码如下所示
#include ./SYSTEM/sys/sys.h
#include ./SYSTEM/usart/usart.h
#include ./SYSTEM/delay/delay.h
#include ./BSP/LED/led.h
#include ./BSP/LCD/lcd.h
#include ./BSP/KEY/key.h
#include ./MALLOC/malloc.h
#include ./BSP/SRAM/sram.h
#include ./BSP/CH395Q/ch395.h
#include ./APP/ch395_demo.h
int main(void)
{HAL_Init(); /* 初始化HAL库*/sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */delay_init(72); /* 延时初始化*/usart_init(115200); /* 串口初始化为115200 */led_init(); /* 初始化LED */lcd_init(); /* 初始化LCD */key_init(); /* 初始化按键*/sram_init(); /* 初始化外部SRAM */my_mem_init(SRAMIN); /* 初始化内部内存*/my_mem_init(SRAMEX); /* 初始化外部内存*/ch395_hardware_init(); /* ch395初始化*/ch395_demo(); /* 例程测试*/
}main 函数调用了ch395_hardware_init 函数初始化CH395Q接着调用ch395_demo 执行测试代码该函数如下所示
/*** brief 显示实验信息* param 无* retval 无*/
void ch395_show_mesg(void){/* LCD显示实验信息*/lcd_show_string(10, 10, 220, 32, 32, STM32, RED);lcd_show_string(10, 47, 220, 24, 24, CH395Q Client, RED);lcd_show_string(10, 76, 220, 16, 16, ATOMALIENTEK, RED);lcd_show_string(10, 97, 200, 16, 16, KEY0: Send, BLUE);/* 串口输出实验信息*/printf(\n);printf(********************************\r\n);printf(STM32\r\n);printf(CH395Q Client\r\n);printf(ATOMALIENTEK\r\n);printf(KEY0: Send\r\n);printf(********************************\r\n);printf(\r\n);
}
/*** brief 例程测试* param 无* retval 无*/
void ch395_demo(void){ch395_show_mesg(); /* 显示信息*/do{if (ch395_int_pin_wire 0){ch395q_handler();/* 中断处理函数*/}}while (g_ch395q_sta.dhcp_status DHCP_STA); /* 获取DHCP*/while(1){ch395q_handler();}
}可以看到ch395_demo 函数调用了ch395_show_mesg 函数显示信息程序继续往下执行直到dhcp_status 不等于DHCP_STA 状态时系统退出do-while 循环表示DHCP 分配成功。
到了这里我们测试一下网络是否正常首先打开串口调式助手并且把代码下载到开发板中如下图所示 在PC 机上按下“winr”快捷键并输入cmd 进去命令行在该命令行上输入“ping 192.168.2.37”命令如果命令行弹出“无法访问目标主机”字符串则PC 机无法与开发板通讯显然我们移植的工程是失败的如果命令行弹出“字节32 时间1ms TTL255”字符串则PC 机能够与开发板通讯证明该工程移植成功下面我们在命令行ping 一下图1.3.7 的IP地址如下图所示 该实验的实验工程请参考《网络实验1 CH395_DHCP 实验》实验。
TCP_Client 实验
本章的目标是完成开发板和电脑之间的TCP 通信开发板作为客户端网络调试助手作为TCP 服务端实验中我们通过电脑端的网络调试助手发送数据到开发板开发板接收数据在串口上显示同时开发板通过按键发送数据到网络调试助手。
TCPClient 配置流程
CH395Q 以太网芯片实现TCPClient 连接是非常简单的我们只需要调用ch595_cmd.c 文件中的配置函数发送相应的命令即可完成下面笔者将介绍CH395Q 以太网芯片是如何实现TCPClient 连接的如下步骤所示
第一步将《网络实验2 CH395_UDP 实验》实验拷贝并复制到该实验路径下并把复制的工程命名为“网络实验3 CH395_TCP 客户端实验”。
第二步选择协议类型这里分为三种协议如下源码所示
/* CH395Q模块Socket协议类型定义*/
#define CH395Q_SOCKET_UDP 0 /* UDP */
#define CH395Q_SOCKET_TCP_CLIENT 1 /* TCP客户端*/
#define CH395Q_SOCKET_TCP_SERVER 2 /* TCP服务器*/本章是TCPClient 实验所以选择CH395Q_SOCKET_TCP_CLIENT 配置项。 注意本实验和“网络实验2 CH395_UDP 实验”一样仅修改显示信息和协议类型即可实现TCPClient。
第三步设置源端口和目标端口。
第四步ch395.c 文件下定义IP 地址、子网掩码和网关等参数这些内容笔者稍后在程序设计小节讲解。
TCPClient 实验
硬件设计
例程功能 在本实验中开发板主控芯片通过SPI 接口与CH395Q 以太网芯片进行通讯从而完成对CH395Q 以太网芯片的功能配置、数据接收等功能同时将CH395Q 以太网芯片的Socket0 配置为TCP 客户端并可通过按键向所连接的TCP 服务器发送数据也能够接收来自TCP 服务 器的数据并实时显示至串口调试助手。 该实验的实验工程请参考《网络实验3 CH395_TCP 客户端实验》。
程序设计
3.2.2.1 程序流程图 本实验的程序流程图如下图所示 程序解析 关于TCPClient 实验的程序解析笔者着重讲解ch395_demo.c/h 文件ch395_demo.h 文件主要声明ch395_demo 函数提供外部文件使用而ch395_demo.c 文件定义了三个函数这些函数如下表所示 下面是对本实验的ch395 _demo.c 文件中的函数进行介绍如下所示
函数ch395_show_mesg该函数主要的作用是显示实验信息如下源码所示
/**
* brief 显示实验信息
* param 无
* retval 无
*/
void ch395_show_mesg(void)
{/* LCD显示实验信息*/lcd_show_string(10, 10, 220, 32, 32, STM32, RED);lcd_show_string(10, 47, 220, 24, 24, CH395Q TCPClient, RED);lcd_show_string(10, 76, 220, 16, 16, ATOMALIENTEK, RED);lcd_show_string(10, 97, 200, 16, 16, KEY0: Send, BLUE);/* 串口输出实验信息*/printf(\n);printf(********************************\r\n);printf(STM32\r\n);printf(CH395Q TCPClient\r\n);printf(ATOMALIENTEK\r\n);printf(KEY0: Send\r\n);printf(********************************\r\n);printf(\r\n);
}此函数调用lcd_show_string 和printf 函数分别在LCD 和串口上显示实验信息。
函数ch395_demo 此函数主要设置TCPClient 相关网络参数如配置协议、IP 及端口等信息如下源码所示
/* 本地网络信息IP地址、网关地址、子网掩码和MAC地址*/
uint8_t ch395_ipaddr[4] {192,168,1,10};
uint8_t ch395_gw_ipaddr[4] {192,168,1,1};
uint8_t ch395_ipmask[4] {255,255,255,0};
uint8_t ch395_macaddr[6] {0xB8,0xAE,0x1D,0x00,0x00,0x00};
/* 远程IP地址设置*/
uint8_t ch395_des_ipaddr[4] {192,168,1,111};
static uint8_t socket0_send_buf[] {This is from CH395Q\r\n};
static uint8_t socket0_recv_buf[1024];
ch395_socket cha95_sockct_sta[8];
/*** brief 例程测试* param 无* retval 无*/
void ch395_demo(void)
{uint8_t key 0;ch395_show_mesg(); /* 显示信息*/do{ch395q_handler();}while (g_ch395q_sta.dhcp_status DHCP_STA); /* 获取DHCP *//* 使能socket接口*/cha95_sockct_sta[0].socket_enable CH395Q_ENABLE;/* 设置socket接口*/cha95_sockct_sta[0].socket_index CH395Q_SOCKET_0;/* 设置目标IP地址*/memcpy(cha95_sockct_sta[0].des_ip, ch395_des_ipaddr,sizeof(cha95_sockct_sta[0].des_ip));/* 设置静态本地IP地址*/memcpy(cha95_sockct_sta[0].net_config.ipaddr, ch395_ipaddr,sizeof(cha95_sockct_sta[0].net_config.ipaddr));/* 设置静态网关IP地址*/memcpy(cha95_sockct_sta[0].net_config.gwipaddr, ch395_gw_ipaddr,sizeof(cha95_sockct_sta[0].net_config.gwipaddr));/* 设置静态子网掩码地址*/memcpy(cha95_sockct_sta[0].net_config.maskaddr, ch395_ipmask,sizeof(cha95_sockct_sta[0].net_config.maskaddr));/* 设置静态MAC地址*/memcpy(cha95_sockct_sta[0].net_config.macaddr, ch395_macaddr,sizeof(cha95_sockct_sta[0].net_config.macaddr));/* 目标端口*/cha95_sockct_sta[0].des_port 8080;/* 源端口*/cha95_sockct_sta[0].sour_port 8080;/* 设置协议*/cha95_sockct_sta[0].proto CH395Q_SOCKET_TCP_CLIENT;/* 发送数据*/cha95_sockct_sta[0].send.buf socket0_send_buf;/* 发送数据大小*/cha95_sockct_sta[0].send.size sizeof(socket0_send_buf);/* 接收数据缓冲区*/cha95_sockct_sta[0].recv.buf socket0_recv_buf;/* 接收数据大小*/cha95_sockct_sta[0].recv.size sizeof(socket0_recv_buf);/* 配置socket参数*/ch395q_socket_config(cha95_sockct_sta[0]);while(1){key key_scan(0);if (key KEY0_PRES){ch395_send_data(0, (uint8_t *)socket0_send_buf,strlen((char *)socket0_send_buf));}ch395q_handler();}
}首先笔者声明了IP 地址、子网掩码、网关和MAC 地址这些网络参数是为了DHCP 失败时可设置默认的网络信息ch395_des_ipaddr 数组需要填写电脑的IP 地址必须填写正确而socket0_send_buf 和socket0_recv_buf 分别为发送的数据和接收缓冲区cha95_sockct_sta 变量的类型为ch395_socket该结构体描述每一个socket 网络参数该结构体如下所示
typedef struct ch395q_socket_t
{
uint8_t socket_enable; /* Socket使能*/
uint8_t socket_index; /* Socket标号*/
uint8_t proto; /* Socket协议*/
uint8_t des_ip[4]; /* 目的IP地址*/
uint16_t des_port; /* 目的端口*/
uint16_t sour_port; /* 源端口*/
struct
{
uint8_t *buf; /* 缓冲空间*/
uint32_t size; /* 缓冲空间大小*/
} send; /* 发送缓冲*/
struct
{
uint8_t *buf; /* 缓冲空间*/
uint32_t size; /* 缓冲空间大小*/
} recv; /* 接收缓冲*/
struct
{
uint8_t ip[4]; /* IP地址*/
uint8_t gwip[4]; /* 网关IP地址*/
uint8_t mask[4]; /* 子网掩码*/
uint8_t dns1[4]; /* DNS服务器1地址*/
uint8_t dns2[4]; /* DNS服务器2地址*/
} net_info; /* 网络信息*/
struct
{
uint8_t ipaddr[4]; /* IP地址32bit*/
uint8_t gwipaddr[4]; /* 网关地址32bit*/
uint8_t maskaddr[4]; /* 子网掩码32bit*/
uint8_t macaddr[6]; /* MAC地址48bit*/
} net_config; /* 网络配置信息*/
} ch395_socket;此结构体笔者分为四个部分讲解如下 ①描述每一个socket 共同部分
uint8_t socket_enable; /* Socket使能*/
uint8_t socket_index; /* Socket标号*/
uint8_t proto; /* Socket协议*/
uint8_t des_ip[4]; /* 目的IP地址*/
uint16_t des_port; /* 目的端口*/
uint16_t sour_port; /* 源端口*/socket_enable 为使能socket因为CH395Q 可以开启8 个socket 接口所以笔者使用这个成员变量描述socket 接口是否打开socket_index 为socket 的编号例如0~7 的socket 接口 proto 为协议选择描述这个socket 接口使用那个协议例如UDP、TCPClient 和TCPServer协议类型des_ip[4]设置目标IP 地址这个成员变量主要用作于UDP 和TCPClient 协议类型 des_port 和sour_port 就是设置目标端口和源端口。
②收发结构体
struct
{uint8_t *buf; /* 缓冲空间*/uint32_t size; /* 缓冲空间大小*/
} send; /* 发送缓冲*/struct
{uint8_t *buf; /* 缓冲空间*/uint32_t size; /* 缓冲空间大小*/
} recv;每一个socket 都可以收发数据所以笔者在这里定义了收发结构体用来描述发送缓冲区和发送缓冲区大小以及接收缓冲区和接收缓冲区大小。 ③网络信息DHCP
struct
{uint8_t ip[4]; /* IP地址*/uint8_t gwip[4]; /* 网关IP地址*/uint8_t mask[4]; /* 子网掩码*/uint8_t dns1[4]; /* DNS服务器1地址*/uint8_t dns2[4]; /* DNS服务器2地址*/
} net_info; /* 网络信息*/该结构体的成员变量是用来保存DHCP 分配的网络信息例如IP 地址、子网掩码、网关以及DNS 服务器地址。
④网络配置信息静态
struct
{uint8_t ipaddr[4]; /* IP地址32bit*/uint8_t gwipaddr[4]; /* 网关地址32bit*/uint8_t maskaddr[4]; /* 子网掩码32bit*/uint8_t macaddr[6]; /* MAC地址48bit*/
} net_config; /* 网络配置信息*/此结构体就是保存静态的网络信息例如DHCP 不成功时我们设置CH395Q 的网络信息为静态网络信息。
至此ch395_socket 结构体讲解完毕总的来说它就是用来描述每一个socket 接口的网络参数等信息。接着回到ch395_demo 函数讲解该函数调用ch395_show_mesg 函数显示实验信息并使用do while 语句获取DHCP 是否成功如果获取成功则dhcp_status 为DHCP_UP否则为DHCP_DOWN然后设置socket 接口网络信息例如socket 编号、静态网络信息、端口号及协议类型等参数最后等待连接连接完成后按下KEY0_PRES 就可以发送数据了。
注意协议类型为CH395Q_SOCKET_TCP_CLIENT。
下载验证
下载代码完成后打开网络调试助手等待开发板的LCD 出现下图所示的界面。 在网络调试助手上点击“连接”按钮如下图所示 设置完网络调试助手后在发送框填入要发送的数据这里输入要发送的数据ALIENTEKDATA然后点击发送这时串口调试助手显示接收的数据接着通过按下KEY0向电脑端发送数据“This is from CH395Q\r\n”如下图所示表明网络调试助手接收到开发板发送的数据这里我们按了13 次KEY0因此在网络调试助手上有13 行数据。 WebServer 实验
本章采用CH395Q 作为服务器在开发板上实现一个WebServer。在本章中笔者通过移植lwIP下的httpserver 实验来展示WebServer在浏览器输入开发板的 IP 地址来访问开发板这时开发板会返回一个网页数据浏览器根据这个网页数据构建一个网页。
WebServer 简介
Web 服务器可以解析HTTP 协议。当Web 服务器接收到一个HTTP 请求会返回一个HTTP 响应。为了处理一个请求Web 服务器可以响应一个静态页面或图片进行页面跳转或者把动态响应的产生委托给一些其它的程序例如CGI 脚本JSP 脚本servletsASP 脚本服务器端JavaScript或者一些其它的服务器端技术。无论它们的目的如何这些服务器端的程序通常产生一个HTML 的响应来让浏览器可以浏览。如下图所示。 WebServer 实验
硬件设计
例程功能
浏览器中输入开发板的IP 地址来访问开发板这时开发板会返回一个网页数据浏览器根据这些网页数据构建一个网页界面。 该实验的实验工程请参考《网络实验5 CH395_WEBSERVER 实验》。
软件设计
5.2.2.1 Webserver 函数解析
本实验是在《网络实验4 CH395_TCP 服务器实验》实验的基础上修改的这里笔者重点讲 解一下ch395_demo.c 文件下的函数如下所示
函数ch395_demo TCPServer 服务器连接及初始化该函数的原型如下源码所示
void ch395_demo(void)函数形参 无。 返回值 无。 2. 函数ch395_show_mesg 显示实验信息该函数的原型如下源码所示
void ch395_show_mesg(void)函数形参 无。 返回值 无。 3. 函数ch395_server_netconn_serve 接收一个HTTP 连接该函数的原型如下源码所示
void ch395_server_netconn_serve(void)函数形参 无。 返回值 无。 4. 函数ch395_data_locate 寻找指定字符位置该函数的原型如下源码所示
char *ch395_data_locate(char *buf, char *name)函数形参 此函数是形参如下表所示 返回值 返回网页字符的地址。
5.2.2.2 Webserver 服务器配置步骤
①配置CH395 为TCP 服务器模式 CH395Q 配置为TCPServer 服务器的流程请参考第四章的内容。 ②接收HTTP 协议请求报文当网页输入IP 地址时CH395 会接收到一个HTTP 协议的请求报文接收完成之后系统调用函数ch395_server_netconn_serve 发送网页数据到浏览器中。
5.2.2.3 程序流程图
本实验的程序流程图如下图所示 程序解析
打开ch395_demo.h 文件在这个文件下我们声明了ch395_demo 函数提供给外部文件使用ch395_demo.c 文件如下所示
①网络信息
/* 本地网络信息IP地址、网关地址、子网掩码和MAC地址*/
uint8_t ch395_ipaddr[4] {192,168,1,10};
uint8_t ch395_gw_ipaddr[4] {192,168,1,1};
uint8_t ch395_ipmask[4] {255,255,255,0};
uint8_t ch395_macaddr[6] {0xB8,0xAE,0x1D,0x00,0x00,0x00};
/* 远程IP地址设置*/
uint8_t ch395_des_ipaddr[4] {192,168,1,111};②声明HTTPS 头部
static const char http_html_hdr[] HTTP/1.1 200 OK\r\nContent-type:
text/html\r\n\r\n;③网页数据
static const char http_index_html[]
!DOCTYPE html\r\n
html xmlnshttp://www.w3.org/1999/xhtml\r\n
head\r\n
title/title\r\n
/head\r\n
body\r\n
div idmain\r\n
h3正点原子学习网/h3\r\n
iframe srchttp://www.openedv.com/docs/index.html
width1024px height1024px/iframe\r\n
/div\r\n
/body\r\n
/html\r\n;由于笔者不怎么会编写HTML 代码所以上述的网页数据比较简单它只能显示正点原子资料中心网页。
④接收一个HTTP 连接
/*** brief 服务HTTP线程中接受的一个HTTP连接* param conn netconn控制块* retval 无*/
void ch395_server_netconn_serve(void)
{char *ptemp;/* 从端口读取数据如果那里还没有数据则阻塞。我们假设请求(我们关心的部分) */if (cha95_sockct_sta[0].recv.size ! 0){/* 这是一个HTTP GET命令吗?只检查前5个字符因为GET还有其他格式我们保持简单)*/if (socket0_recv_buf[0] G socket0_recv_buf[1] E socket0_recv_buf[2] T socket0_recv_buf[3] socket0_recv_buf[4] / ){
start_html:/* 发送HTML标题从大小中减去1因为我们没有在字符串中发送\0
NETCONN_NOCOPY:我们的数据是常量静态的所以不需要复制它*/ch395_send_data(0, (uint8_t *)http_html_hdr,sizeof(http_html_hdr) - 1);/* 发送我们的HTML页面*/ch395_send_data(0, (uint8_t *)http_index_html,sizeof(http_index_html) - 1);}else if(socket0_recv_buf[0] P socket0_recv_buf[1] O socket0_recv_buf[2] S socket0_recv_buf[3] T){ptemp ch395_data_locate((char *)socket0_recv_buf, led1);if (ptemp ! NULL){/* 查看led1的值。为1则灯亮为2则灭此值与HTML网页中设置有关*/if (*ptemp 1){/* 点亮LED1 */}else{/* 熄灭LED1 */}}/* 查看beep的值。为3则灯亮为4则灭此值与HTML网页中设置有关*/ptemp ch395_data_locate((char *)socket0_recv_buf, beep);if (ptemp ! NULL ){if (*ptemp 3){/* 打开蜂鸣器*/}else{/* 关闭蜂鸣器*/}}goto start_html;}}memset(socket0_recv_buf,0, sizeof(socket0_recv_buf));delay_ms(100);
}此函数可以分为两个部分讲解第一部分是开发板接收到网页的“GET”请求系统先发送HTTPS 协议头部接着发送网页数据这样网页就可以显示了 第二部分是开发板接收到网页的“POST”请求系统会根据ch395_data_locate 函数判断请求的位置并做出相应的动作。
⑥查询字符位置
/**
* brief 寻找指定字符位置
* param buf 缓冲区指针
* param name 寻找字符
* retval 返回字符的地址
*/
char *ch395_data_locate(char *buf, char *name)
{char *p;p strstr((char *)buf, name);if (p NULL){return NULL;}p strlen(name);return p;
}此函数非常简单它根据接收的“POST”数据查询指定字符的位置。
⑦测试例程
/*** brief 例程测试* param 无* retval 无*/
void ch395_demo(void)
{ch395_show_mesg(); /* 显示信息*/do{ch395q_handler();}while (g_ch395q_sta.dhcp_status DHCP_STA); /* 获DHCP*//* 使能socket接口*/cha95_sockct_sta[0].socket_enable CH395Q_ENABLE;/* 设置socket接口*/cha95_sockct_sta[0].socket_index CH395Q_SOCKET_0;/* 设置目标IP地址*/memcpy(cha95_sockct_sta[0].des_ip, ch395_des_ipaddr,sizeof(cha95_sockct_sta[0].des_ip));/* 设置静态本地IP地址*/memcpy(cha95_sockct_sta[0].net_config.ipaddr, ch395_ipaddr,sizeof(cha95_sockct_sta[0].net_config.ipaddr));/* 设置静态网关IP地址*/memcpy(cha95_sockct_sta[0].net_config.gwipaddr, ch395_gw_ipaddr,sizeof(cha95_sockct_sta[0].net_config.gwipaddr));/* 设置静态子网掩码地址*/memcpy(cha95_sockct_sta[0].net_config.maskaddr, ch395_ipmask,sizeof(cha95_sockct_sta[0].net_config.maskaddr));/* 设置静态MAC地址*/memcpy(cha95_sockct_sta[0].net_config.macaddr, ch395_macaddr,sizeof(cha95_sockct_sta[0].net_config.macaddr));/* 源端口*/cha95_sockct_sta[0].sour_port 80;/* 设置协议*/cha95_sockct_sta[0].proto CH395Q_SOCKET_TCP_SERVER;/* 接收数据缓冲区*/cha95_sockct_sta[0].recv.buf socket0_recv_buf;/* 接收数据大小*/cha95_sockct_sta[0].recv.size sizeof(socket0_recv_buf);/* 配置socket参数*/ch395q_socket_config(cha95_sockct_sta[0]);while(1){ch395_server_netconn_serve();ch395q_handler();}
}注意源端口必须设置为80 端口因为80 端口是为HTTP 超文本传输协议开放的端口。
下载验证
打开网页输入IP 地址如下图所示。 可以看出浏览器比喻成一个HTML 的编译器而CH395Q 传输的网页数据就是编译器可识别的代码段。
NTP 实时时间实验
NTP 网络时间协议是基于UDP 协议上实现它用于网络时间同步使网络中的计算机时钟同步到UTC再配合各个时区的偏移调整就能实现精准同步对时功能。
NTP 简介
NTP 服务器是用来使计算机时间同步化的一种协议它可以使计算机对其服务器或时钟源如石英钟GPS 等等做同步化它还可以提供高精准度的时间校正且可介由加密确认的方式来防止恶毒的协议攻击。NTP 协议的数据报文格式如下图所示 从上图可知NTP 报文是由多个字段组成每一个字段所获取的功能都不一样一般获取实时时间只使用VN 字段和Mode 字段即可完成其他的字段使用请读者查阅相关的NTP 协议资料。
NTP 数据报文格式的各个字段的作用如下表所示 从上表可知NTP 报文的字段非常多这些字段并不是每一个都必须设置的请大家根据项目的需要来构建NTP 请求报文。下面笔者使用网络调试助手制作一个简单的NTP 实验如下图所示 这里使用UDP 协议连接阿里云 NTP 服务器该服务器的域名为 ntp1.aliyun.com我们可在在线域名网页解析该域名并使用网络调式助手连接阿里云NTP 服务器。这里笔者只设置NTP 报文的VN 字段和Mode 字段该些字段组成后可以填入0xa3版本4、0x1b版本3、0x13版本2和0x0b版本1其他字段可设置为0。
上图中笔者使用的是版本3 来获取阿里云NTP 时间信息发送NTP 报文完成之后阿里云NTP 服务器会返回一些数据这些数据包含了当前时间数据例如上图中0xE67231D3 十六进制就是NTP 服务器返回的时间戳这个数据是十六进制展示的所以我们把这个十六进制转成是十进制3866243539接着减去1900-1970 的时间差2208988800 秒就等于当前的秒数1657254739最后把这个秒数进行时间戳转换转换完成后可得到实时时间信息。
在浏览器上打开https://tool.lu/timestamp/网址从这个网页上计算秒数并把它转换成实时时间如下图所示 获取NTP 实时时间需要哪些步骤了如下所示
①连接阿里云NTP 服务器我们以UDP 协议为例。②使用开发板发送NTP 报文到阿里云NTP 服务器中。③获取阿里云NTP 服务器的数据取第40 位到43 位的十六进制。④把40 位到43 位的十六进制转成十进制。⑤把十进制数减去1900-1970 的时间差2208988800 秒。⑥数值转成年月日时分秒。
根据上述的流程我们可在工程中构建NTP 报文并且传输到阿里云服务器传输完成之后接收阿里云服务器应答的数据最后对这些数据从③~⑥进行转换。
NTP 实验
硬件设计
例程功能
使用UDP 协议连接阿里云的NTP 服务器并周期发送NTP 请求报文发送完成之后对阿里云NTP 服务器返回的数据进行解析把它转换成实时时间信息。 该实验的实验工程请参考《网络实验6 CH395_NTP 网络时间实验》。
软件设计
6.2.2.1 NTP 函数解析 本实验是在《网络实验2 CH395_UDP 实验》实验的基础上修改的这里笔者重点讲解一下ch395_deno.c 文件下的ch395_ntp_client_init、ch395_get_seconds_from_ntp_server 和ch395_calc_date_time 函数如下所示
函数ch395_ntp_client_init 构建NTP 请求报文该函数的原型如下源码所示
void ch395_ntp_client_init(void)函数形参 无。 返回值 无。 2. 函数ch395_get_seconds_from_ntp_server 从NTP 服务器获取时间该函数的原型如下源码所示
void ch395_get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx)函数形参 此函数只有2 个形参如下表所示 函数返回值 无。 3. 函数ch395_calc_date_time 计算日期时间并转换成UTC 世界标准时间该函数的原型如下源码所示
void ch395_calc_date_time(unsigned long long time)函数形参 此函数的形参如下表所示 函数返回值 无。
6.2.2.2 NTP 配置步骤 ①配置CH395 为UDP 模式 CH395Q 配置为UDP 方式请参考第二章的内容。 ②制作NTP 请求报文 调用函数ch395_ntp_client_init 制作NTP 请求报文周期发送NTP 请求报文。 ③处理NTP 返回的信息 调用函数ch395_get_seconds_from_ntp_server 处理NTP 服务器返回的数据这里笔者取40位到43 位的数据并且转换成十进制。 ④计算日期时间 总秒数需要减去1900-1970 的时间差2208988800 秒减去之后的秒数就是当前时间的总秒数利用算法把秒数转换成当前时间。
6.2.2.3 程序流程图 本实验的程序流程图如下图所示 程序解析 本次实验重点的内容是在ch395_demo.c/h 文件中下面笔者分别地讲解这两个文件实现的内容如下所示
文件ch395_demo.h
打开ch395_demo.h 文件在这个文件中笔者定义了NPTformat 和DateTime 结构体NPTformat 结构体的成员变量与NTP 的报文结构的字段是一一对应的而DateTime 结构体主要保存转换后标准时间的年月日时分秒等信息它还声明了ch395_demo.c 文件中的函数提供给外部文件使用如下源码所示
#ifndef __CH395_DEMO_H
#define __CH395_DEMO_H
#include ./SYSTEM/sys/sys.htypedef struct _NPTformat
{char version; /* 版本号*/char leap; /* 时钟同步*/char mode; /* 模式*/char stratum; /* 系统时钟的层数*/char poll; /* 更新间隔*/signed char precision; /* 精密度*/unsigned int rootdelay; /* 本地到主参考时钟源的往返时间*/unsigned int rootdisp; /* 统时钟相对于主参考时钟的最大误差*/char refid; /* 参考识别码*/unsigned long long reftime; /* 参考时间*/unsigned long long org; /* 开始的时间戳*/unsigned long long rec; /* 收到的时间戳*/unsigned long long xmt; /* 传输时间戳*/
} NPTformat;typedef struct _DateTime /*此结构体定义了NTP时间同步的相关变量*/
{int year; /* 年*/int month; /* 月*/int day; /* 天*/int hour; /* 时*/int minute; /* 分*/int second; /* 秒*/
} DateTime;#define SECS_PERDAY 86400UL /* 一天中的几秒钟 60*60*24 */
#define UTC_ADJ_HRS 8 /* SEOUL : GMT8东八区北京*/
#define EPOCH 1900 /* NTP 起始年*/
void ch395_demo(void); /* 例程测试*/
void ch395_get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx);
#endif从上述源码可以看出ch395_demo.h 文件主要声明了一些结构体和变量这些结构体和宏定义会在ch395_demo.c 文件中调用。
文件ch395_demo.c
①函数ch395_ntp_client_init 构建NTP 请求报文如下源码所示
/*** brief 初始化NTP Client信息* param 无* retval 无*/
void ch395_ntp_client_init(void)
{uint8_t flag;g_ntpformat.leap 0; /* leap indicator */g_ntpformat.version 3; /* version number */g_ntpformat.mode 3; /* mode */g_ntpformat.stratum 0; /* stratum */g_ntpformat.poll 0; /* poll interval */g_ntpformat.precision 0; /* precision */g_ntpformat.rootdelay 0; /* root delay */g_ntpformat.rootdisp 0; /* root dispersion */g_ntpformat.refid 0; /* reference ID */g_ntpformat.reftime 0; /* reference time */g_ntpformat.org 0; /* origin timestamp */g_ntpformat.rec 0; /* receive timestamp */g_ntpformat.xmt 0; /* transmit timestamp */flag (g_ntpformat.version 3) g_ntpformat.mode; /* one byte Flag */memcpy(socket0_ntp_message, (void const *)(flag), 1);btim_timx_int_init(9999,7199);
}从上述源码可以看出笔者只设置version、mode 字段其他字段都设置为0接着对这两个字段进行移位0011 3(version) 0011(mode) 0x1b可得到0x1b 十六进制数值并把它保存在socket0_ntp_message 数组当中开启定时器周期发送NTP 请求报文。该定时器服务函数如下所示
/**
* brief 回调函数定时器中断服务函数调用
* param 无
* retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim (timx_handler)){ch395_udp_send_data(cha95_sockct_sta[0].send.buf,cha95_sockct_sta[0].send.size,cha95_sockct_sta[0].des_ip,cha95_sockct_sta[0].des_port,cha95_sockct_sta[0].socket_index); /* 发送一个NTP包*/}
}此定时服务函数的工作是1s 发送一次NTP 请求包。
②函数ch395_get_seconds_from_ntp_server 处理NTP 服务器返回的数据如下源码所示
/**
* brief 从NTP服务器获取时间
* param buf存放缓存
* param idx定义存放数据起始位置
* retval 无
*/
void ch395_get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx)
{unsigned long long atk_seconds 0;uint8_t i 0;for (i 0; i 4; i) /* 获取40~43位的数据*/{/* 把40~43位转成16进制再转成十进制*/atk_seconds (atk_seconds 8) | buf[idx i];}/* 减去减去1900-1970的时间差2208988800秒*/atk_seconds - NTP_TIMESTAMP_DELTA;ch395_calc_date_time(atk_seconds); /*由UTC时间计算日期*/
}该函数主要完成三个操作
①从NTP 服务器返回的数据中获取40~43 位的数据并把它转换成十进制数值。 ②减去减去1900-1970 的时间差2208988800 秒。 ③调用ch395_calc_date_time 函数计算日期。 ③函数ch395_calc_date_time
利用总秒数换算成日期数据如下源码所示
/***brief 计算日期时间*param time UTC 世界标准时间*retval 无*/
void ch395_calc_date_time(unsigned long long time)
{unsigned int Pass4year;int hours_per_year;if (time 0){time 0;}g_nowdate.second (int)(time % 60); /* 取秒时间*/time / 60;g_nowdate.minute (int)(time % 60); /* 取分钟时间*/time / 60;g_nowdate.hour (int)(time % 24); /* 小时数*//* 取过去多少个四年每四年有1461*24 小时*/Pass4year ((unsigned int)time / (1461L * 24L));g_nowdate.year (Pass4year 2) 1970; /* 计算年份*/time % 1461 * 24; /* 四年中剩下的小时数*/for (;;) /* 校正闰年影响的年份计算一年中剩下的小时数*/{hours_per_year 365 * 24; /* 一年的小时数*/if ((g_nowdate.year 3) 0) /* 判断闰年*/{hours_per_year 24; /* 是闰年一年则多24小时即一天*/}if (time hours_per_year){break;}g_nowdate.year;time - hours_per_year;}time / 24; /* 一年中剩下的天数*/time; /* 假定为闰年*/if ((g_nowdate.year 3) 0) /* 校正闰年的误差计算月份日期*/{if (time 60){time--;}else{if (time 60){g_nowdate.month 1;g_nowdate.day 29;return ;}}}/* 计算月日*/for (g_nowdate.month 0; Days[g_nowdate.month] time; g_nowdate.month){time - Days[g_nowdate.month];}g_nowdate.day (int)(time);return;
}计算闰年等信息最后把年月日时分秒等数据存储在g_nowdate 结构体当中。
下载验证
编译下载到开发板打开串口调试助手如下图所示 基于MQTT 协议连接OneNET 服务器
本章主要介绍CH395Q 如何通过MQTT 协议将设备连接OneNET 平台并实现端云控制。
移植 MQTT 协议
若CH395Q 以太网芯片实现 MQTT 协议的应用则必须在工程中添加 MQTT 代码包也就是实现第一张图中的应用层吧该库可在http://mqtt.org/网址下载。这里笔者以《网络实验3 CH395_TCP 客户端实验》实验为基础在该工程Middlewares 目录下新建MQTT 文件夹用来保存MQTT 代码包MQTTPacket\src 的.c/h 文件该文件夹结构如下所示 我们在工程新建Middlewares/MQTT 分组在此分组下添加MQTT 文件该分组如下图所示。 配置OneNET 平台
配置OneNET 服务器步骤
第一步打开OneNET 服务器并注册账号注册之后在主界面下打开产品服务页面的MQTT物联网套件如下图所示 第二步点击上图的“立刻使用”选项跳到其他页面之后点击“添加产品”选项此时该页面会弹出产品信息小界面这小界面如下图所示 上图中笔者重点填写联网方式和设备接入协议的选项它们分别选择移动蜂窝网络及MQTT 协议接入至于其他选项请大家根据爱好选择。创建MQTT 产品之后用户可以得到该产品的信息如下图所示 本实验会用到上述的产品信息例如产品ID366007、“access_key”产品密钥以及产品名称MQTT_TSET。
第三步在设备列表中添加设备如下图所示 这里也是需要用户自己填写设备的名称。填写完成之后可得到设备信息如下图所示 本实验会用到上图中的设备ID617747917、设备名称MQTT 以及“key”设备的密钥。 打开OneNET 在线开发指南在这个指南中找到服务器地址这些服务器地址就是MQTT服务器地址如下图所示 可以看到OneNTE 的MQTT 服务器具有两个连接方式一种是加密接口连接而另一种是非加密接口连接本章实验使用的是非加密接口连接MQTT 服务器。
注意MQTT 物联网套件采用安全鉴权策略进行访问认证即通过核心密钥计算的token进行访问认证简单来讲若用户想连接OneNET 的MQTT 服务器则必须计算核心密钥。这个核心密钥是根据前面创建的产品和设备相关的信息计算得来的密钥的计算方法可以使用OneNET 提供的 token 生成工具计算该软件在这个网址下载https://open.iot.10086.cn/doc/v5/develop/detail/242
下面笔者简单讲解一下token 生成工具的使用如下图所示 接下来笔者分别地讲解这个生成工具各个选项的作用如下所示 res输入格式为“products/{pid}/devices/{device_name}”这个输入格式中的“pid”就是MQTT 产品ID而“device_name”就是设备的名称。根据前面创建的产品和设备来填写res 选项的参数如下图所示 et访问过期时间expirationTimeunix时间设置访问时间应大于当前的时间这里笔者选择参考文档中的数值1672735919如下图所示 key是指选择设备的key 密钥如下图所示 最后按下上图中的“Generate”按键生成核心密钥如下图所示。 这个核心密钥会在MQTT 客户端的结构体client_pass 成员变量保存。
工程配置
小上节我们使用token 生成工具根据产品信息以及设备信息来计算核心密钥这样的方式导致每次创建一个设备都必须根据这个设备信息再计算一次核心密钥才能连接这种方式会大 大地降低我们的开发效率。为了解决这个问题笔者使用另一个方法那就是使用代码的方式计算核心密钥。
这些代码怎么编写呢其实读者可在OneOS 官网下载OneOS 源码包它里面包含了MQTT 协议连接OneNET 平台的核心密钥计算代码这些代码在oneos2.0\components\cloud\onenet\mqtt-kit\authorization 路径下找到大家先下载OneOS 源码并在该路径下复制token文件夹到工程的User\APP 路径当中如下如图所示 打开工程并新建User/APP/token 分组在这个分组中添加User\APP\token 路径下的.c 文件如下图所示 基于OneNET 平台MQTT 实验
硬件设计
例程功能 本章目标是开发板使用MQTT 协议连接OneNET 服务器并实现数据上存及更新。 该实验的实验工程请参考《网络实验7 CH395_OneNET_MQTT 实验》。
软件设计
7.4.2.1 MQTT 函数解析 本实验是在《网络实验3 CH395_TCP 客户端实验》实验的基础上修改的这里笔者重点讲解一下ch395_demo.c 文件下的函数如下所示
函数ch395_demo
测试通讯、SPI 初始化、网络参数初始化以及打开socket 以及MQTT 连接、订阅及发布等操作该函数的原型如下所示
void ch395_demo(void)函数形参 无。 返回值 无。 2. 函数ch395_show_mesg 显示实验信息该函数原型如下所示
void ch395_show_mesg(void)函数形参 无。 返回值 无。 3. 函数ch395_transport_send_packet_buffer 发送数据到OneNET 服务器该函数原型如下所示
int ch395_transport_send_packet_buffer( int sock,
unsigned char* buf,
int buflen)函数形参 返回值 大于0 表示发送成功。
函数ch395_transport_get_data 获取返回的数据该函数原型如下所示
int ch395_transport_get_data(unsigned char *buf, int count)函数形参 返回值 大于0 表示发送成功。
7.4.2.2 MQTT 配置步骤 ①配置CH395 为TCP 客户端模式 CH395Q 配置为TCP 客户端方式请参考第三章的内容。 ②设置三元组内容 在OneNET 平台创建MQTT 服务器创建完成之后得到产品ID、产品密钥、设备ID 和设备密钥等参数。 ③定义发布和订阅命令 根据OneNET 平台的要求定义发布和订阅命令。
7.4.2.3 程序流程图 本实验的程序流程图如下图所示 程序解析
打开ch395_demo.h 文件在这个文件中笔者定义了OneNET 服务器连接的产品ID 与设备APIKey 等参数另外该文件还声明了ch395_demo.c 下的函数接下来笔者分别地讲解这两个文件实现代码如下所示
文件ch395_onenet.h
#ifndef __CH395_DEMO_H
#define __CH395_DEMO_H
#include ./SYSTEM/sys/sys.h
/* 用户需要根据设备信息完善以下宏定义中的三元组内容*/
#define USER_DEVICE_NAME MQTT /* 设备名*/
#define USER_PRODUCT_ID 366007 /* 产品ID */
/* 产品密钥*/
#define USER_ACCESS_KEY qlWudWg/3ANGVQLeHGfAu0Eh8J7CWgozfOpljIGy8k
#define USER_DEVICE_ID 617747917 /* 产品设备ID */
/* 设备密钥*/
#define USER_KEY QyxIRiJNQG5wPmEmMGY8QGVsRUtIZDtVUGI0eCQ1V3A
/* 该密码需要onenet提供的token软件计算得出*/
#define PASSWORD version2018-10-31resproducts%2F366007%2Fdevices%2FMQTTet1672735919methodmd5signqI0pgDJnICGoPdhNi%2BHtfg%3D%3D
/* 以下参数的宏定义固定不需要修改只修改上方的参数即可*/
#define HOST_NAME open.iot.10086.cn /*onenet域名*/
#define DEVICE_SUBSCRIBE $sys/USER_PRODUCT_ID/USER_DEVICE_NAME/dp/post/json/ /* 订阅*/
#define DEVICE_PUBLISH $sys/USER_PRODUCT_ID/USER_DEVICE_NAME/dp/post/json /* 发布*/
#define SERVER_PUBLISH $sys/USER_PRODUCT_ID/USER_DEVICE_NAME/cmd/request/ /* 服务器下发命令*/
typedef struct
{char pro_id[10];char access_key[48];char dev_name[64 1];char dev_id[16];char key[48];
} onenet_info_t;
void ch395_demo(void); /* 例程测试*/
#endif上述的订阅和发布的topic 主题请大家参考OneNET 官方文档该文档的地址为https://open.iot.10086.cn/doc/v5/develop/detail/251接着笔者定义了onenet_info_t 结构体该结构体主要存储MQTT 产品和设备信息最后笔者在此文件声明了ch395_demo 函数提供外部文件使用。
文件ch395_demo.c
此文件笔者重点讲解ch395_demo 函数该函数分别完成了四个任务下面笔者分别地讲解这四个任务的内容。
①配置CH395Q 为客户端模式
cha95_sockct_sta[0].socket_enable CH395Q_ENABLE; /* 使能socket接口*/
cha95_sockct_sta[0].socket_index CH395Q_SOCKET_0;/* 设置socket接口*/
/* 设置目标IP地址*/
memcpy(cha95_sockct_sta[0].des_ip, ch395_des_ipaddr,sizeof(cha95_sockct_sta[0].des_ip));
/* 设置静态本地IP地址*/
memcpy(cha95_sockct_sta[0].net_config.ipaddr, ch395_ipaddr,sizeof(cha95_sockct_sta[0].net_config.ipaddr));
/* 设置静态网关IP地址*/
memcpy(cha95_sockct_sta[0].net_config.gwipaddr, ch395_gw_ipaddr,sizeof(cha95_sockct_sta[0].net_config.gwipaddr));
/* 设置静态子网掩码地址*/
memcpy(cha95_sockct_sta[0].net_config.maskaddr, ch395_ipmask,sizeof(cha95_sockct_sta[0].net_config.maskaddr));
/* 设置静态MAC地址*/
memcpy(cha95_sockct_sta[0].net_config.macaddr, ch395_macaddr,sizeof(cha95_sockct_sta[0].net_config.macaddr));
cha95_sockct_sta[0].des_port 1883; /* 目标端口*/
cha95_sockct_sta[0].sour_port 5000; /* 源端口*/
cha95_sockct_sta[0].proto CH395Q_SOCKET_TCP_CLIENT; /* 设置协议*/
cha95_sockct_sta[0].send.buf socket0_send_buf; /* 发送数据*/
cha95_sockct_sta[0].send.size sizeof(socket0_send_buf); /* 发送数据大小*/
cha95_sockct_sta[0].recv.buf socket0_recv_buf; /* 接收数据缓冲区*/
cha95_sockct_sta[0].recv.size sizeof(socket0_recv_buf); /* 接收数据大小*/
ch395q_socket_config(cha95_sockct_sta[0]); /* 配置socket参数*/配置CH395Q 芯片为TCPClient 模式并设置目标端口为1883。
②计算连接核心密钥 这里我们根据前面7.3 小节的内容来计算OneNET 服务器核心密钥如下源码所示
char pro_id[] USER_PRODUCT_ID; /* 产品ID */
char access_key[] USER_ACCESS_KEY; /* 产品密钥*/
char dev_name[] USER_DEVICE_NAME; /* 设备名称*/
char dev_id[] USER_DEVICE_ID; /* 产品设备ID */
char key[] USER_KEY; /* 设备密钥*/
char version[] 2018-10-31;
unsigned int expiration_time 1956499200;
char authorization_buf[160] {0};
/* 把各个参数保存在g_onenet_info结构体的成员变量中*/
memset(g_onenet_info.pro_id, 0, sizeof(g_onenet_info.pro_id));
strcpy(g_onenet_info.pro_id, pro_id);
memset(g_onenet_info.access_key, 0, sizeof(g_onenet_info.access_key));
strcpy(g_onenet_info.access_key, access_key);
memset(g_onenet_info.dev_name, 0, sizeof(g_onenet_info.dev_name));
strcpy(g_onenet_info.dev_name, dev_name);
memset(g_onenet_info.dev_id, 0, sizeof(g_onenet_info.dev_id));
strcpy(g_onenet_info.dev_id, dev_id);
memset(g_onenet_info.key, 0, sizeof(g_onenet_info.key));
strcpy(g_onenet_info.key, key);
/* 根据这些参数进行解码当然这个密码可以在token软件下解码*/
onenet_authorization(version,(char *)g_onenet_info.pro_id,expiration_time,(char *)g_onenet_info.key,(char *)g_onenet_info.dev_name,authorization_buf,sizeof(authorization_buf),0);
data.clientID.cstring (char *)g_onenet_info.dev_name; /* 设备名称*/;
data.username.cstring (char *)g_onenet_info.pro_id; /* 产品ID */;
data.password.cstring (char *)authorization_buf; /* 计算出来的密码*/;
data.keepAliveInterval 100; /* 保活时间*/笔者根据OneNET 服务器创建的产品信息使用onenet_authorization 函数计算核心密钥并把它存储在authorization_buf 缓存区当中。
③连接OneNET 服务器根据上述的核心密钥和创建MQTT 产品的信息连接OneNET 平台如下源码所示
case CONNECT: /* 客户端发送服务器的连接操作*/
/* 获取数据组长发送连接信息*/
g_len MQTTSerialize_connect((unsigned char *)socket0_send_buf,sizeof(socket0_send_buf), data);
/* 发送返回发送数组长度*/
g_rc ch395_transport_send_packet_buffer(CH395Q_SOCKET_0,(unsigned char *)socket0_send_buf,g_len);
if (g_rc g_len)printf(发送连接成功\r\n);
elseprintf(发送连接失败\r\n);g_msgtypes 0;
break;上述源码调用了MQTTSerialize_connect 函数把MQTT 相关的信息转换成序列码接着调用ch395_transport_send_packet_buffer 发送连接序列码。
④订阅操作 发送连接序列码之后程序就会发送一个订阅主题到OneNET 平台如下源码所示
case SUBSCRIBE: /* 客户端发送到服务器的订阅操作*/
topicString.cstring DEVICE_SUBSCRIBE;
g_len MQTTSerialize_subscribe((unsigned char *)socket0_send_buf,sizeof(socket0_send_buf), 0, 1, 1, topicString, g_req_qos);
g_rc ch395_transport_send_packet_buffer(CH395Q_SOCKET_0,(unsigned char *)socket0_send_buf, g_len);
if (g_rc g_len)printf(send SUBSCRIBE Successfully\r\n);else
{int t 0;t ;if (t 10){t 0;g_msgtypes CONNECT;}elseg_msgtypes SUBSCRIBE;break;
}
g_msgtypes 0;
break;可以看到DEVICE_SUBSCRIBE 配置项指向的是订阅主题字符串接着程序调用函数MQTTSerialize_subscribe 把订阅主题数据转换成序列码最后调用函数ch395_transport_send_packet_buffer 发送该序列码以表示订阅操作。
⑤发布数据 当订阅完成以后才能实施发布措施如下源码所示
if (g_ch395q_sta.switch_status CONNECT_STAT)
{g_temp 30 rand() % 10 1; /* 温度的数据*/g_humid 54.8 rand() % 10 1; /* 湿度的数据*/sprintf((char *)payload_out, {\id\: 123,\dp\: { \temperatrue\:[{\v\: %0.1f,}],\power\: [{\v\: %0.1f,}]}}, g_temp, g_humid);payload_out_len strlen((char *)payload_out);topicString.cstring DEVICE_PUBLISH; /* 属性上报发布*/g_len MQTTSerialize_publish((unsigned char *)socket0_send_buf,sizeof(socket0_send_buf), 0, 1, 0, 1, topicString, payload_out,payload_out_len);g_rc ch395_transport_send_packet_buffer(CH395Q_SOCKET_0,(unsigned char *)socket0_send_buf, g_len);if (g_rc g_len){printf(send PUBLISH Successfully\r\n);}else{printf(send PUBLISH failed\r\n);}
}
delay_ms(100);从上述源码可以看出payload_out 存储的是发布的数据这个数据结构必须符合OneNET平台的要求接着DEVICE_PUBLISH 配置项指向发布操作的指令它们经过函数MQTTSerialize_publish 转换成序列码接着调用函数ch395_transport_send_packet_buffer 发送序列码这样才能发布成功。
下载验证
编译代码并把下载到开发板上运行打开OneNET 的MQTT 服务器查看数据流展示如下图所示 原子云平台连接
原子云即原子云服务器是正点原子推出的物联网云服务平台目前它可以实现数据的监控、转发和管理等功能在未来也会持续更新更多的功能以满足用户的需求。原子云域名为cloud.alientek.com端口号为59666。原子云已经支持通过API 接口进行访问相关文档说明在“资料包→1文档资料”下查找。
原子云工作流程
如何获取设备编号与设备密码 本小节就来教大家如何从原子云上获取设备编号与设备密码,以实现开发板与原子云之间的数据透传用户不仅可以在原子云上查看设备上传到原子云的数据而且可以使用API 接入原子云来获取这些数据以便用户开发。
原子云获取设备编号与设备密码步骤
第一步账号注册。登陆原子云服务器https://cloud.alientek.com/没有账号可以先注册一个账号有了账号后直接输入用户名和密码登录原子云就可以了。如下图所示 第二步创建设备节点。进入“设备管理”界面点击“新增设备”开始创建设备节点在“新增设备/选择设备类型”中选择“ATK-UART2ETH”类型然后输入“设备名称和密码”选择“新增”就可以成功创建一个设备。创建成功的设备节点会在列表中显示。如下图所示 第三步配置DTU 的设备编号与设备密码参数。新增设备节点成功后就能在设备节点列表中查看刚刚新增成功的设备节点将上一步中新增的设备编号和设备密码填入函数atk_decode 中就可以实现开发板与原子云之间的连接, 如下图所示 第四步在分组中添加lib 文件。在工程添加atk_decobe.lib 和atk.h 文件如下图所示 原子云连接实验
硬件设计
例程功能 实现开发板与正点原子的物联网云服务平台连接与交互数据。当按下KEY0 时把数据上存到原子云平台上当然原子云平台下发数据可在串口上打印。 该实验的实验工程请参考《网络实验8 CH395_连接原子云》。
软件设计
8.2.2.1 原子云函数解析
atk_decode 函数 该函数是实现MQTT 连接、收发等功能该函数的原型如以下源码所示
uint8_t * atk_decode(char *mpid, char *pwd);函数形参 函数atk_decode()具有2 个参数如表9.2.2.1.1 所示 函数返回值 无。
8.2.2.2 原子云配置步骤 ①配置CH395 为TCP 客户端模式 CH395Q 配置为TCP 客户端方式请参考第三章的内容。 ②配置端口号 配置目标端口和源端口为原子云端口59666。 ③计算连接密钥 把设备号和设备密码传入atk_decode 函数来计算连接密钥。 ④连接原子云 把计算得出的密钥以ch395_send_data 函数发送。
8.2.2.3 程序流程图 本实验的程序流程图如下图所示 程序解析 笔者重点讲解ch395_demo.c 文件的实现代码如下所示
ch395_demo.c 文件 该文件定义了2 个函数如下表所示 ①定义网络参数及连接参数
/* 本地网络信息IP地址、网关地址、子网掩码和MAC地址*/
uint8_t ch395_ipaddr[4] {192,168,1,10};
uint8_t ch395_gw_ipaddr[4] {192,168,1,1};
uint8_t ch395_ipmask[4] {255,255,255,0};
uint8_t ch395_macaddr[6] {0xB8,0xAE,0x1D,0x00,0x00,0x00};
/* 远程IP地址设置*/
uint8_t ch395_des_ipaddr[4] {47, 98, 186, 15};
static uint8_t socket0_send_buf[] {This is from CH395Q\r\n};
static uint8_t socket0_recv_buf[1024];
ch395_socket cha95_sockct_sta[8];
#define DEVICE 33057282794714357363 /* 设备号*/
#define PAW 12345678 /* 设备密码*/ch395_des_ipaddr 数组为原子云的IP 地址DEVICE 和PAW 为设备的ID 和密钥。
②显示实验信息
/**
* brief 显示实验信息
* param 无
* retval 无
*/
void ch395_show_mesg(void)
{/* LCD显示实验信息*/lcd_show_string(10, 10, 220, 32, 32, STM32, RED);lcd_show_string(10, 47, 220, 24, 24, CH395Q YuanZiYun, RED);lcd_show_string(10, 76, 220, 16, 16, ATOMALIENTEK, RED);lcd_show_string(10, 97, 200, 16, 16, KEY0: Send, BLUE);/* 串口输出实验信息*/printf(\n);printf(********************************\r\n);printf(STM32\r\n);printf(CH395Q YuanZiYun\r\n);printf(ATOMALIENTEK\r\n);printf(KEY0: Send\r\n);printf(********************************\r\n);printf(\r\n);
}显示实验信息和串口打印实验信息。
③测试实验
/*** brief 例程测试* param 无* retval 无*/
void ch395_demo(void)
{uint8_t key 0;ch395_show_mesg(); /* 显示信息*/do{ch395q_handler();}while (g_ch395q_sta.dhcp_status DHCP_STA); /* 获DHCP*//* 使能socket接口*/cha95_sockct_sta[0].socket_enable CH395Q_ENABLE;/* 设置socket接口*/cha95_sockct_sta[0].socket_index CH395Q_SOCKET_0;/* 设置目标IP地址*/memcpy(cha95_sockct_sta[0].des_ip, ch395_des_ipaddr,sizeof(cha95_sockct_sta[0].des_ip));/* 设置静态本地IP地址*/memcpy(cha95_sockct_sta[0].net_config.ipaddr, ch395_ipaddr,sizeof(cha95_sockct_sta[0].net_config.ipaddr));/* 设置静态网关IP地址*/memcpy(cha95_sockct_sta[0].net_config.gwipaddr, ch395_gw_ipaddr,sizeof(cha95_sockct_sta[0].net_config.gwipaddr));/* 设置静态子网掩码地址*/memcpy(cha95_sockct_sta[0].net_config.maskaddr, ch395_ipmask,sizeof(cha95_sockct_sta[0].net_config.maskaddr));/* 设置静态MAC地址*/memcpy(cha95_sockct_sta[0].net_config.macaddr, ch395_macaddr,sizeof(cha95_sockct_sta[0].net_config.macaddr));/* 目标端口*/cha95_sockct_sta[0].des_port 59666;/* 源端口*/cha95_sockct_sta[0].sour_port 59666;/* 设置协议*/cha95_sockct_sta[0].proto CH395Q_SOCKET_TCP_CLIENT;/* 发送数据*/cha95_sockct_sta[0].send.buf socket0_send_buf;/* 发送数据大小*/cha95_sockct_sta[0].send.size sizeof(socket0_send_buf);/* 接收数据缓冲区*/cha95_sockct_sta[0].recv.buf socket0_recv_buf;/* 接收数据大小*/cha95_sockct_sta[0].recv.size sizeof(socket0_recv_buf);/* 配置socket参数*/ch395q_socket_config(cha95_sockct_sta[0]);ch395_send_data(CH395Q_SOCKET_0, (uint8_t *)atk_decode(DEVICE,PAW),strlen((char *)atk_decode(DEVICE,PAW)));while (1){key key_scan(0);if (key KEY0_PRES){ch395_send_data(CH395Q_SOCKET_0, (uint8_t *)socket0_send_buf,strlen((char *)socket0_send_buf));}ch395q_handler();}
}从上述源码可知看出根据设备号和设备密码计算连接密钥并把连接密钥调用函数ch395_send_data 发送到原子云服务器以表示握手连接当按下KEY0_PRES 可向原子云发送数据。
下载验证
编译程序并下载到开发板上打开原子云平台https://cloud.alientek.com/如下图所示 按下开发板上的KEY0 把“This is from CH395Q\r\n”数据发送到原子云服务器当中原子云平台发送的数据可在串口上显示。