LwIP源码分析(4):netif网卡驱动初始化代码分析
admin
2024-04-27 21:49:49
0

本篇文章以以太网网卡驱动为例,分析SDK中的TCP echo回传源码,主要是对netif的初始化进行分析,从而深入地理解LwIP中netif的作用。

  • 开发板:NXP Kinetis
  • 操作系统:FreeRTOS,LwIP版本:2.2.0
  • 文章中分析的代码均把没有用到的宏定义相关的代码去掉了,为了方便阅读,代码都是预编译完替换了#define后的代码,有些没有用的代码也去掉

文章目录

  • 1 TCP_ECHO程序
  • 2 netif结构体
  • 3 netif_add函数详解
  • 4 netif_set_default和netif_setup
  • 5 总结

1 TCP_ECHO程序

首先来看一下TCP回显的代码,大概流程就是初始化网卡netif,然后创建一个任务接收TCP数据并回显:

const mdio_operations_t enet_ops = {.mdioInit  = ENET_MDIO_Init,.mdioWrite = ENET_MDIO_Write,.mdioRead  = ENET_MDIO_Read,.mdioWriteExt = NULL,.mdioReadExt  = NULL
};const phy_operations_t phyksz8081_ops = {.phyInit            = PHY_KSZ8081_Init,.phyWrite           = PHY_KSZ8081_Write,.phyRead            = PHY_KSZ8081_Read,.getAutoNegoStatus  = PHY_KSZ8081_GetAutoNegotiationStatus,.getLinkStatus      = PHY_KSZ8081_GetLinkStatus,.getLinkSpeedDuplex = PHY_KSZ8081_GetLinkSpeedDuplex,.setLinkSpeedDuplex = PHY_KSZ8081_SetLinkSpeedDuplex,.enableLoopback     = PHY_KSZ8081_EnableLoopback
};static mdio_handle_t mdioHandle = {.ops = &enet_ops};
static phy_handle_t phyHandle   = {/* 开发板以太网0的物理地址 */.phyAddr = 0x00, /* MDIO(Management Data Input/Output)操作结构体 */.mdioHandle = &mdioHandle, /* 以太网收发器phyksz8081的驱动 */.ops = &phyksz8081_ops
};
/* 初始化函数 */
static void stack_init(void *arg)
{static struct netif netif;ip4_addr_t netif_ipaddr, netif_netmask, netif_gw;ethernetif_config_t enet_config = {.phyHandle  = &phyHandle,.macAddress = {0x02, 0x12, 0x13, 0x10, 0x15, 0x11},};mdioHandle.resource.csrClock_Hz = CLOCK_GetFreq(kCLOCK_CoreSysClk);/* 设置IP地址,子网掩码和网关 */IP4_ADDR(&netif_ipaddr, 192, 168, 0, 102);IP4_ADDR(&netif_netmask, 255, 255, 255, 0);IP4_ADDR(&netif_gw, 192, 168, 0, 100);tcpip_init(NULL, NULL);netifapi_netif_add(&netif, &netif_ipaddr, &netif_netmask, &netif_gw, &enet_config, ethernetif0_init,tcpip_input);netifapi_netif_set_default(&netif);netifapi_netif_set_up(&netif);/* 创建TCP_ECHO任务进行回显 */tcpecho_init();vTaskDelete(NULL);
}

其中tcpip_init在我的另一篇博客tcpip_init和tcpip_thread函数分析中我简单地分析过。

可以看到上面主要是传入一些参数,然后初始化netif结构体。所以我们就从这个结构体出发来一探究竟:

  • 这里不对IPV6做分析,故在下面代码中将与IPV6相关的结构体成员的去掉以易于查看

2 netif结构体

struct netif {/* 如果程序中有多个网卡,如用PPP,以太网等连接,则不同的网卡netif用该链表连接 */struct netif *next;/* IPV4地址配置(网络字节序):IP,子网掩码,网关 */ip_addr_t ip_addr;ip_addr_t netmask;ip_addr_t gw;/* 由网络设备驱动调用该函数在TCP/IP协议栈上传递一组数据包 */netif_input_fn input;/* 由IP模块调用来解析硬件地址再发送一组数据包,对于以太网的物理层来说,该函数为etharp_output */netif_output_fn output;/* 当想要发送一组数据包时,在ethernet_output中调用,它输出链路层中的原始pbuf */netif_linkoutput_fn linkoutput;/* 当链路层网卡状态改变时(连接/断开)时调用 */netif_status_callback_fn status_callback;/* 当链路层网卡发起连接或断开时调用 */netif_status_callback_fn link_callback;/* 当一个netif网卡结构体移除时调用 */netif_status_callback_fn remove_callback;/* 供用户传递数据使用 */void *state;/* 存放一些客户端的数据,如DHCP客户端结构体 */void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];/* netif的主机名 */const char*  hostname;/* 每个netif都可以使能/失能校验和的生成和校验 */u16_t chksum_flags;/* 最大传输单元(单位:bytes) */u16_t mtu;/* 链路层的硬件地址 */u8_t hwaddr[NETIF_MAX_HWADDR_LEN];/* 硬件地址的长度 */u8_t hwaddr_len;/* 网卡状态信息标志位,包括网卡功能使能、广播使能、 ARP使能等标志位 */u8_t flags;/** 该netif的名字 */char name[2];/* 用来标示使用同种驱动类型的不同网卡的数量 */u8_t num;/* 该函数用来添加或删除以太网MAC层组播的过滤表中的条目 */netif_igmp_mac_filter_fn igmp_mac_filter;/* ACD模块:有关自动IP获取 */struct acd *acd_list;/* VLAN PCP相关 */struct netif_hint *hints;/* 环回相关:略 */
};

部分不好理解的参数具体在代码中用到了我们再来研究。我们注意到网卡接口netif是在netifapi_netif_add函数中进行初始化的,现在来看看这个函数:

3 netif_add函数详解

struct netifapi_msg {struct tcpip_api_call_data call;struct netif *netif;union {struct {const ip4_addr_t * ipaddr;const ip4_addr_t * netmask;const ip4_addr_t * gw;void *state;netif_init_fn init;netif_input_fn input;} add;struct {netifapi_void_fn voidfunc;netifapi_errt_fn errtfunc;} common;struct {char *name;u8_t index;} ifs;} msg;
};err_t netifapi_netif_add(struct netif *netif, const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input)
{err_t err;struct netifapi_msg msg;msg.netif = netif;msg.msg.add.ipaddr = ipaddr;msg.msg.add.netmask = netmask;msg.msg.add.gw = gw;msg.msg.add.state = state;msg.msg.add.init = init;msg.msg.add.input = input;/* msg.call为结构体第一个元素的地址,也就是结构体的地址 */err = tcpip_api_call(netifapi_do_netif_add, &msg.call);return err;
}

tcpip_api_call实际上就是执行netifapi_do_netif_add函数,然后在执行前获得互斥锁,执行后释放互斥锁,这样可以让用户在自己的代码中实现LwIP的一些操作。

err_t tcpip_api_call(tcpip_api_call_fn fn, struct tcpip_api_call_data *call)
{err_t err;sys_lock_tcpip_core();err = fn(call);sys_unlock_tcpip_core();return err;
}

现在来看看netifapi_do_netif_add函数:

static err_t netifapi_do_netif_add(struct tcpip_api_call_data *m)
{struct netifapi_msg *msg = (struct netifapi_msg *)(void *)m;if (!netif_add( msg->netif,msg->msg.add.ipaddr,msg->msg.add.netmask,msg->msg.add.gw,msg->msg.add.state,msg->msg.add.init,msg->msg.add.input)) {return ERR_IF;} return ERR_OK;
}

所以到头来就是调用了netif_add函数,由于例程中有操作系统所以需要考虑互斥,现在来看看netif_add

struct netif* netif_add(struct netif *netif,const ip4_addr_t *ipaddr, const ip4_addr_ *netmask, const ip4_addr_t *gw, void *state, netif_init_fn init, netif_input_fn input)
{sys_check_core_locking();/* 如果IP、子网掩码、网关为0的话,赋一个默认值,地址用32位数表示 */if (ipaddr == 0) {ipaddr = ((&ip_addr_any));}if (netmask == 0) {netmask = ((&ip_addr_any));}if (gw == 0) {gw = ((&ip_addr_any));}/* 将netif结构体中的IP、子网掩码、网关清零 */((&netif->ip_addr)->addr = 0);((&netif->netmask)->addr = 0);((&netif->gw)->addr = 0);/* 设置默认output函数为netif_null_output_ip4,该函数中没有内容 */netif->output = netif_null_output_ip4;/* 设置结构体的初始值 */netif->mtu = 0;netif->flags = 0;memset(netif->client_data, 0, sizeof(netif->client_data));/* 用户传的自定义传输,这里为enet_config  */netif->state = state;/* 记录netif网卡的数量 */netif->num = netif_num;/* 网卡向TCP协议栈发送数据的函数,这里为tcpip_input */netif->input = input;/* 设置 */netif_set_addr(netif, ipaddr, netmask, gw);     //(1)/* 调用前面传入的init参数(函数),即ethernetif0_init */if (init(netif) != ERR_OK) {                    //(2)return 0;}/* 前面netif->num已经赋值为netif_num,这里遍历整个netif_list寻找一个唯一的num给当前的netif */struct netif *netif2;do {if (netif->num == 255) {netif->num = 0;}for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) {if (netif2->num == netif->num) {netif->num++;break;}}} while (netif2 != NULL);/* netif_num用来记录上次分配的netif->num+1,方便下次分配 */if (netif->num == 254) {netif_num = 0;} else {netif_num = (u8_t)(netif->num + 1);}/* 将当前netif结构体加入netif_list链表中 */netif->next = netif_list;netif_list = netif;return netif;
}

(1)netif_set_addr

void netif_set_addr(struct netif *netif, const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw)
{ip_addr_t *old_nm = 0;ip_addr_t *old_gw = 0;ip_addr_t old_addr;int remove;sys_check_core_locking();/* 再判断IP、子网掩码和网关是否为0,是的话设置为ip_addr_any:该部分代码略 *//* 如果没有设置IP地址或者设置为ip_addr_any,则remove为真 */remove = ((ipaddr) == 0 || ((*(ipaddr)).addr == ((u32_t)0x00000000UL)));if (remove) {/* 检查IP和之前netif中设置的是否一样,若不一样,则保存之前的IP到old_addr* 然后调用tcp_netif_ip_addr_changed修改tcp_active_pcbs和tcp_bound_pcbs* 两个TCP链表,最后再判断如何在listen之前的地址,改为listen新设置的地址*/netif_do_set_ipaddr(netif, ipaddr, &old_addr);}/* 设置子网掩码:仅仅修改netif结构体中的netmask项 */netif_do_set_netmask(netif, netmask, old_nm));/* 设置网关:仅仅修改netif结构体中的gateway项 */netif_do_set_gw(netif, gw, old_gw);/* 如果前面没有设置IP,则最后设置,这样做的原因:移除地址前必须先修改子网掩码和网关 * 以保证tcp RST段可以正确地发送,可以在最前面设置是因为remove表示该netif没有建立连接*/if (!remove) {netif_do_set_ipaddr(netif, ipaddr, &old_addr);}
}

(2)ethernetif0_init

err_t ethernetif0_init(struct netif *netif)
{static struct ethernetif ethernetif_0;__attribute__((aligned((16U)))) static enet_rx_bd_struct_t rxBuffDescrip_0[5];__attribute__((aligned((16U)))) static enet_tx_bd_struct_t txBuffDescrip_0[3];__attribute__((aligned((16U)))) static rx_buffer_t rxDataBuff_0[5*2];__attribute__((aligned((16U)))) static tx_buffer_t txDataBuff_0[3];ethernetif_0.RxBuffDescrip = &(rxBuffDescrip_0[0]);ethernetif_0.TxBuffDescrip = &(txBuffDescrip_0[0]);ethernetif_0.RxDataBuff = &(rxDataBuff_0[0]);ethernetif_0.TxDataBuff = &(txDataBuff_0[0]);return ethernetif_init(netif, ðernetif_0, ethernetif_get_enet_base(0U), (ethernetif_config_t *)netif->state);
}

可以看到ethernetif_0就是声明了几个数组填充到ethernetif结构体中,然后调用ethernetif_init来初始化以太网:

err_t ethernetif_init(struct netif *netif,struct ethernetif *ethernetif,void *enetBase,const ethernetif_config_t *ethernetifConfig)
{/* netif->state赋值为ethernetif0_init()中声明的ethernetif_0结构体 */netif->state = ethernetif;netif->name[0] = 'e';netif->name[1] = 'n';/* output函数为解析硬件地址并发送数据包,以太网的output函数是etharp_output */netif->output = etharp_output;/* 以太网输出链路层中的原始pbuf的函数为ethernetif_linkoutput */netif->linkoutput = ethernetif_linkoutput;/* 设置ethernetif->base函数参数中的enetBase,即芯片中以太网的物理地址 */*ethernetif_enet_ptr(ethernetif) = enetBase;/* 设置MAC硬件地址长度 */netif->hwaddr_len = 6;/* ethernetifConfig即netif_add函数传的用户变量state,实际上是enet_config */memcpy(netif->hwaddr, ethernetifConfig->macAddress, 6U);/* 设置以太网MTU */netif->mtu = 1500;/* 设置以太网Braodcast、ARP和LinkUp的Flag */netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;/* 以太网初始化函数 */ethernetif_enet_init(netif, ethernetif, ethernetifConfig);return ERR_OK;
}

可以看到ethernetif_init就是设置了一些参数,最后调用ethernetif_enet_init来初始化,最后我们来看看这个函数做了什么事

void ethernetif_enet_init(struct netif *netif,struct ethernetif *ethernetif,const ethernetif_config_t *ethernetifConfig)
{enet_config_t config;uint32_t sysClock;enet_buffer_config_t buffCfg[1U];phy_speed_t speed;phy_duplex_t duplex;int i;/* 接收buffer描述符号码 */buffCfg[0].rxBdNumber = 5;/* 发送buffer描述符号码 */buffCfg[0].txBdNumber = 3;/* 接收buffer的对齐字节数 */buffCfg[0].rxBuffSizeAlign = sizeof(rx_buffer_t);/* 发送buffer的对齐字节数 */buffCfg[0].txBuffSizeAlign = sizeof(tx_buffer_t);/* 接收buffer描述符的起始地址(ethernetif0_init中声明的数组地址) */buffCfg[0].rxBdStartAddrAlign = &(ethernetif->RxBuffDescrip[0]);/* 发送buffer描述符的起始地址(ethernetif0_init中声明的数组地址) */buffCfg[0].txBdStartAddrAlign = &(ethernetif->TxBuffDescrip[0]);/* 接收buffer的起始地址,NULL表示该buffer由回调函数分配 */buffCfg[0].rxBufferAlign = NULL;/* 发送buffer的起始地址(ethernetif0_init中声明的数组地址) */buffCfg[0].txBufferAlign = &(ethernetif->TxDataBuff[0][0]);/* 发送帧信息的起始地址 */buffCfg[0].txFrameInfo = NULL;/* 接收buffer的cache维护 */buffCfg[0].rxMaintainEnable = 1;/* 发送buffer的cache维护 */buffCfg[0].txMaintainEnable = 1;/* csrClock_Hz在前面stack_init()函数中初始化 */sysClock = ethernetifConfig->phyHandle->mdioHandle->resource.csrClock_Hz;/* 获取默认配置结构体:MII mode,全双工,100Mbps等 */ENET_GetDefaultConfig(&config);/* 仅使用一个ring */config.ringNum = 1U;/* 接收buffer的分配函数:事先分配好ENET_RXBUFF_NUM个数组作为接收buffer */config.rxBuffAlloc = ethernetif_rx_alloc;/* 接收buffer的释放函数 */config.rxBuffFree = ethernetif_rx_free;/* netif结构体作为用户参数 */config.userData = netif;/* 调用之前填入的硬件上的以太网收发器phyksz8081的ops中的初始化函数PHY_KSZ8081_Init对网卡进行初始化* 然后调用PHY_KSZ8081_GetAutoNegotiationStatus和PHY_KSZ8081_GetLinkStatus判断是否初始化成功* 若初始化成功则调用PHY_KSZ8081_GetLinkSpeedDuplex获得交互后实际设置的网卡速度和全/半双工*/ethernetif_phy_init(ethernetif, ethernetifConfig, &speed, &duplex);/* 将获取的网卡速度和全/半双工状态记录到config结构体中 */config.miiSpeed = (enet_mii_speed_t)speed;config.miiDuplex = (enet_mii_duplex_t)duplex;uint32_t instance;/* 硬件上ENET的基地址 */static ENET_Type *const enetBases[] = { ((ENET_Type *)(0x400C0000u)) };/* 以太网硬件发送中断IRQ */static const IRQn_Type enetTxIrqId[] = { ENET_Transmit_IRQn };/* 以太网硬件接收中断IRQ */static const IRQn_Type enetRxIrqId[] = { ENET_Receive_IRQn };/* 创建一个事件标志位来处理多个发送请求 */ethernetif->enetTransmitAccessEvent = xEventGroupCreate();ethernetif->txFlag = 0x1;/* 打开TX/RX Frame中断、TX Buffer中断和Late Collision(在本该发生collision的时间窗口结束后产生collision)中断 */config.interrupt |=kENET_RxFrameInterrupt | kENET_TxFrameInterrupt | kENET_TxBufferInterrupt | kENET_LateCollisionInterrupt;/* 回调函数:处理以太网数据的输入和发送时enetTransmitAccessEvent事件位的管理 */config.callback = ethernet_callback;/* 芯片支持一个/多个以太网,遍历所有以太网的物理地址,若和当前初始化的以太网地址匹配,则设置发送/接收IRQ的优先级 */for (instance = 0; instance < (sizeof(enetBases) / sizeof((enetBases)[0])); instance++){if (enetBases[instance] == ethernetif->base){__NVIC_SetPriority(enetRxIrqId[instance], (6U));__NVIC_SetPriority(enetTxIrqId[instance], (6U));break;}}/* 前面提到的ENET_RXBUFF_NUM个分配好的接收buffer需要初始化 */for (i = 0; i < ENET_RXBUFF_NUM; i++){/* 设置释放函数 */ethernetif->RxPbufs[i].p.custom_free_function = ethernetif_rx_release;/* 原始数据buffer */ethernetif->RxPbufs[i].buffer = &(ethernetif->RxDataBuff[i][0]);/* 标记是否使用 */ethernetif->RxPbufs[i].buffer_used = 0;/* 网卡netif结构体 */ethernetif->RxPbufs[i].netif = netif;}/* 使能ENET时钟并复位ENET,最后调用ENET_Up函数,完成如下工作:* 1.检查前面的发送/接收buffer描述符的合法性;* 2.根据配置设置MAC控制器相关寄存器;* 3.保存buffCfg的部分到ethernetif->handle,再将该handle保存到变量s_ENETHandle供后续使用,最后设置中断处理函数 */ENET_Init(ethernetif->base, ðernetif->handle, &config, &buffCfg[0], netif->hwaddr, sysClock);/* 设置ENET->RDAR寄存器以开启以太网的数据读取 */ENET_ActiveRead(ethernetif->base);
}

这里的初始化还是比较复杂的,但无非就是根据用户的配置对ENET的寄存器进行相应的配置,然后将一些软件上的配置,如buffer,保存在本地供后续以太网收发时使用这些变量。如果要深入了解ENET的寄存器,建议详细阅读ENET_Up函数。

4 netif_set_default和netif_setup

netif_add一样,netifapi_netif_set_defaultnetifapi_netif_set_up最终将调用netif_set_defaultnetif_setup。最后来看看这两个函数:

void netif_set_default(struct netif *netif)
{netif_default = netif;
}

顾名思义,netif_set_default就是设置默认网卡,LwIP支持多个网卡同时使用,比如一个用以太网上网,一个用4G PPP拨号上网,但LwIP要怎么区分当前使用哪个网卡呢?就是通过netif_default


void netif_set_up(struct netif *netif)
{if(!(netif->flags & NETIF_FLAG_UP)){/* 设置该netif的状态为UP */netif_set_flags(netif, NETIF_FLAG_UP);/* netif状态的变化在一些特定情况下,还需要通知ARP/IGMP/MLD/RS层* 这里会调用etharp_gratuitous来发送一个ARP请求包来请求IP地址,然后设置到netif结构体* /netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV4 | NETIF_REPORT_TYPE_IPV6);}
}

5 总结

本文主要介绍了LwIP中netif网卡的初始化代码流程,如果想深入了解LwIP协议中的以太网,最好还是要先理解以太网协议,比如前面phyksz8081网卡是怎么根据参考手册进行配置的、用户设置的哪些参数需要设置到芯片的ENET寄存器中。这个如果后续有时间,我会专门写一个博客进行介绍。

本文中一些变量的作用也不太明显,光看名字看不出来是干什么的,也没有在我们分析的过程中再出现过,我们也不可能一个个刨根问底地进行分析。但是在后续代码分析过程中遇到了这些变量,回过头来看的时候,我们便会恍然大悟,或者再回来补充这些。

相关内容

热门资讯

奉献爱心的成语 奉献爱心的成语春风送暖 体贴入微 无微不至 雪中送炭关怀备至嘘寒问暖
安徒生在1827年发表过《垂死... 安徒生在1827年发表过《垂死的小孩》这篇文章吗问一下、帮帮忙!《垂死的小孩》是一首诗,是安徒生18...
跆拳道比赛的进攻技术是怎样的? 跆拳道比赛的进攻技术是怎样的?跆拳道比赛的进攻技术跆拳道比赛的进攻技术包括拳攻和踢法进攻两类。现代竞...
本人书荒,求几本书 本人书荒,求几本书我最近在看紫玉钗街诡怪传说。我觉得还不错
情玫公寓和紫色蜜桃哪个更加好 ... 情玫公寓和紫色蜜桃哪个更加好 会不会有什么问题 里面的女的是不是都是莫须有的都是骗人的,想骗取你的钱...
求一早期的动画片名称,每集一个... 求一早期的动画片名称,每集一个单独故事,似乎是世界怪异故事集还有一组头发的女主人公为了买礼物的演员卖...
无双大蛇各武将的终极武器是什么... 无双大蛇各武将的终极武器是什么名字?正在收集终极武器,已经弄到赵云.周泰.织田信长的.其他武将的终极...
你觉得,你们遇到最无奈的时候,... 你觉得,你们遇到最无奈的时候,是什么时候?一个男人最无奈的时候就是在最没能力的时候遇到最想照顾的人,...
我们常把那些对事物只有一知半解... 我们常把那些对事物只有一知半解却喜欢在人前卖弄的人叫做什么哦,这种人我们这里叫半吊子,就是半瓶醋,一...
砸锅卖铁去上学谁是背后反派 砸锅卖铁去上学谁是背后反派 肖伊莱。《砸锅卖铁去上学》是由奇迹文学城作者红刺北写作的一篇女强爽文...
为什么让我漩涡中挣脱 为什么让我漩涡中挣脱这个还是你自己的心理的想法,想开,自然好了
苍山洱海的任务从哪儿开始做啊 苍山洱海的任务从哪儿开始做啊从地图的右上方莫雨少爷开始任务。地图北边路口
如果你可以选择自己的人生道路,... 如果你可以选择自己的人生道路,你会如何抉择?我会选择从初中就开始奋斗的一种人生道路,不过是那种见过世...
哪种五笔好? 哪种五笔好?用万能五笔啊功能更强大!五笔加加 搜狗呵呵,建议你使用极点五笔。。。。敲一下空格,能切换...
为什么和陈伟霆一样帅的人 总是... 为什么和陈伟霆一样帅的人 总是被女生盯着看?反而女生不敢和他说话爱美之心人皆有之啊和陈伟霆一样帅的人...
算命能不能改命 算命能不能改命算命的不能够改命,你没听说过有一句话吗,三分天注定,七分靠打拼,也就是说有三分就是靠天...
谁知道赛尔号怎么进入夜魔古堡 谁知道赛尔号怎么进入夜魔古堡登录赛尔号,打开左上角的航行日志,选择右侧栏第一个圣甲永夜,再点击图中间...
爱情公寓5里人人都爱吕小布是哪... 爱情公寓5里人人都爱吕小布是哪一集?爱情公寓5里人人都爱吕小布,是在第13集上。有好几集都有好像在3...
不败是什么意思? 不败是什么意思?包含成功和从未挑战两种意思
叔向见韩宣子中栾武子等人的事例... 叔向见韩宣子中栾武子等人的事例。分析刘禹锡和韩宣子为人的异同刘禹锡是一个洁身自好的人,韩宣子是一个清...