devicetree.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Lukasz Stelmach <l.stelmach@samsung.com>
To: Andrew Lunn <andrew@lunn.ch>
Cc: "David S. Miller" <davem@davemloft.net>,
	"Jakub Kicinski" <kuba@kernel.org>,
	"Krzysztof Kozlowski" <krzk@kernel.org>,
	"Kukjin Kim" <kgene@kernel.org>,
	"Rob Herring" <robh+dt@kernel.org>,
	"Russell King" <linux@armlinux.org.uk>,
	jim.cromie@gmail.com, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, linux-samsung-soc@vger.kernel.org,
	netdev@vger.kernel.org, devicetree@vger.kernel.org,
	"Bartłomiej Żolnierkiewicz" <b.zolnierkie@samsung.com>,
	"Marek Szyprowski" <m.szyprowski@samsung.com>
Subject: Re: [PATCH v2 2/4] net: ax88796c: ASIX AX88796C SPI Ethernet Adapter Driver
Date: Tue, 13 Oct 2020 22:04:52 +0200	[thread overview]
Message-ID: <dleftjd01l99jv.fsf%l.stelmach@samsung.com> (raw)
In-Reply-To: <20201002203641.GI3996795@lunn.ch> (Andrew Lunn's message of "Fri, 2 Oct 2020 22:36:41 +0200")

[-- Attachment #1: Type: text/plain, Size: 14237 bytes --]

It was <2020-10-02 pią 22:36>, when Andrew Lunn wrote:
>> +static u32 ax88796c_get_link(struct net_device *ndev)
>> +{
>> +	struct ax88796c_device *ax_local = to_ax88796c_device(ndev);
>> +
>> +	mutex_lock(&ax_local->spi_lock);
>> +
>> +	phy_read_status(ndev->phydev);
>> +
>> +	mutex_unlock(&ax_local->spi_lock);
>
> Why do you take this mutux before calling phy_read_status()? The
> phylib core will not be taking this mutex when it calls into the PHY
> driver. This applies to all the calls you have with phy_
>

I need to review the use of this mutex. Thanks for spotting.

> There should not be any need to call phy_read_status(). phylib will do
> this once per second, or after any interrupt from the PHY. so just use
>
>      phydev->link
>

Using ethtool_op_get_link()

>> +static void
>> +ax88796c_get_regs(struct net_device *ndev, struct ethtool_regs *regs, void *_p)
>> +{
>> +	struct ax88796c_device *ax_local = to_ax88796c_device(ndev);
>> +	u16 *p = _p;
>> +	int offset, i;
>> +
>> +	memset(p, 0, AX88796C_REGDUMP_LEN);
>> +
>> +	for (offset = 0; offset < AX88796C_REGDUMP_LEN; offset += 2) {
>> +		if (!test_bit(offset / 2, ax88796c_no_regs_mask))
>> +			*p = AX_READ(&ax_local->ax_spi, offset);
>> +		p++;
>> +	}
>> +
>> +	for (i = 0; i < AX88796C_PHY_REGDUMP_LEN / 2; i++) {
>> +		*p = phy_read(ax_local->phydev, i);
>> +		p++;
>
> Depending on the PHY, that can be dangerous.

This is a built-in generic PHY. The chip has no lines to attach any
other external one.

> phylib could be busy doing things with the PHY. It could be looking at

How does phylib prevent concurrent access to a PHY? 

> a different page for example.

Different page? 

> miitool(1) can give you the same functionally without the MAC driver
> doing anything, other than forwarding the IOCTL call on.

No, I am afraid mii-tool is not able to dump registers. I am not insisting
on dumping PHY registeres but I think it is nice to have them. Intel
drivers do it.

>> +int ax88796c_mdio_read(struct mii_bus *mdiobus, int phy_id, int loc)
>> +{
>> +	struct ax88796c_device *ax_local = mdiobus->priv;
>> +	int ret;
>> +
>> +	AX_WRITE(&ax_local->ax_spi, MDIOCR_RADDR(loc)
>> +			| MDIOCR_FADDR(phy_id) | MDIOCR_READ, P2_MDIOCR);
>> +
>> +	ret = read_poll_timeout(AX_READ, ret,
>> +				(ret != 0),
>> +				0, jiffies_to_usecs(HZ / 100), false,
>> +				&ax_local->ax_spi, P2_MDIOCR);
>> +	if (ret)
>> +		return -EBUSY;
>
> Return whatever read_poll_timeout() returned. It is probably
> -ETIMEDOUT, but it could also be -EIO for example.

Indeed it is -ETIMEDOUT. Returning ret.

>> +ax88796c_mdio_write(struct mii_bus *mdiobus, int phy_id, int loc, u16 val)
>> +{
>> +	struct ax88796c_device *ax_local = mdiobus->priv;
>> +	int ret;
>> +
>> +	AX_WRITE(&ax_local->ax_spi, val, P2_MDIODR);
>> +
>> +	AX_WRITE(&ax_local->ax_spi,
>> +		 MDIOCR_RADDR(loc) | MDIOCR_FADDR(phy_id)
>> +		 | MDIOCR_WRITE, P2_MDIOCR);
>> +
>> +	ret = read_poll_timeout(AX_READ, ret,
>> +				((ret & MDIOCR_VALID) != 0), 0,
>> +				jiffies_to_usecs(HZ / 100), false,
>> +				&ax_local->ax_spi, P2_MDIOCR);
>> +	if (ret)
>> +		return -EIO;
>> +
>> +	if (loc == MII_ADVERTISE) {
>> +		AX_WRITE(&ax_local->ax_spi, (BMCR_FULLDPLX | BMCR_ANRESTART |
>> +			  BMCR_ANENABLE | BMCR_SPEED100), P2_MDIODR);
>> +		AX_WRITE(&ax_local->ax_spi, (MDIOCR_RADDR(MII_BMCR) |
>> +			  MDIOCR_FADDR(phy_id) | MDIOCR_WRITE),
>> +			  P2_MDIOCR);
>>
>
> What is this doing?
>

Well… it turns autonegotiation when changing advertised link modes. But
this is obvious. As to why this code is here, I will honestly say — I am
not sure (Reminder: this is a vendor driver I am porting, I am more than
happy to receive any comments, thank you). Apparently it is not required
and I am willing to remove it.  It could be of some use when the driver
didn't use phylib.

>> +		ret = read_poll_timeout(AX_READ, ret,
>> +					((ret & MDIOCR_VALID) != 0), 0,
>> +					jiffies_to_usecs(HZ / 100), false,
>> +					&ax_local->ax_spi, P2_MDIOCR);
>> +		if (ret)
>> +			return -EIO;
>> +	}
>> +
>> +	return 0;
>> +}
>
>> +static char *no_regs_list = "80018001,e1918001,8001a001,fc0d0000";
>> +unsigned long ax88796c_no_regs_mask[AX88796C_REGDUMP_LEN / (sizeof(unsigned long) * 8)];
>> +
>> +module_param(comp, int, 0444);
>> +MODULE_PARM_DESC(comp, "0=Non-Compression Mode, 1=Compression Mode");
>> +
>> +module_param(msg_enable, int, 0444);
>> +MODULE_PARM_DESC(msg_enable, "Message mask (see linux/netdevice.h for bitmap)");
>
> No module parameters allowed, not in netdev.
>
>> +static int ax88796c_reload_eeprom(struct ax88796c_device *ax_local)
>> +{
>> +	int ret;
>> +
>> +	AX_WRITE(&ax_local->ax_spi, EECR_RELOAD, P3_EECR);
>> +
>> +	ret = read_poll_timeout(AX_READ, ret,
>> +				(ret & PSR_DEV_READY),
>> +				0, jiffies_to_usecs(2 * HZ / 1000), false,
>> +				&ax_local->ax_spi, P0_PSR);
>> +	if (ret) {
>> +		dev_err(&ax_local->spi->dev,
>> +			"timeout waiting for reload eeprom\n");
>> +		return -1;
>
> return ret not EINVAL which is -1
>

Done.

>> +static int ax88796c_set_mac_address(struct net_device *ndev, void *p)
>> +{
>> +	struct ax88796c_device *ax_local = to_ax88796c_device(ndev);
>> +	struct sockaddr *addr = p;
>> +
>> +	if (!is_valid_ether_addr(addr->sa_data))
>> +		return -EADDRNOTAVAIL;
>
> It would be better to just use eth_mac_addr().
>

Done.

>> +static int
>> +ax88796c_check_free_pages(struct ax88796c_device *ax_local, u8 need_pages)
>> +{
>> +	u8 free_pages;
>> +	u16 tmp;
>> +
>> +	free_pages = AX_READ(&ax_local->ax_spi, P0_TFBFCR) & TX_FREEBUF_MASK;
>> +	if (free_pages < need_pages) {
>> +		/* schedule free page interrupt */
>> +		tmp = AX_READ(&ax_local->ax_spi, P0_TFBFCR)
>> +				& TFBFCR_SCHE_FREE_PAGE;
>> +		AX_WRITE(&ax_local->ax_spi, tmp | TFBFCR_TX_PAGE_SET |
>> +				TFBFCR_SET_FREE_PAGE(need_pages),
>> +				P0_TFBFCR);
>> +		return -ENOMEM;
>> +	}
>> +
>> +	return 0;
>> +}
>> +
>> +static struct sk_buff *
>> +ax88796c_tx_fixup(struct net_device *ndev, struct sk_buff_head *q)
>> +{
>> +	if (netif_msg_pktdata(ax_local)) {
>> +		char pfx[IFNAMSIZ + 7];
>> +
>> +		snprintf(pfx, sizeof(pfx), "%s:     ", ndev->name);
>> +
>> +		netdev_info(ndev, "TX packet len %d, total len %d, seq %d\n",
>> +			    pkt_len, tx_skb->len, seq_num);
>> +
>> +		netdev_info(ndev, "  SPI Header:\n");
>> +		print_hex_dump(KERN_INFO, pfx, DUMP_PREFIX_OFFSET, 16, 1,
>> +			       tx_skb->data, 4, 0);
>> +
>> +		netdev_info(ndev, "  TX SOP:\n");
>> +		print_hex_dump(KERN_INFO, pfx, DUMP_PREFIX_OFFSET, 16, 1,
>> +			       tx_skb->data + 4, TX_OVERHEAD, 0);
>> +
>> +		netdev_info(ndev, "  TX packet:\n");
>> +		print_hex_dump(KERN_INFO, pfx, DUMP_PREFIX_OFFSET, 16, 1,
>> +			       tx_skb->data + 4 + TX_OVERHEAD,
>> +			       tx_skb->len - TX_EOP_SIZE - 4 - TX_OVERHEAD, 0);
>> +
>> +		netdev_info(ndev, "  TX EOP:\n");
>> +		print_hex_dump(KERN_INFO, pfx, DUMP_PREFIX_OFFSET, 16, 1,
>> +			       tx_skb->data + tx_skb->len - 4, 4, 0);
>> +	}
>
> I expect others are going to ask you to remove this.
>

You mean dumping packets? I will if they do. What is pktdata flag for then?

>> +static void ax88796c_handle_link_change(struct net_device *ndev)
>> +{
>> +	if (net_ratelimit())
>> +		phy_print_status(ndev->phydev);
>> +}
>> +
>> +void ax88796c_phy_init(struct ax88796c_device *ax_local)
>> +{
>> +	/* Enable PHY auto-polling */
>> +	AX_WRITE(&ax_local->ax_spi,
>> +		 PCR_PHYID(0x10) | PCR_POLL_EN |
>> +		 PCR_POLL_FLOWCTRL | PCR_POLL_BMCR, P2_PCR);
>
> Auto-polling of the PHY is generally a bad idea. The hardware is not
> going to respect the phydev->lock mutex, for example. Disable this,
> and add a proper ax88796c_handle_link_change().
>

Done.

>> +static int
>> +ax88796c_open(struct net_device *ndev)
>> +{
>> +	struct ax88796c_device *ax_local = to_ax88796c_device(ndev);
>> +	int ret;
>> +	unsigned long irq_flag = IRQF_SHARED;
>> +
>> +	mutex_lock(&ax_local->spi_lock);
>> +
>> +	ret = ax88796c_soft_reset(ax_local);
>> +	if (ret < 0)
>> +		return -ENODEV;
>> +
>> +	ret = request_irq(ndev->irq, ax88796c_interrupt,
>> +			  irq_flag, ndev->name, ndev);
>
> Maybe look at using request_threaded_irq(). You can then remove your
> work queue, and do the work in the thread_fn.
>

There is other work beeing done in the work queue too.

>> +	if (ret) {
>> +		netdev_err(ndev, "unable to get IRQ %d (errno=%d).\n",
>> +			   ndev->irq, ret);
>> +		return -ENXIO;
>
> return ret;
>
> In general, never change a return code unless you have a really good
> reason why. And if you do have a reason, document it.
>

OK, Done.

>> +static int
>> +ax88796c_close(struct net_device *ndev)
>> +{
>> +	struct ax88796c_device *ax_local = to_ax88796c_device(ndev);
>> +
>> +	netif_stop_queue(ndev);
>> +
>> +	free_irq(ndev->irq, ndev);
>> +
>> +	phy_stop(ndev->phydev);
>> +
>> +	mutex_lock(&ax_local->spi_lock);
>> +
>> +	AX_WRITE(&ax_local->ax_spi, IMR_MASKALL, P0_IMR);
>> +	ax88796c_free_skb_queue(&ax_local->tx_wait_q);
>> +
>> +	ax88796c_soft_reset(ax_local);
>> +
>> +	mutex_unlock(&ax_local->spi_lock);
>> +	netif_carrier_off(ndev);
>
> phy_stop() will do that for you.
>

Removed.

>> +static int ax88796c_probe(struct spi_device *spi)
>> +{
>
>> +	ax_local->mdiobus->priv = ax_local;
>> +	ax_local->mdiobus->read = ax88796c_mdio_read;
>> +	ax_local->mdiobus->write = ax88796c_mdio_write;
>> +	ax_local->mdiobus->name = "ax88976c-mdiobus";
>> +	ax_local->mdiobus->phy_mask = ~(1 << 0x10);
>
> BIT(0x10);
>

Done.

>> +
>> +	ret = devm_register_netdev(&spi->dev, ndev);
>> +	if (ret) {
>> +		dev_err(&spi->dev, "failed to register a network device\n");
>> +		destroy_workqueue(ax_local->ax_work_queue);
>> +		goto err;
>> +	}
>
> The device is not live. If this is being used for NFS root, the kernel
> will start using it. So what sort of mess will it get into, if there
> is no PHY yet? Nothing important should happen after register_netdev().
>

But, with an unregistered network device ndev_owner in
phy_attach_direct() is NULL. Thus, phy_connect_direct() below fails.

--8<---------------cut here---------------start------------->8---
   1332         if (dev)
   1333                 ndev_owner = dev->dev.parent->driver->owner;
   1334         if (ndev_owner != bus->owner &&  !try_module_get(bus->owner)) {
   1335                 phydev_err(phydev, "failed to get the bus  module\n");
   1336                 return -EIO;
   1337         }
--8<---------------cut here---------------end--------------->8---


>> +
>> +	ax_local->phydev = phy_find_first(ax_local->mdiobus);
>> +	if (!ax_local->phydev) {
>> +		dev_err(&spi->dev, "no PHY found\n");
>> +		ret = -ENODEV;
>> +		goto err;
>> +	}
>> +
>> +	ax_local->phydev->irq = PHY_IGNORE_INTERRUPT;
>> +	phy_connect_direct(ax_local->ndev, ax_local->phydev,
>> +			   ax88796c_handle_link_change,
>> +			   PHY_INTERFACE_MODE_MII);
>> +
>> +	netif_info(ax_local, probe, ndev, "%s %s registered\n",
>> +		   dev_driver_string(&spi->dev),
>> +		   dev_name(&spi->dev));
>> +	phy_attached_info(ax_local->phydev);
>> +
>> +	ret = 0;
>> +err:
>> +	return ret;
>> +}
>> +
>> +static int ax88796c_remove(struct spi_device *spi)
>> +{
>> +	struct ax88796c_device *ax_local = dev_get_drvdata(&spi->dev);
>> +	struct net_device *ndev = ax_local->ndev;
>
> You might want to disconnect the PHY.
>

I do (-; Done.

>> +
>> +	netif_info(ax_local, probe, ndev, "removing network device %s %s\n",
>> +		   dev_driver_string(&spi->dev),
>> +		   dev_name(&spi->dev));
>> +
>> +	destroy_workqueue(ax_local->ax_work_queue);
>> +
>> +	return 0;
>> +}
>> +
>> +static const struct of_device_id ax88796c_dt_ids[] = {
>> +	{ .compatible = "asix,ax88796c" },
>> +	{},
>> +};
>> +MODULE_DEVICE_TABLE(of, ax88796c_dt_ids);
>> +
>> +static const struct spi_device_id asix_id[] = {
>> +	{ "ax88796c", 0 },
>> +	{ }
>> +};
>> +MODULE_DEVICE_TABLE(spi, asix_id);
>> +
>> +static struct spi_driver ax88796c_spi_driver = {
>> +	.driver = {
>> +		.name = DRV_NAME,
>> +#ifdef CONFIG_USE_OF
>> +		.of_match_table = of_match_ptr(ax88796c_dt_ids),
>> +#endif
>
> I don't think you need the #ifdef.
>

Indeed, it appears to be an uncommon practice to use it in this
context. Done.

>> +#ifndef _AX88796C_MAIN_H
>> +#define _AX88796C_MAIN_H
>> +
>> +#include <linux/netdevice.h>
>> +#include <linux/mii.h>
>> +
>> +#include "ax88796c_spi.h"
>> +
>> +/* These identify the driver base version and may not be removed. */
>> +#define DRV_NAME	"ax88796c"
>> +#define ADP_NAME	"ASIX AX88796C SPI Ethernet Adapter"
>> +#define DRV_VERSION	"1.2.0"
>
> DRV_VERSION are pretty pointless. Not sure you use it anyway. Please
> remove.
>

Done.

>> +	unsigned long		capabilities;
>> +		#define AX_CAP_DMA		1
>> +		#define AX_CAP_COMP		2
>> +		#define AX_CAP_BIDIR		4
>
> BIT(0), BIT(1), BIT(2)...
>

No problem. Do you have any recommendation how to express this

 #define PSR_RESET  (0 << 15)

I know it equals 0, but shows explicitly the bit number.

>> +struct skb_data;
>> +
>> +struct skb_data {
>> +	enum skb_state state;
>> +	struct net_device *ndev;
>> +	struct sk_buff *skb;
>> +	size_t len;
>> +	dma_addr_t phy_addr;
>> +};
>
> A forward definition, followed by the real definition?
>

There must have been something in between. Done.

>> +	#define FER_IPALM		(1 << 0)
>> +	#define FER_DCRC		(1 << 1)
>> +	#define FER_RH3M		(1 << 2)
>> +	#define FER_HEADERSWAP		(1 << 7)
>> +	#define FER_WSWAP		(1 << 8)
>> +	#define FER_BSWAP		(1 << 9)
>> +	#define FER_INTHI		(1 << 10)
>> +	#define FER_INTLO		(0 << 10)
>> +	#define FER_IRQ_PULL		(1 << 11)
>> +	#define FER_RXEN		(1 << 14)
>> +	#define FER_TXEN		(1 << 15)
>
> Isn't checkpatch giving warnings and suggesting BIT?

Not exactly. It gives green CHECK messages, which I decided to
ignore. Apparently a wrong move.

Thanks for the feedback.
-- 
Łukasz Stelmach
Samsung R&D Institute Poland
Samsung Electronics

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 487 bytes --]

  parent reply	other threads:[~2020-10-13 20:05 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <CGME20201002192215eucas1p2c8f4a4bf8e411ed8ba75383fd58e85ac@eucas1p2.samsung.com>
2020-10-02 19:22 ` [PATCH v2 0/4] AX88796C SPI Ethernet Adapter Łukasz Stelmach
     [not found]   ` <CGME20201002192215eucas1p2c1d2baebfe2a9caa11d88175a2899fea@eucas1p2.samsung.com>
2020-10-02 19:22     ` [PATCH v2 1/4] dt-bindings: net: Add bindings for " Łukasz Stelmach
2020-10-03 10:09       ` Krzysztof Kozlowski
2020-10-05 14:03         ` Rob Herring
2020-10-05 13:59       ` Rob Herring
2020-10-05 14:01       ` Krzysztof Kozlowski
     [not found]   ` <CGME20201002192216eucas1p16f54584cf50fff56edc278102a66509e@eucas1p1.samsung.com>
2020-10-02 19:22     ` [PATCH v2 2/4] net: ax88796c: ASIX AX88796C SPI Ethernet Adapter Driver Łukasz Stelmach
2020-10-02 20:36       ` Andrew Lunn
     [not found]         ` <CGME20201013200453eucas1p1b77c93275b518422429ff1481f88a4be@eucas1p1.samsung.com>
2020-10-13 20:04           ` Lukasz Stelmach [this message]
2020-10-16 18:01             ` Andrew Lunn
     [not found]               ` <CGME20201016191905eucas1p235fb430a8f330a39e64f4fd6b81decb2@eucas1p2.samsung.com>
2020-10-16 19:18                 ` Lukasz Stelmach
     [not found]         ` <CGME20201019125624eucas1p257a76c307adfb27202332658f93c9aba@eucas1p2.samsung.com>
2020-10-19 12:56           ` Lukasz Stelmach
2020-10-19 13:02             ` Andrew Lunn
2020-10-03 12:59       ` Heiner Kallweit
     [not found]         ` <CGME20201013200250eucas1p2445005531b86f246d7a14b7fc8016e80@eucas1p2.samsung.com>
2020-10-13 20:02           ` Lukasz Stelmach
     [not found]   ` <CGME20201002192216eucas1p16933608dcb0fb8ceee21caa3455cbaf1@eucas1p1.samsung.com>
2020-10-02 19:22     ` [PATCH v2 3/4] ARM: dts: exynos: Add Ethernet to Artik 5 board Łukasz Stelmach
2020-10-03 10:13       ` Krzysztof Kozlowski
     [not found]         ` <CGME20201006100556eucas1p2b69f76968a7a5901b5e9c66338c388d4@eucas1p2.samsung.com>
2020-10-06 10:05           ` Lukasz Stelmach
2020-10-06 10:17             ` Krzysztof Kozlowski
     [not found]               ` <CGME20201006140300eucas1p2006a96630d4a825500e5fc72016cf9d7@eucas1p2.samsung.com>
2020-10-06 14:02                 ` Lukasz Stelmach
     [not found]   ` <CGME20201002192217eucas1p2357f80b100fb130d1ef1ac281042ff7c@eucas1p2.samsung.com>
2020-10-02 19:22     ` [PATCH v2 4/4] ARM: defconfig: Enable ax88796c driver Łukasz Stelmach
2020-10-02 19:45   ` [PATCH v2 0/4] AX88796C SPI Ethernet Adapter Andrew Lunn
     [not found]     ` <CGME20201006083749eucas1p160a3bed4cdb67cc8e05ca4a57d8907ca@eucas1p1.samsung.com>
2020-10-06  8:37       ` Lukasz Stelmach

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=dleftjd01l99jv.fsf%l.stelmach@samsung.com \
    --to=l.stelmach@samsung.com \
    --cc=andrew@lunn.ch \
    --cc=b.zolnierkie@samsung.com \
    --cc=davem@davemloft.net \
    --cc=devicetree@vger.kernel.org \
    --cc=jim.cromie@gmail.com \
    --cc=kgene@kernel.org \
    --cc=krzk@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-samsung-soc@vger.kernel.org \
    --cc=linux@armlinux.org.uk \
    --cc=m.szyprowski@samsung.com \
    --cc=netdev@vger.kernel.org \
    --cc=robh+dt@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).