IMX6ULL以太网卡移植与驱动分析
IMX6ULL以太网卡移植与驱动分析
一、嵌入式以太网硬件基础知识
一般如果说一款SOC支持以太网,那么就是说SOC里集成了MAC芯片。而要想完成网络的通信不仅需要MAC还需要PHY芯片。
MAC与PHY的区别:
PHY(物理层)芯片负责将网络数据在物理层面上转换为电子信号或光信号,以便在网络中传输。它处理数据的物理传输,包括电气、光学和无线形式,并且可以处理一些基本的信号调制和解调任务。例如,以太网中的PHY芯片将数字数据转换为模拟信号以进行电气传输,或将数字数据转换为光信号以进行光纤传输。
MAC(媒体访问控制)芯片负责管理数据在网络中的传输和访问。它处理数据的逻辑传输,决定哪个设备可以发送数据、何时发送数据、如何发送数据等。例如,以太网中的MAC芯片负责实现CSMA/CD协议来协调设备之间的传输,防止数据碰撞。
简单的说就是MAC芯片是用于控制数据传输的(它的作用其实与I2C,SPI控制器差不多),而PHY的作用主要是将数据转化成物理的形式传输出去。
内置MAC与不内置MAC的区别:
内置MAC:
①、内部 MAC 外设会有专用的加速模块,比如专用的 DMA,加速网速数据的处理。
②、网速快,可以支持 10/100/1000M 网速。
③、外接 PHY 可选择性多,成本低。
硬件连接图:
不内置MAC:(如三星平台一般都是不内置的)
它们使用的芯片是MAC+PHY一体的(如:DM9000)
缺点:速度慢,成本高
硬件连接图:
由于我们是IMX6ULL的平台,内部支持了MAC,所以我们现在讨论内置MAC的连接图。
MII(Media Independent Interface)
作用:用于MAC与PHY芯片之间传输数据。
RMII(Reduced Media Independent Interface)
精简的MII(比MII少了9根线)
TX_EN**:**发送使能信号。
**TXD[1:0]****:**发送数据信号线,一共 2 根。
RXD[1:0]:接收数据信号线,一共 2 根。
CRS_DV**:**相当于 MII 接口中的 RX_DV 和 CRS 这两个信号的混合。
REF_CLK**:**参考时钟,由外部时钟源提供, 频率为 50MHz。
现在一般都不使用MII了。(IMX使用的就是RMII)
MDIO(管理数据输入输出接口)
一个简单的两线串行接口,一根 MDIO 数据线,一根 MDC 时钟线。驱动程序可以通过 MDIO 和
MDC 这两根线访问 PHY 芯片的任意一个寄存器。MDIO 接口支持多达 32 个 PHY。同一时刻
内只能对一个 PHY 进行操作,那么如何区分这 32 个 PHY 芯片呢?和 IIC 一样,使用器件地址
即可。同一 MDIO 接口下的所有 PHY 芯片,其器件地址不能冲突,必须保证唯一,具体器件
地址值要查阅相应的 PHY 数据手册。
RJ45接口
RJ45接口的作用是用于供网线插入的,一般RJ45接口与PHY芯片连接在一起,但是中间需要一个网络变压器,网络变压器用于隔离
以及滤波等,网络变压器也是一个芯片。(现在一般RJ45接口内部集成了变压器,但是我们还是得确定一下是否集成了)
最终内部集成MAC得以太网接口:
二、清楚IMX6ULL的硬件信息
MAC:
I.MX6ULL 内部自
带的 ENET 外设其实就是一个网络 MAC,支持 10/100M。实现了三层网络加速,用于加速那些
通用的网络协议,比如 IP、TCP、UDP 和 ICMP 等,为客户端应用程序提供加速服务。
PHY:
其实对于我们来说PHY才是重点,正如I2C控制器的控制器不是我们重点,而从设备才是,PHY就是MAC的从设备。
PHY 是 IEEE 802.3 规定的一个标准模块,它的前16位寄存器是指定好的,可以提供查看IEEE 802.3英文文档查看,而后16位可以由不同厂家自定义,但是前16位已经包含了基本的通信信息。所以在内核中有一种通用的PHY驱动,它虽然不能驱动厂家的特殊功能,但是基本上的通信功能是能胜任的。(不一定会成功,需要我们调试)
SR8201F
正点原子使用的就是这款PHY
PHY 地址设置:
正点原子 ALPHA 开发板 ENET1 网络的 SR8201F 上的 LED1/PHYAD1 引脚上拉,LED0/PHYDAD0 引脚下来下拉,因此 ENET1 上的 SR8201F 地址为 0X02。ENET2 网络上的SR8201F 的 LED1/PHYAD1 引脚下拉,LED0/PHYAD0 引脚上拉,因此 ENET2 上的 SR8201F
地址为 1。
SR8021F 内部寄存器:
我们说的配置 PHY 芯片,重点就是配置 BCR 寄存器
三、IMX6ULL的网卡驱动(我们默认是大概清楚了内核网络驱动框架的,可以通过看书了解)我们以驱动网卡2为例(1/2都一样)
1.确定设备树:
1.控制器驱动(imx6ull.dtsi)
此节点可以参考Documentation/devicetree/bindings/net/fsl-fec.txt
fec2: ethernet@020b4000 {
compatible = "fsl,imx6ul-fec", "fsl,imx6q-fec";
reg = <0x020b4000 0x4000>;
interrupts = <GIC_SPI 120 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ENET>,
<&clks IMX6UL_CLK_ENET_AHB>,
<&clks IMX6UL_CLK_ENET_PTP>,
<&clks IMX6UL_CLK_ENET2_REF_125M>,
<&clks IMX6UL_CLK_ENET2_REF_125M>;
clock-names = "ipg", "ahb", "ptp",
"enet_clk_ref", "enet_out";
stop-mode = <&gpr 0x10 4>;
fsl,num-tx-queues=<1>;
fsl,num-rx-queues=<1>;
fsl,magic-packet;
fsl,wakeup_irq = <0>;
status = "disabled";
};
2.自己的设备树中补充控制器节点
其中phy相关节点部分可以参考:Documentation/devicetree/bindings/net/phy.txt
&fec2 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet2//MDIO与MDC两个引脚的IO复用
&pinctrl_enet2_reset>;//PHY中Reset引脚的IO复用为普通GPIO
phy-mode = "rmii";//我们使用的是rmii与PHY通信
phy-handle = <ðphy1>;//子节点
phy-reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;//配置GPIO
phy-reset-duration = <200>;//复位时间
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
/*
ethphy0: ethernet-phy@2 {
compatible = "ethernet-phy-ieee802.3-c22";
smsc,disable-energy-detect;
reg = <2>;
};
*/
ethphy1: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c22";
smsc,disable-energy-detect;
reg = <1>;//芯片地址
};
};
};
pinctrl_enet2: enet2grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO07__ENET2_MDC 0x1b0b0
MX6UL_PAD_GPIO1_IO06__ENET2_MDIO 0x1b0b0
MX6UL_PAD_ENET2_RX_EN__ENET2_RX_EN 0x1b0b0
MX6UL_PAD_ENET2_RX_ER__ENET2_RX_ER 0x1b0b0
MX6UL_PAD_ENET2_RX_DATA0__ENET2_RDATA00 0x1b0b0
MX6UL_PAD_ENET2_RX_DATA1__ENET2_RDATA01 0x1b0b0
MX6UL_PAD_ENET2_TX_EN__ENET2_TX_EN 0x1b0b0
MX6UL_PAD_ENET2_TX_DATA0__ENET2_TDATA00 0x1b0b0
MX6UL_PAD_ENET2_TX_DATA1__ENET2_TDATA01 0x1b0b0
MX6UL_PAD_ENET2_TX_CLK__ENET2_REF_CLK2 0x4001b031
>;
};
pinctrl_enet2_reset: enet2resetgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0
>;
};
注意:其中pinctrl_enet2_reset要放在&iomuxc_snvs节点下,因为MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO0中有SNVS,而pinctrl_enet2放在普通的&iomuxc节点下。
2.分析驱动
一、MAC控制器驱动
如何查找到控制器驱动所在文件呢?可以根据设备树控制器节点的compatible在内核源码中搜索。
grep -r “fsl,imx6ul-fec” .
最终找到:linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\drivers\net\ethernet\freescale\fec_main.c
static struct platform_driver fec_driver = {
.driver = {
.name = DRIVER_NAME,
.pm = &fec_pm_ops,
.of_match_table = fec_dt_ids,
},
.id_table = fec_devtype,
.probe = fec_probe,
.remove = fec_drv_remove,
};
module_platform_driver(fec_driver);
MODULE_ALIAS("platform:"DRIVER_NAME);
MODULE_LICENSE("GPL");
可以发现像i2c控制器驱动一样,它也是注册进platform总线里。
主要看看probe:fec_probe
static int
fec_probe(struct platform_device *pdev)
{
struct fec_enet_private *fep;//定义私有数据对象
struct fec_platform_data *pdata;
struct net_device *ndev;// 定义net_device对象,网络驱动框架的核心对象
.....
int num_tx_qs;
int num_rx_qs;
/* zuozhongkai 2019/2/20 设置MX6UL_PAD_ENET1_TX_CLK和
* MX6UL_PAD_ENET2_TX_CLK这两个IO的复用寄存器的SION位
* 为1。
*/
void __iomem *IMX6U_ENET1_TX_CLK;
void __iomem *IMX6U_ENET2_TX_CLK;
IMX6U_ENET1_TX_CLK = ioremap(0X020E00DC, 4);
writel(0X14, IMX6U_ENET1_TX_CLK);
IMX6U_ENET2_TX_CLK = ioremap(0X020E00FC, 4);
writel(0X14, IMX6U_ENET2_TX_CLK);
fec_enet_get_queue_num(pdev, &num_tx_qs, &num_rx_qs);
/* Init network device */
ndev = alloc_etherdev_mqs(sizeof(struct fec_enet_private),
num_tx_qs, num_rx_qs);//分配并初始化net_device
......
......
phy_node = of_parse_phandle(np, "phy-handle", 0);//获取phy节点
if (!phy_node && of_phy_is_fixed_link(np)) {
ret = of_phy_register_fixed_link(np);
if (ret < 0) {
dev_err(&pdev->dev,
"broken fixed-link specification\n");
goto failed_phy;
}
phy_node = of_node_get(np);
}
fep->phy_node = phy_node;
ret = of_get_phy_mode(pdev->dev.of_node);//获取phy的传输描述(使用mii、rmii,..)
......
......
fec_reset_phy(pdev);//复位phy
if (fep->bufdesc_ex)
fec_ptp_init(pdev);
ret = fec_enet_init(ndev);//初始化net_device(包括两个ops变量的赋值),以及设置NAPI的POLL
if (ret)
goto failed_init;
for (i = 0; i < FEC_IRQ_NUM; i++) {
irq = platform_get_irq(pdev, i);
if (irq < 0) {
if (i)
break;
ret = irq;
goto failed_irq;
}
ret = devm_request_irq(&pdev->dev, irq, fec_enet_interrupt,
0, pdev->name, ndev);//设置fec_enet_interrupt很重要,发送与接收数据包都要次中断
.......
ret = of_property_read_u32(np, "fsl,wakeup_irq", &irq);
if (!ret && irq < FEC_IRQ_NUM)
fep->wake_irq = fep->irq[irq];
else
fep->wake_irq = fep->irq[0];
init_completion(&fep->mdio_done);
ret = fec_enet_mii_init(pdev);//完成 MII/RMII 接口的初始化,主要是配置SOC 读写 PHY 内部寄存器的函数,这里面除了注册mii_bus还注册了phy设备
ret = register_netdev(ndev);//注册net_device
......
......
return ret;
}
我们先分析一下fec_enet_init:
1.fec_enet_init:(主要作用:初始化net_device,注册NAPI)
static int fec_enet_init(struct net_device *ndev)
{
struct fec_enet_private *fep = netdev_priv(ndev);//获取私有数据
struct fec_enet_priv_tx_q *txq;
struct fec_enet_priv_rx_q *rxq;
struct bufdesc *cbd_base;
dma_addr_t bd_dma;
.....
.....
fec_enet_alloc_queue(ndev);//分配请求队列
/* Allocate memory for buffer descriptors. */
cbd_base = dma_alloc_coherent(NULL, bd_size, &bd_dma,
GFP_KERNEL);//分配dma缓冲区
if (!cbd_base) {
return -ENOMEM;
}
memset(cbd_base, 0, bd_size);//清空缓冲区
/* Get the Ethernet address */
fec_get_mac(ndev);//得到mac控制器地址
/* make sure MAC we just acquired is programmed into the hw */
fec_set_mac_address(ndev, NULL);
/* Set receive and transmit descriptor base. */
//设置发送与接收描述符地址
for (i = 0; i < fep->num_rx_queues; i++) {
rxq = fep->rx_queue[i];
rxq->index = i;
rxq->rx_bd_base = (struct bufdesc *)cbd_base;
rxq->bd_dma = bd_dma;
if (fep->bufdesc_ex) {
bd_dma += sizeof(struct bufdesc_ex) * rxq->rx_ring_size;
cbd_base = (struct bufdesc *)
(((struct bufdesc_ex *)cbd_base) + rxq->rx_ring_size);
} else {
bd_dma += sizeof(struct bufdesc) * rxq->rx_ring_size;
cbd_base += rxq->rx_ring_size;
}
}
for (i = 0; i < fep->num_tx_queues; i++) {
txq = fep->tx_queue[i];
txq->index = i;
txq->tx_bd_base = (struct bufdesc *)cbd_base;
txq->bd_dma = bd_dma;
if (fep->bufdesc_ex) {
bd_dma += sizeof(struct bufdesc_ex) * txq->tx_ring_size;
cbd_base = (struct bufdesc *)
(((struct bufdesc_ex *)cbd_base) + txq->tx_ring_size);
} else {
bd_dma += sizeof(struct bufdesc) * txq->tx_ring_size;
cbd_base += txq->tx_ring_size;
}
}
/* The FEC Ethernet specific entries in the device structure */
ndev->watchdog_timeo = TX_TIMEOUT;
//实习网络设备操作函数,并赋值
ndev->netdev_ops = &fec_netdev_ops;//这些都是在这个文件中实习的
ndev->ethtool_ops = &fec_enet_ethtool_ops;
netif_napi_add(ndev, &fep->napi, fec_enet_rx_napi, NAPI_POLL_WEIGHT);//启用NAPI
.....
.....
fec_restart(ndev);//重启网络传输
return 0;
}
如何使用NAPI实现收发数据?
首先我们知道网络设备中收发数据都会在底层产生中断传输给上层。而这时就会执行到我们前面注册的中断fec_enet_interrupt
fec_enet_interrupt:
static irqreturn_t
fec_enet_interrupt(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct fec_enet_private *fep = netdev_priv(ndev);
......
if (napi_schedule_prep(&fep->napi)) {
/* Disable the NAPI interrupts */
writel(FEC_ENET_MII, fep->hwp + FEC_IMASK);
__napi_schedule(&fep->napi);//调用napi
}
......
return ret;
}
__napi_schedule:
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}
____napi_schedule:
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);//触发中断下半部,也就是NAPI的执行
}
这时就会调用到netif_napi_add(ndev, &fep->napi, fec_enet_rx_napi, NAPI_POLL_WEIGHT);//启用NAPI中的fec_enet_rx_napi
fec_enet_rx_napi:
static int fec_enet_rx_napi(struct napi_struct *napi, int budget)
{
struct net_device *ndev = napi->dev;
struct fec_enet_private *fep = netdev_priv(ndev);
int pkts;
pkts = fec_enet_rx(ndev, budget);//读
fec_enet_tx(ndev);//写
if (pkts < budget) {
napi_complete(napi);
writel(FEC_DEFAULT_IMASK, fep->hwp + FEC_IMASK);//打开中断
}
return pkts;
}
napi的作用其实就是为了避免频繁的进入中断而浪费性能,它采用的是轮询的方式,一旦有中断到来,先关中断,然后在轮询中不断主动检测,直到没有数据收发,于是退出轮询,开启中断。
2.fec_enet_mii_init:(主要作用:注册PHY设备与MII_BUS设备,实现PHY总线中驱动与设备的匹配)
static int fec_enet_mii_init(struct platform_device *pdev)
{
static struct mii_bus *fec0_mii_bus;//定义mii_bus对象,(mii_bus并不是总线模型里的总线,而是包含这device的对象,而且它里面还有很多与PHY设备相关的信息)
......
......
fep->mii_bus = mdiobus_alloc();//申请mdiobus
if (fep->mii_bus == NULL) {
err = -ENOMEM;
goto err_out;
}
fep->mii_bus->name = "fec_enet_mii_bus";
//实现读写函数,并赋值,这里的读写主要是读写phy的寄存器
fep->mii_bus->read = fec_enet_mdio_read;
fep->mii_bus->write = fec_enet_mdio_write;
......
......
node = of_get_child_by_name(pdev->dev.of_node, "mdio");
if (node) {
err = of_mdiobus_register(fep->mii_bus, node);//在里面既注册了mii_bus的device也注册了PHY的device
of_node_put(node);
} else {
err = mdiobus_register(fep->mii_bus);//在of_mdiobus_register中也调用了它
}
.....
.....
return err;
}
of_mdiobus_register:
int of_mdiobus_register(struct mii_bus *mdio, struct device_node *np)
{
......
......
/* Register the MDIO bus */
rc = mdiobus_register(mdio);//注册mii_bus的device
if (rc)
return rc;
/* Loop over the child nodes and register a phy_device for each one */
for_each_available_child_of_node(np, child) {
addr = of_mdio_parse_addr(&mdio->dev, child);
if (addr < 0) {
scanphys = true;
continue;
}
rc = of_mdiobus_register_phy(mdio, child, addr);//注册PHY设备
if (rc)
continue;
}
......
return 0;
}
of_mdiobus_register_phy:
static int of_mdiobus_register_phy(struct mii_bus *mdio, struct device_node *child,
u32 addr)
{
struct phy_device *phy;
.......
is_c45 = of_device_is_compatible(child,
"ethernet-phy-ieee802.3-c45");
if (!is_c45 && !of_get_phy_id(child, &phy_id))
phy = phy_device_create(mdio, addr, phy_id, 0, NULL);//创建phy_device
else
phy = get_phy_device(mdio, addr, is_c45);//获取phy_device
if (!phy || IS_ERR(phy))
return 1;
......
......
/* All data is now stored in the phy struct;
* register it */
rc = phy_device_register(phy);//注册phy_device
......
......
return 0;
}
二、PHY总线驱动
首先看phy总线是在哪里注册的。
在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\drivers\net\phy\mdio_bus.c中
struct bus_type mdio_bus_type = {
.name = "mdio_bus",
.match = mdio_bus_match,
.pm = MDIO_BUS_PM_OPS,
.dev_groups = mdio_dev_groups,
};
EXPORT_SYMBOL(mdio_bus_type);
int __init mdio_bus_init(void)
{
int ret;
ret = class_register(&mdio_bus_class);
if (!ret) {
ret = bus_register(&mdio_bus_type);
if (ret)
class_unregister(&mdio_bus_class);
}
return ret;
}
void mdio_bus_exit(void)
{
class_unregister(&mdio_bus_class);
bus_unregister(&mdio_bus_type);
}
看来这个总线是内核启动会自动注册的,那么phy_driver是在哪里注册的?
在linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\drivers\net\phy\device.c中
static int __init phy_init(void)
{
int rc;
rc = mdio_bus_init();
if (rc)
return rc;
rc = phy_drivers_register(genphy_driver,
ARRAY_SIZE(genphy_driver));
if (rc)
mdio_bus_exit();
return rc;
}
static void __exit phy_exit(void)
{
phy_drivers_unregister(genphy_driver,
ARRAY_SIZE(genphy_driver));
mdio_bus_exit();
}
subsys_initcall(phy_init);
module_exit(phy_exit);
这个driver是通用的phy驱动。
static struct phy_driver genphy_driver[] = {
{
.phy_id = 0xffffffff,
.phy_id_mask = 0xffffffff,
.name = "Generic PHY",//从名字就可以看出这是通用的phy驱动
.soft_reset = genphy_soft_reset,
.config_init = genphy_config_init,
.features = PHY_GBIT_FEATURES | SUPPORTED_MII |
SUPPORTED_AUI | SUPPORTED_FIBRE |
SUPPORTED_BNC,
.config_aneg = genphy_config_aneg,
.aneg_done = genphy_aneg_done,
.read_status = genphy_read_status,
.suspend = genphy_suspend,
.resume = genphy_resume,
.driver = { .owner = THIS_MODULE, },
}, {
.phy_id = 0xffffffff,
.phy_id_mask = 0xffffffff,
.name = "Generic 10G PHY",
.soft_reset = gen10g_soft_reset,
.config_init = gen10g_config_init,
.features = 0,
.config_aneg = gen10g_config_aneg,
.read_status = gen10g_read_status,
.suspend = gen10g_suspend,
.resume = gen10g_resume,
.driver = {.owner = THIS_MODULE, },
} };
这里面没有.compatible属性,那是如何匹配的呢?
看mdio_bus_match:
static int mdio_bus_match(struct device *dev, struct device_driver *drv)
{
struct phy_device *phydev = to_phy_device(dev);
struct phy_driver *phydrv = to_phy_driver(drv);
if (of_driver_match_device(dev, drv))
return 1;
if (phydrv->match_phy_device)
return phydrv->match_phy_device(phydev);
return (phydrv->phy_id & phydrv->phy_id_mask) ==
(phydev->phy_id & phydrv->phy_id_mask);//最后通过phy的id与phy的mask_id来比较
}
至此驱动的分析就差不多了。
如果想用厂家自己的phy_driver那么可以在内核中配置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-du4JViZp-1680859809927)(C:\Users\cww\AppData\Roaming\Typora\typora-user-images\image-20230406205447986.png)]
总结:
其实网络设备驱动很想I2C或者SPI驱动,它分为MAC与PHY,MAC驱动叫做控制器驱动,是一个platform,内核实现,而PHY驱动是有自己的phy_bus,内核实现了一个通用的phy驱动,但是一般厂家自己也会提供驱动。
.compatible属性,那是如何匹配的呢?
看mdio_bus_match:
static int mdio_bus_match(struct device *dev, struct device_driver *drv)
{
struct phy_device *phydev = to_phy_device(dev);
struct phy_driver *phydrv = to_phy_driver(drv);
if (of_driver_match_device(dev, drv))
return 1;
if (phydrv->match_phy_device)
return phydrv->match_phy_device(phydev);
return (phydrv->phy_id & phydrv->phy_id_mask) ==
(phydev->phy_id & phydrv->phy_id_mask);//最后通过phy的id与phy的mask_id来比较
}
至此驱动的分析就差不多了。
如果想用厂家自己的phy_driver那么可以在内核中配置。
总结:
其实网络设备驱动很想I2C或者SPI驱动,它分为MAC与PHY,MAC驱动叫做控制器驱动,是一个platform,内核实现,而PHY驱动是有自己的phy_bus,内核实现了一个通用的phy驱动,但是一般厂家自己也会提供驱动。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)