前提 开发环境

Chip : TsingMicro dt56
OS : ubuntu20.04
Compiler : gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf
Test Mechine : HAPS (FPGA)

概要 WTD驱动程序开发过程分为四个部分

  1. WTD概念,原理以及配置方式的学习
  2. 硬件操作的代码编写
  3. 驱动框架的理解
  4. 测试

一 WTD的概念

WTD全称WatchDog,用于对计算机系统的自动复位。计算机在工作时容易受到各种干扰,导致计算机程序进入死循环,跑飞,或者死机崩溃。看门狗这种硬件用于解救这种情况下的计算机。

看门狗的工作逻辑如下:
初始化时配置初值,频率,以及减到0时是否要产生中断等
开始计数后可以选择在计数减为0之前重新设置初值,也就是所谓的Tick Dog,俗称喂狗,系统正常工作
如果系统崩溃则无法置初值,WTD在计数减为0时会触发系统复位,从而保证系统工作

二 WTD 首要寄存器

WDT_CR寄存器:

Offset address: 0x00, Control Register.
对RPL域配置,设置pclk cycles 这个值决定了时钟频率,也就是计数快慢
对RMOD配置,设置响应模式,决定计数到0值时产生系统复位还是产生中断
对WDT_EN域配置,控制WDT的使能或失能

WDT_TORR寄存器

Offset address = 0x04, Timeout Range Register.

对TOP_INIT域配置,初始化超时值,在系统复位之前或者之后被写入

一套简单的配置

  1. 配置WDT_CR 失能WDT
  2. 配置WDT_TORR 设置WDT计数初始值
  3. 配置WDT_CR 使能WDT

一套复杂的配置

使用用户定义的计数器配置WDT:

  1. 通过写入看门狗控制寄存器WDT_CR禁用WDT。
  2. 配置WDT_TORR / WDT_TORR_USR / WDT_TORR_USR_INIT。
  3. 配置WDT_PAUSE在默认值和用户定义的超时值之间切换
    价值。
  4. 通过写入WDT_CR来启用WDT。
  5. 通过写入WDT_PAUSE暂停WDT。
  6. 通过写入WDT_PAUSE释放暂停,WDT继续工作。

三 硬件操作部分的代码

需实现的驱动接口列表

包含启动,停止,对WTD设值,喂狗,获取计数剩余值,重启。

static const struct watchdog_ops dw_wdt_ops = {.owner       = THIS_MODULE,.start       = dw_wdt_start,.stop       = dw_wdt_stop,.ping        = dw_wdt_ping,.set_timeout = dw_wdt_set_timeout,.get_timeleft = dw_wdt_get_timeleft,.restart = dw_wdt_restart,
};

硬件相关的宏定义 以及 WDT参数的定义

这部分代码包含了以下信息:
寄存器以及其偏移值的映射
时钟的PLCK一个脉冲的长度
响应模式
最大可设超时时间
默认超时时间
是否外界可以关闭WTD 作为模块参数可由用户加载模块式配置
wdt结构体 其继承了watchdog_device

//寄存器以及其偏移值的映射
#define WDOG_CONTROL_REG_OFFSET             0x00
#define WDOG_CONTROL_REG_WDT_EN_MASK        0x01
#define WDOG_CONTROL_REG_RESP_MODE_MASK     0x02
#define WDOG_CONTROL_REG_RESP_PULSE_LENGTH_MASK     0x07
#define WDOG_CONTROL_REG_RESP_PULSE_LENGTH_POS      (2)
#define WDOG_CONTROL_REG_RESET_MODE_MASK        0x1
#define WDOG_CONTROL_REG_RESET_MODE_POS         (1)#define WDOG_TIMEOUT_RANGE_REG_OFFSET        0x04
#define WDOG_TIMEOUT_RANGE_TOPINIT_SHIFT    4#define WDOG_CURRENT_COUNT_REG_OFFSET      0x08
#define WDOG_COUNTER_RESTART_REG_OFFSET     0x0c
#define WDOG_COUNTER_RESTART_KICK_VALUE     0x76#define WDOG_CONTROL_REG_CLEAR_INT          0x14//时钟的PLCK一个脉冲的长度
typedef enum {WDT_ResetPulseLength_2_PCLK_CYCLES = 0,WDT_ResetPulseLength_4_PCLK_CYCLES,WDT_ResetPulseLength_8_PCLK_CYCLES,WDT_ResetPulseLength_16_PCLK_CYCLES,WDT_ResetPulseLength_32_PCLK_CYCLES,WDT_ResetPulseLength_64_PCLK_CYCLES,WDT_ResetPulseLength_128_PCLK_CYCLES,WDT_ResetPulseLength_256_PCLK_CYCLES,
} eWDT_ResetPulseLength_t;//响应模式
typedef enum {WDT_SYSTEM_RESET = 0,WDT_INTERRUPT,
} eWDT_ResponseMode_t;//最大可设超时时间
/* The maximum TOP (timeout period) value that can be set in the watchdog. */
#define DW_WDT_MAX_TOP      15
//默认超时时间
#define DW_WDT_DEFAULT_SECONDS  5
#define DW_WDT_DEFAULT_RESET_PULSE_LENGTH   WDT_ResetPulseLength_64_PCLK_CYCLES
#define DW_WDT_DEFAULT_RESET_MODE   WDT_SYSTEM_RESET//是否外界可以关闭WTD  作为模块参数可由用户加载模块式配置
static bool nowayout = WATCHDOG_NOWAYOUT;
module_param(nowayout, bool, 0);
MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started ""(default=" __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");//wdt结构体 继承了watchdog_device
struct dw_wdt {void __iomem     *regs;struct clk        *clk;unsigned long      rate;struct watchdog_device wdd;struct reset_control    *rst;
};#define to_dw_wdt(wdd)    container_of(wdd, struct dw_wdt, wdd)

实现硬件操作的接口

按照调用关系排列
最基础的函数是dw_wdt_set_timeout

dw_wdt_set_timeout

该函数采取迭代的方法,先将用户所传入的top_s(秒)值转化为

static int dw_wdt_set_timeout(struct watchdog_device *wdd, unsigned int top_s)
{struct dw_wdt *dw_wdt = to_dw_wdt(wdd);int i, top_val = DW_WDT_MAX_TOP;/** Iterate over the timeout values until we find the closest match. We* always look for >=.*/for (i = 0; i <= DW_WDT_MAX_TOP; ++i)if (dw_wdt_top_in_seconds(dw_wdt, i) >= top_s) {top_val = i;break;}/** Set the new value in the watchdog.  Some versions of dw_wdt* have have TOPINIT in the TIMEOUT_RANGE register (as per* CP_WDT_DUAL_TOP in WDT_COMP_PARAMS_1).  On those we* effectively get a pat of the watchdog right here.*/writel(top_val | top_val << WDOG_TIMEOUT_RANGE_TOPINIT_SHIFT,dw_wdt->regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);wdd->timeout = dw_wdt_top_in_seconds(dw_wdt, top_val);return 0;
}

dw_wdt_ping

ping函数主要用于喂狗,往上面所说的寄存器里写值即可

static int dw_wdt_ping(struct watchdog_device *wdd)
{struct dw_wdt *dw_wdt = to_dw_wdt(wdd);writel(WDOG_COUNTER_RESTART_KICK_VALUE, dw_wdt->regs +WDOG_COUNTER_RESTART_REG_OFFSET);return 0;
}

dw_wdt_arm_system_reset

系统重启函数:配置了失能中断模式,计数到0时系统总是会重启
配置WDT_CR寄存器的EN域,使能watchdog

static void dw_wdt_arm_system_reset(struct dw_wdt *dw_wdt)
{u32 val = readl(dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);/* Disable interrupt mode; always perform system reset. */val &= ~(WDOG_CONTROL_REG_RESET_MODE_MASK << WDOG_CONTROL_REG_RESET_MODE_POS);val |= (DW_WDT_DEFAULT_RESET_MODE << WDOG_CONTROL_REG_RESET_MODE_POS);/* Enable watchdog. */val |= WDOG_CONTROL_REG_WDT_EN_MASK;writel(val, dw_wdt->regs + WDOG_CONTROL_REG_OFFSET);
}

dw_wdt_start

wdt的开启函数,调用上面俩函数,设置超时值后将,配置响应模式,系统重启

static int dw_wdt_start(struct watchdog_device *wdd)
{struct dw_wdt *dw_wdt = to_dw_wdt(wdd);dw_wdt_set_timeout(wdd, wdd->timeout);dw_wdt_arm_system_reset(dw_wdt);return 0;
}

dw_wdt_stop

配置wdt的停止函数

static int dw_wdt_stop(struct watchdog_device *wdd)
{struct dw_wdt *dw_wdt = to_dw_wdt(wdd);if (!dw_wdt->rst) {set_bit(WDOG_HW_RUNNING, &wdd->status);return 0;}reset_control_assert(dw_wdt->rst);reset_control_deassert(dw_wdt->rst);return 0;
}

四 驱动框架的理解

内核单独给WatchDog准备了一套框架,与Platform平台设备类似,WatchDog框架包含以下三部分:
驱动层watchdog_drv — 核心层watchdog_core — 设备层watchdog_ dev

前面章节遇到的框架中一般都是要自己手动创建节点或者通过程序运行时自动创建节点
对于watchdog比较特别,节点创建并登记的函数(watchdog_cdev_register)已经定义在watchdog_dev.c中,并在watchdog_dev_register函数中调用,
而watchdog_dev_register又是被__watchdog_register_device调用的,
而__watchdog_register_devicer又是被watchdog_register_device调用。
所以这个函数watchdog_register_device才是留给我们调用注册节点的接口,需要在probe函数中调用

节点注册的调用关系图

probe函数-->watchdog_register_device-->__watchdog_register_device-->watchdog_dev_register-->watchdog_cdev_register-->watchdog_miscdev.parent = wdd->parent;err = misc_register(&watchdog_miscdev);-----------------------cdev_init(&wd_data->cdev, &watchdog_fops);err = cdev_device_add(&wd_data->cdev, &wd_data->dev);

可见这里层级调用中watchdog_cdev_register注册了一个混杂设备watchdog_miscdev

static struct miscdevice watchdog_miscdev = {.minor     = WATCHDOG_MINOR,.name     = "watchdog",.fops       = &watchdog_fops,
};

其fops如下:
可以看到这里就是应用层调用文件操作函数时最后会调用到的相应的函数接口

static const struct file_operations watchdog_fops = {.owner     = THIS_MODULE,.write       = watchdog_write,.unlocked_ioctl   = watchdog_ioctl,.open     = watchdog_open,.release   = watchdog_release,
};

实现fops里的接口

write实现

拿write接口的实现为例:
最主要的功能就是调用了硬件操作部分已经实现的 watchdog_ping函数,达到喂狗的目的。

static ssize_t watchdog_write(struct file *file, const char __user *data,size_t len, loff_t *ppos)
{struct watchdog_core_data *wd_data = file->private_data;struct watchdog_device *wdd;int err;size_t i;char c;if (len == 0)return 0;/** Note: just in case someone wrote the magic character* five months ago...*/clear_bit(_WDOG_ALLOW_RELEASE, &wd_data->status);/* scan to see whether or not we got the magic character */for (i = 0; i != len; i++) {if (get_user(c, data + i))return -EFAULT;if (c == 'V')set_bit(_WDOG_ALLOW_RELEASE, &wd_data->status);}/* someone wrote to us, so we send the watchdog a keepalive ping */err = -ENODEV;mutex_lock(&wd_data->lock);wdd = wd_data->wdd;if (wdd)err = watchdog_ping(wdd);mutex_unlock(&wd_data->lock);if (err < 0)return err;return len;
}

ioctl的实现

可以将ioctl看作一个控制器,(约定好底层驱动对应哪个命令)用户调用这个函数,并指定使用哪个命令,就可以调用到哪个函数

/**    watchdog_ioctl: handle the different ioctl's for the watchdog device.*    @file: file handle to the device*    @cmd: watchdog command*    @arg: argument pointer**    The watchdog API defines a common set of functions for all watchdogs*    according to their available features.*/static long watchdog_ioctl(struct file *file, unsigned int cmd,unsigned long arg)
{struct watchdog_core_data *wd_data = file->private_data;void __user *argp = (void __user *)arg;struct watchdog_device *wdd;int __user *p = argp;unsigned int val;int err;mutex_lock(&wd_data->lock);wdd = wd_data->wdd;if (!wdd) {err = -ENODEV;goto out_ioctl;}err = watchdog_ioctl_op(wdd, cmd, arg);if (err != -ENOIOCTLCMD)goto out_ioctl;switch (cmd) {case WDIOC_GETSUPPORT:err = copy_to_user(argp, wdd->info,sizeof(struct watchdog_info)) ? -EFAULT : 0;break;case WDIOC_GETSTATUS:val = watchdog_get_status(wdd);err = put_user(val, p);break;case WDIOC_GETBOOTSTATUS:err = put_user(wdd->bootstatus, p);break;case WDIOC_SETOPTIONS:if (get_user(val, p)) {err = -EFAULT;break;}if (val & WDIOS_DISABLECARD) {err = watchdog_stop(wdd);if (err < 0)break;}if (val & WDIOS_ENABLECARD)err = watchdog_start(wdd);break;case WDIOC_KEEPALIVE:if (!(wdd->info->options & WDIOF_KEEPALIVEPING)) {err = -EOPNOTSUPP;break;}err = watchdog_ping(wdd);break;case WDIOC_SETTIMEOUT:if (get_user(val, p)) {err = -EFAULT;break;}err = watchdog_set_timeout(wdd, val);if (err < 0)break;/* If the watchdog is active then we send a keepalive ping* to make sure that the watchdog keep's running (and if* possible that it takes the new timeout) */err = watchdog_ping(wdd);if (err < 0)break;/* Fall */case WDIOC_GETTIMEOUT:/* timeout == 0 means that we don't know the timeout */if (wdd->timeout == 0) {err = -EOPNOTSUPP;break;}err = put_user(wdd->timeout, p);break;case WDIOC_GETTIMELEFT:err = watchdog_get_timeleft(wdd, &val);if (err < 0)break;err = put_user(val, p);break;case WDIOC_SETPRETIMEOUT:if (get_user(val, p)) {err = -EFAULT;break;}err = watchdog_set_pretimeout(wdd, val);break;case WDIOC_GETPRETIMEOUT:err = put_user(wdd->pretimeout, p);break;default:err = -ENOTTY;break;}out_ioctl:mutex_unlock(&wd_data->lock);return err;
}

匹配

如前面章节剖析的一样,probe函数如何才能被调用?
答:设备树上注册的.compatible和driver里的.of_match_table能够被匹配上

static const struct of_device_id dw_wdt_of_match[] = {{ .compatible = "snps,dw-wdt", },{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw_wdt_of_match);static struct platform_driver dw_wdt_driver = {.probe        = dw_wdt_drv_probe,.remove        = dw_wdt_drv_remove,.driver        = {.name    = "dw_wdt",.of_match_table = of_match_ptr(dw_wdt_of_match),.pm    = &dw_wdt_pm_ops,},
};

五 编写测试App

应用层代码需要一一检验是否驱动功能已经全部实现:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <unistd.h>
#include <time.h>
#include <getopt.h>
#include <sys/signal.h>//watchdog
#define WATCHDOG_IOCTL_BASE     'W'struct watchdog_info {unsigned int options;          /* Options the card/driver supports */unsigned int firmware_version; /* Firmware version of the card */char identity[32];     /* Identity of the board */
};#define WDIOC_GETSUPPORT        _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info)
#define WDIOC_GETSTATUS         _IOR(WATCHDOG_IOCTL_BASE, 1, int)
#define WDIOC_GETBOOTSTATUS     _IOR(WATCHDOG_IOCTL_BASE, 2, int)
#define WDIOC_GETTEMP           _IOR(WATCHDOG_IOCTL_BASE, 3, int)
#define WDIOC_SETOPTIONS        _IOR(WATCHDOG_IOCTL_BASE, 4, int)
#define WDIOC_KEEPALIVE         _IOR(WATCHDOG_IOCTL_BASE, 5, int)
#define WDIOC_SETTIMEOUT        _IOWR(WATCHDOG_IOCTL_BASE, 6, int)
#define WDIOC_GETTIMEOUT        _IOR(WATCHDOG_IOCTL_BASE, 7, int)
#define WDIOC_SETPRETIMEOUT     _IOWR(WATCHDOG_IOCTL_BASE, 8, int)
#define WDIOC_GETPRETIMEOUT     _IOR(WATCHDOG_IOCTL_BASE, 9, int)
#define WDIOC_GETTIMELEFT       _IOR(WATCHDOG_IOCTL_BASE, 10, int)#define WDIOF_OVERHEAT          0x0001  /* Reset due to CPU overheat */
#define WDIOF_FANFAULT          0x0002  /* Fan failed */
#define WDIOF_EXTERN1           0x0004  /* External relay 1 */
#define WDIOF_EXTERN2           0x0008  /* External relay 2 */
#define WDIOF_POWERUNDER        0x0010  /* Power bad/power fault */
#define WDIOF_CARDRESET         0x0020  /* Card previously reset the CPU */
#define WDIOF_POWEROVER         0x0040  /* Power over voltage */
#define WDIOF_SETTIMEOUT        0x0080  /* Set timeout (in seconds) */
#define WDIOF_MAGICCLOSE        0x0100  /* Supports magic close char */
#define WDIOF_PRETIMEOUT        0x0200  /* Pretimeout (in seconds), get/set */
#define WDIOF_KEEPALIVEPING     0x8000  /* Keep alive ping reply */#define WDIOS_DISABLECARD       0x0001  /* Turn off the watchdog timer */
#define WDIOS_ENABLECARD        0x0002  /* Turn on the watchdog timer */
#define WDIOS_TEMPPANIC         0x0004  /* Kernel panic on temperature trip */int wdt_fd;
int time_out = 5;
#define DEFAULT_PING_RATE    1
void stop_signal()
{int val = 0 , ret = 0 ;val = WDIOS_DISABLECARD ;ret = ioctl(wdt_fd, WDIOC_SETOPTIONS, &val) ;if (ret < 0)printf("ioctl WDIOC_GETSUPPORT failed with %d.\n", ret);printf("===watchdow will be closed===\n") ;close(wdt_fd) ;exit(0);}int main(int argc, char *argv[])
{int ret;static int count = 0;struct watchdog_info wdt_info;unsigned int ping_rate = DEFAULT_PING_RATE;signal(SIGINT, stop_signal) ;wdt_fd = open("/dev/watchdog0", O_RDWR);if(wdt_fd < 0){printf("open /dev/watchdog0 failed.\n");}/* get watchdog infomation struct */ret = ioctl(wdt_fd, WDIOC_GETSUPPORT, &wdt_info);if (ret < 0)printf("ioctl WDIOC_GETSUPPORT failed.\n");else{printf("options = 0x%x,id = %s\n", wdt_info.options, wdt_info.identity);}ioctl(wdt_fd, WDIOC_SETTIMEOUT, &time_out);if (ret < 0)printf("ioctl WDIOC_SETTIMEOUT failed.\n");while(1){if(count > 10){printf("unfood watchdog, count = %d \n",count++);}else{ioctl(wdt_fd,WDIOC_KEEPALIVE,NULL);printf("food watchdog, count = %d \n",count++);}sleep(DEFAULT_PING_RATE);}    close(wdt_fd);return 0;
}

嵌入式Linux驱动开发9---WTD驱动程序以及测试过程记录相关推荐

  1. 嵌入式Linux驱动开发【学习小结】

    文章目录 前言 一.嵌入式Linux驱动程序和单片机裸奔有啥区别? 二.为什么需要嵌入式Linux驱动开发 三.驱动程序框架大致演变过程 总结 前言 随着去嵌入式设备资源不断丰富,主频不断升高,搭载操 ...

  2. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED模板驱动程序的改造:设备树

    文章目录 前言 1.驱动的三种编写方法 2.怎么使用设备树写驱动程序 2.1.设备树节点要与platform_driver能匹配 2.2.修改platform_driver的源码 3.实验和调试技巧 ...

  3. 嵌入式Linux驱动开发 02:将驱动程序添加到内核中

    文章目录 目的 基础说明 添加到内核中 Kconfig Makefile 驱动程序 编译与测试 模块方式 编译到内核中 总结 目的 在上一篇文章 <嵌入式Linux驱动开发 01:基础开发与使用 ...

  4. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之Pinctrl子系统和GPIO子系统的使用

    文章目录 前言 1.Pinctrl子系统 1.1.为什么有Pinctrl子系统 1.2.重要的概念 1.3.代码中怎么引用pinctrl 2.GPIO子系统 2.1.为什么有GPIO子系统 2.2.在 ...

  5. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之按键驱动框架

    文章目录 前言 1.APP怎么读取按键值 1.1.查询方式 1.2.休眠-唤醒方式 1.3.poll方式 1.3.异步通知方式 1.5. 驱动程序提供能力,不提供策略 2.按键驱动程序框架--查询方式 ...

  6. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之设备树模型

    文章目录 前言 1.设备树的作用 2.设备树的语法 2.1.设备树的逻辑图和dts文件.dtb文件 2.1.1.1Devicetree格式 1DTS文件的格式 node的格式 properties的格 ...

  7. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之总线设备驱动模型

    文章目录 前言 1.驱动编写的三种方法 1.1.传统写法 1.2.总线驱动模型 1.3.设备树驱动模型 2.Linux实现分离:Bus/Dev/Drv模型 2.1.Bus/Dev/Drv模型 2.2. ...

  8. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离

    文章目录 前言 1.分离设计 驱动程序分析---程序分层 通用驱动程序---面向对象 个性化驱动程序---分离 APP 程序分析 前言 韦东山嵌入式Linux驱动开发基础知识学习笔记 文章中大多内容来 ...

  9. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED驱动框架--面向对象、分层设计思想

    文章目录 前言 1.LED驱动程序框架 1.1.对于LED驱动,我们想要什么样的接口? 1.2.LED驱动要怎么写,才能支持多个板子?分层写 1.3.程序分析 驱动程序 应用程序 Makefile 1 ...

  10. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之第一个驱动

    文章目录 前言 1.Hello驱动 1.1.APP打开的文件在内核中如何表示? 1.2.打开字符设备节点时,内核中也有对应的struct file 1.3.如何编写驱动程序? 1.4.驱动程序代码 1 ...

最新文章

  1. StartOS 5.0 正式版发布
  2. python函数代码的复用_Python__函数和代码复用
  3. java notify唤醒原理_Java wait和notify虚假唤醒原理
  4. 结构体变量和结构体指针变量作为函数参数传递问题
  5. win10无法成功完成操作,文件包含病毒怎么办
  6. seL4操作系统基础06:dataport interface与seL4SharedData connector
  7. 超实用一键破解网页不能复制/右键菜单限制的 Bookmarklet 收藏夹书签小工具
  8. 东芝服务器报错误代码维修,实战维修 东芝复印机故障维修详解
  9. 博客园博客使用无觅插件
  10. python实现定时任务的8种方式详解
  11. 这个策略曾赚000万美元
  12. 深度学习-活体检测发展之数据篇(二)
  13. React 函数式组件缓存原理
  14. 使用js乘法 精度错乱 使用Decimal插件处理格式问题
  15. DICOM:基于JMeter+dcm4che2测试PACS服务器性能的解决方案(续篇)
  16. 招商银行fintech选拔课题---《基于微博爬虫的舆情分析》上
  17. 计算机编程语言排行榜—TIOBE世界编程语言排行榜(2020年5月份最新版)
  18. 非常好看的二次元导航源码
  19. 北邮人导航html代码,北邮人导航 · BYR-Navi 的安装与配置
  20. 通达OA系统对接 单点登录平台使用和开发手册

热门文章

  1. d3dcompiler_43.dll缺失怎么修复
  2. 每天5分钟玩转Kubernetes | Cluster IP底层实现
  3. 计算机配置好坏怎么看,电脑配置怎么看 史上最全的查看电脑配置好坏方法
  4. web网页设计期末课程大作业:环境保护主题网站设计——农业三级带表单带js(14页)HTML+CSS+JavaScript
  5. 【小知识】java中的this.name=name是什么意思啊??this 有什么用啊,再哪出现?
  6. Python面向对象实现栈和图书管理系统
  7. 互联网电视变身哄娃神器:YOYO搜片真方便
  8. 1079 活字印刷
  9. 网络请求urllib库使用总结
  10. MongoDB副本集的部署与操作