linux-spi.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Grant Likely <grant.likely-s3s/WqlpOiPyB63q8FvJNQ@public.gmane.org>
To: Cyril Chemparathy <cyril-l0cyMroinI0@public.gmane.org>
Cc: spi-devel-general-5NWGOfrQmneRv+LV9MX5uipxlwaOVQ5f@public.gmane.org,
	davinci-linux-open-source-VycZQUHpC/PFrsHnngEfi1aTQe2KTcn/@public.gmane.org,
	broonie-yzvPICuk2AATkU/dhu1WVueM+bqZidxxQQ4Iyu8u01E@public.gmane.org,
	dbrownell-Rn4VEauK+AKRv+LV9MX5uipxlwaOVQ5f@public.gmane.org,
	lrg-kDsPt+C1G03kYMGBc/C6ZA@public.gmane.org
Subject: Re: [PATCH v2 04/12] spi: add ti-ssp spi master driver
Date: Wed, 20 Oct 2010 09:32:51 -0600	[thread overview]
Message-ID: <20101020153251.GC7285@angua.secretlab.ca> (raw)
In-Reply-To: <1287429922-18870-5-git-send-email-cyril-l0cyMroinI0@public.gmane.org>

On Mon, Oct 18, 2010 at 03:25:14PM -0400, Cyril Chemparathy wrote:
> This patch adds an SPI master implementation that operates on top of an
> underlying TI-SSP port.
> 
> Signed-off-by: Cyril Chemparathy <cyril-l0cyMroinI0@public.gmane.org>
> ---
>  arch/arm/mach-davinci/include/mach/ti_ssp.h |    5 +
>  drivers/spi/Kconfig                         |    7 +
>  drivers/spi/Makefile                        |    1 +
>  drivers/spi/spi_ti_ssp.c                    |  397 +++++++++++++++++++++++++++
>  4 files changed, 410 insertions(+), 0 deletions(-)
>  create mode 100644 drivers/spi/spi_ti_ssp.c

Minor comments below, but otherwise:

Acked-by: Grant Likely <grant.likely-s3s/WqlpOiPyB63q8FvJNQ@public.gmane.org>

This patch should be merged with the rest of the series.  I don't
think there is any point in me picking it up into the SPI tree.

> 
> diff --git a/arch/arm/mach-davinci/include/mach/ti_ssp.h b/arch/arm/mach-davinci/include/mach/ti_ssp.h
> index 51afc42..63c88fe 100644
> --- a/arch/arm/mach-davinci/include/mach/ti_ssp.h
> +++ b/arch/arm/mach-davinci/include/mach/ti_ssp.h
> @@ -34,6 +34,11 @@ struct ti_ssp_data {
>  	struct ti_ssp_dev_data	dev_data[2];
>  };
>  
> +struct ti_ssp_spi_data {
> +	int		num_cs;
> +	void		(*select)(int cs);
> +};

Hmmm. I really should look into standardizing the way spi busses
handle slave selects.  In particular, it would be valuable to have a
stock method for the common case of simple gpios used for ss lines.

(Not a critique on this patch, just thinking out loud).

> +
>  /*
>   * Sequencer port IO pin configuration bits.  These do not correlate 1-1 with
>   * the hardware.  The iosel field in the port data combines iosel1 and iosel2,
> diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
> index 91c2f4f..be86354 100644
> --- a/drivers/spi/Kconfig
> +++ b/drivers/spi/Kconfig
> @@ -298,6 +298,13 @@ config SPI_STMP3XXX
>  	help
>  	  SPI driver for Freescale STMP37xx/378x SoC SSP interface
>  
> +config SPI_TI_SSP
> +	tristate "TI SSP Controller SPI Driver"
> +	depends on SPI_MASTER && TI_SSP

This is already in the "if SPI_MASTER" block.  Make it simply "depends
on TI_SSP".

> +	help
> +	  This selects an SPI master implementation using a TI sequencer
> +	  serial port.
> +
>  config SPI_TXX9
>  	tristate "Toshiba TXx9 SPI controller"
>  	depends on GENERIC_GPIO && CPU_TX49XX
> diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
> index e9cbd18..953aba7 100644
> --- a/drivers/spi/Makefile
> +++ b/drivers/spi/Makefile
> @@ -40,6 +40,7 @@ obj-$(CONFIG_SPI_S3C24XX_GPIO)		+= spi_s3c24xx_gpio.o
>  obj-$(CONFIG_SPI_S3C24XX)		+= spi_s3c24xx_hw.o
>  obj-$(CONFIG_SPI_S3C64XX)		+= spi_s3c64xx.o
>  obj-$(CONFIG_SPI_TXX9)			+= spi_txx9.o
> +obj-$(CONFIG_SPI_TI_SSP)		+= spi_ti_ssp.o

Nit: Try to keep the list both a) in the same order as Kconfig, and b)
in alphabetical order (I know the kernel list isn't very close to
alphabetical, but try not to make it worse).  :-)

>  obj-$(CONFIG_SPI_XILINX)		+= xilinx_spi.o
>  obj-$(CONFIG_SPI_XILINX_OF)		+= xilinx_spi_of.o
>  obj-$(CONFIG_SPI_XILINX_PLTFM)		+= xilinx_spi_pltfm.o
> diff --git a/drivers/spi/spi_ti_ssp.c b/drivers/spi/spi_ti_ssp.c
> new file mode 100644
> index 0000000..a566158
> --- /dev/null
> +++ b/drivers/spi/spi_ti_ssp.c
> @@ -0,0 +1,397 @@
> +/*
> + * Sequencer Serial Port (SSP) based SPI master driver
> + *
> + * Copyright (C) 2010 Texas Instruments Inc
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU General Public License for more details.
> + *
> + * You should have received a copy of the GNU General Public License
> + * along with this program; if not, write to the Free Software
> + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
> + */
> +
> +#include <linux/kernel.h>
> +#include <linux/err.h>
> +#include <linux/completion.h>
> +#include <linux/delay.h>
> +#include <linux/platform_device.h>
> +#include <linux/spi/spi.h>
> +
> +#include <mach/ti_ssp.h>
> +
> +#define MODE_BITS	(SPI_CPHA | SPI_CPOL | SPI_CS_HIGH)
> +
> +struct ti_ssp_spi {
> +	const struct ti_ssp_spi_data	*pdata;
> +	struct spi_master		*master;
> +	struct device			*dev;
> +	spinlock_t			lock;
> +	struct list_head		msg_queue;
> +	struct completion		complete;
> +	int				shutdown:1;
> +	struct workqueue_struct		*workqueue;
> +	struct work_struct		work;
> +	u8				mode, bpw;
> +	int				cs_active;
> +	u32				pc_en, pc_dis, pc_wr, pc_rd;
> +};
> +
> +static u32 do_read_data(struct ti_ssp_spi *hw)
> +{
> +	u32 ret;
> +
> +	ti_ssp_run(hw->dev, hw->pc_rd, 0, &ret);
> +	return ret;
> +}
> +
> +static void do_write_data(struct ti_ssp_spi *hw, u32 data)
> +{
> +	ti_ssp_run(hw->dev, hw->pc_wr, data << (32 - hw->bpw), NULL);
> +}
> +
> +static int do_transfer(struct ti_ssp_spi *hw, struct spi_message *msg,
> +		       struct spi_transfer *t)
> +{
> +	int count;
> +
> +	if (hw->bpw <= 8) {
> +		u8		*rx = t->rx_buf;
> +		const u8	*tx = t->tx_buf;
> +
> +		for (count = 0; count < t->len; count += 1) {
> +			if (t->tx_buf)
> +				do_write_data(hw, *tx++);
> +			if (t->rx_buf)
> +				*rx++ = do_read_data(hw);
> +		}
> +	} else if (hw->bpw <= 16) {
> +		u16		*rx = t->rx_buf;
> +		const u16	*tx = t->tx_buf;
> +
> +		for (count = 0; count < t->len; count += 2) {
> +			if (t->tx_buf)
> +				do_write_data(hw, *tx++);
> +			if (t->rx_buf)
> +				*rx++ = do_read_data(hw);
> +		}
> +	} else {
> +		u32		*rx = t->rx_buf;
> +		const u32	*tx = t->tx_buf;
> +
> +		for (count = 0; count < t->len; count += 4) {
> +			if (t->tx_buf)
> +				do_write_data(hw, *tx++);
> +			if (t->rx_buf)
> +				*rx++ = do_read_data(hw);
> +		}
> +	}
> +
> +	msg->actual_length += count; /* bytes transferred */
> +
> +	dev_dbg(&msg->spi->dev, "xfer %s%s, %d bytes, %d bpw, count %d%s\n",
> +		t->tx_buf ? "tx" : "", t->rx_buf ? "rx" : "", t->len,
> +		hw->bpw, count, (count < t->len) ? " (under)" : "");
> +
> +	return (count < t->len) ? -EIO : 0; /* left over data */
> +}
> +
> +static void chip_select(struct ti_ssp_spi *hw, int cs_active)
> +{
> +	cs_active = !!cs_active;
> +	if (cs_active == hw->cs_active)
> +		return;
> +	ti_ssp_run(hw->dev, cs_active ? hw->pc_en : hw->pc_dis, 0, NULL);
> +	hw->cs_active = cs_active;
> +}
> +
> +#define __SHIFT_OUT(bits)	(SSP_OPCODE_SHIFT | SSP_OUT_MODE | \
> +				 cs_en | clk | SSP_COUNT((bits) * 2 - 1))
> +#define __SHIFT_IN(bits)	(SSP_OPCODE_SHIFT | SSP_IN_MODE  | \
> +				 cs_en | clk | SSP_COUNT((bits) * 2 - 1))
> +
> +static int setup_xfer(struct ti_ssp_spi *hw, u8 bpw, u8 mode)
> +{
> +	int error, idx = 0;
> +	u32 seqram[16];
> +	u32 cs_en, cs_dis, clk;
> +	u32 topbits, botbits;
> +
> +	mode &= MODE_BITS;
> +	if (mode == hw->mode && bpw == hw->bpw)
> +		return 0;
> +
> +	cs_en  = (mode & SPI_CS_HIGH) ? SSP_CS_HIGH : SSP_CS_LOW;
> +	cs_dis = (mode & SPI_CS_HIGH) ? SSP_CS_LOW  : SSP_CS_HIGH;
> +	clk    = (mode & SPI_CPOL)    ? SSP_CLK_HIGH : SSP_CLK_LOW;
> +
> +	/* Construct instructions */
> +
> +	/* Disable Chip Select */
> +	hw->pc_dis = idx;
> +	seqram[idx++] = SSP_OPCODE_DIRECT | SSP_OUT_MODE | cs_dis | clk;
> +	seqram[idx++] = SSP_OPCODE_STOP   | SSP_OUT_MODE | cs_dis | clk;
> +
> +	/* Enable Chip Select */
> +	hw->pc_en = idx;
> +	seqram[idx++] = SSP_OPCODE_DIRECT | SSP_OUT_MODE | cs_en | clk;
> +	seqram[idx++] = SSP_OPCODE_STOP   | SSP_OUT_MODE | cs_en | clk;
> +
> +	/* Reads and writes need to be split for bpw > 16 */
> +	topbits = (bpw > 16) ? 16 : bpw;
> +	botbits = bpw - topbits;
> +
> +	/* Write */
> +	hw->pc_wr = idx;
> +	seqram[idx++] = __SHIFT_OUT(topbits) | SSP_ADDR_REG;
> +	if (botbits)
> +		seqram[idx++] = __SHIFT_OUT(botbits)  | SSP_DATA_REG;
> +	seqram[idx++] = SSP_OPCODE_STOP | SSP_OUT_MODE | cs_en | clk;
> +
> +	/* Read */
> +	hw->pc_rd = idx;
> +	if (botbits)
> +		seqram[idx++] = __SHIFT_IN(botbits) | SSP_ADDR_REG;
> +	seqram[idx++] = __SHIFT_IN(topbits) | SSP_DATA_REG;
> +	seqram[idx++] = SSP_OPCODE_STOP | SSP_OUT_MODE | cs_en | clk;
> +
> +	error = ti_ssp_load(hw->dev, 0, seqram, idx);
> +	if (error < 0)
> +		return error;
> +
> +	error = ti_ssp_set_mode(hw->dev, ((mode & SPI_CPHA) ?
> +					  0 : SSP_EARLY_DIN));
> +	if (error < 0)
> +		return error;
> +
> +	hw->bpw = bpw;
> +	hw->mode = mode;
> +
> +	return error;
> +}
> +
> +static void ti_ssp_spi_work(struct work_struct *work)
> +{
> +	struct ti_ssp_spi *hw = container_of(work, struct ti_ssp_spi, work);
> +
> +	spin_lock_irq(&hw->lock);
> +
> +	 while (!list_empty(&hw->msg_queue)) {
> +		struct spi_message	*m;
> +		struct spi_device	*spi;
> +		struct spi_transfer	*t = NULL;
> +		int			status = 0;
> +
> +		m = container_of(hw->msg_queue.next, struct spi_message,
> +				 queue);
> +
> +		list_del_init(&m->queue);
> +
> +		spi = m->spi;
> +
> +		if (hw->pdata->select)
> +			hw->pdata->select(spi->chip_select);
> +
> +		list_for_each_entry(t, &m->transfers, transfer_list) {
> +			int bpw = spi->bits_per_word;
> +			int xfer_status;
> +
> +			if (t->bits_per_word)
> +				bpw = t->bits_per_word;
> +
> +			if (setup_xfer(hw, bpw, spi->mode) < 0)
> +				break;
> +
> +			chip_select(hw, 1);
> +
> +			spin_unlock_irq(&hw->lock);
> +
> +			xfer_status = do_transfer(hw, m, t);
> +			if (xfer_status < 0)
> +				status = xfer_status;
> +
> +			if (t->delay_usecs)
> +				udelay(t->delay_usecs);
> +
> +			spin_lock_irq(&hw->lock);
> +
> +			if (t->cs_change)
> +				chip_select(hw, 0);
> +		}
> +
> +		chip_select(hw, 0);
> +		m->status = status;
> +		m->complete(m->context);
> +	}
> +
> +	if (hw->shutdown)
> +		complete(&hw->complete);
> +
> +	spin_unlock_irq(&hw->lock);
> +}
> +
> +static int ti_ssp_spi_setup(struct spi_device *spi)
> +{
> +	if (spi->bits_per_word > 32)
> +		return -EINVAL;
> +
> +	return 0;
> +}
> +
> +static int ti_ssp_spi_transfer(struct spi_device *spi, struct spi_message *m)
> +{
> +	struct ti_ssp_spi	*hw;
> +	struct spi_transfer	*t;
> +	unsigned long		flags;
> +	int			error = 0;
> +
> +	m->actual_length = 0;
> +	m->status = -EINPROGRESS;
> +
> +	hw = spi_master_get_devdata(spi->master);
> +
> +	if (list_empty(&m->transfers) || !m->complete)
> +		return -EINVAL;
> +
> +	list_for_each_entry(t, &m->transfers, transfer_list) {
> +		if (t->len && !(t->rx_buf || t->tx_buf)) {
> +			dev_err(&spi->dev, "invalid xfer, no buffer\n");
> +			return -EINVAL;
> +		}
> +
> +		if (t->len && t->rx_buf && t->tx_buf) {
> +			dev_err(&spi->dev, "invalid xfer, full duplex\n");
> +			return -EINVAL;
> +		}
> +
> +		if (t->bits_per_word > 32) {
> +			dev_err(&spi->dev, "invalid xfer width %d\n",
> +				t->bits_per_word);
> +			return -EINVAL;
> +		}
> +	}
> +
> +	spin_lock_irqsave(&hw->lock, flags);
> +	if (hw->shutdown) {
> +		error = -ESHUTDOWN;
> +		goto error_unlock;
> +	}
> +	list_add_tail(&m->queue, &hw->msg_queue);
> +	queue_work(hw->workqueue, &hw->work);
> +error_unlock:
> +	spin_unlock_irqrestore(&hw->lock, flags);
> +	return error;
> +}
> +
> +static int __devinit ti_ssp_spi_probe(struct platform_device *pdev)
> +{
> +	const struct ti_ssp_spi_data *pdata;
> +	struct ti_ssp_spi *hw;
> +	struct spi_master *master;
> +	struct device *dev = &pdev->dev;
> +	int error = 0;
> +
> +	pdata = dev->platform_data;
> +	if (!pdata) {
> +		dev_err(dev, "platform data not found\n");
> +		return -EINVAL;
> +	}
> +
> +	master = spi_alloc_master(dev, sizeof(struct ti_ssp_spi));
> +	if (!master) {
> +		dev_err(dev, "cannot allocate SPI master\n");
> +		return -ENOMEM;
> +	}
> +
> +	hw = spi_master_get_devdata(master);
> +	platform_set_drvdata(pdev, hw);
> +
> +	hw->master = master;
> +	hw->dev = dev;
> +	hw->pdata = pdata;
> +
> +	spin_lock_init(&hw->lock);
> +	init_completion(&hw->complete);
> +	INIT_LIST_HEAD(&hw->msg_queue);
> +	INIT_WORK(&hw->work, ti_ssp_spi_work);
> +
> +	hw->workqueue = create_singlethread_workqueue(dev_name(dev));
> +	if (!hw->workqueue) {
> +		error = -ENOMEM;
> +		dev_err(dev, "work queue creation failed\n");
> +		goto error_wq;
> +	}
> +
> +	master->bus_num		= pdev->id;
> +	master->num_chipselect	= hw->pdata->num_cs;
> +	master->mode_bits	= MODE_BITS;
> +	master->flags		= SPI_MASTER_HALF_DUPLEX;
> +	master->setup		= ti_ssp_spi_setup;
> +	master->transfer	= ti_ssp_spi_transfer;
> +
> +	error = spi_register_master(master);
> +	if (error) {
> +		dev_err(dev, "master registration failed\n");
> +		goto error_reg;
> +	}
> +
> +	return 0;
> +
> +error_reg:
> +	destroy_workqueue(hw->workqueue);
> +error_wq:
> +	spi_master_put(master);
> +	return error;
> +}
> +
> +static int __devexit ti_ssp_spi_remove(struct platform_device *pdev)
> +{
> +	struct ti_ssp_spi *hw = platform_get_drvdata(pdev);
> +	int error;
> +
> +	hw->shutdown = 1;
> +	while (!list_empty(&hw->msg_queue)) {
> +		error = wait_for_completion_interruptible(&hw->complete);
> +		if (error < 0) {
> +			hw->shutdown = 0;
> +			return error;
> +		}
> +	}
> +	destroy_workqueue(hw->workqueue);
> +	spi_unregister_master(hw->master);
> +
> +	return 0;
> +}
> +
> +static struct platform_driver ti_ssp_spi_driver = {
> +	.probe		= ti_ssp_spi_probe,
> +	.remove		= __devexit_p(ti_ssp_spi_remove),
> +	.driver		= {
> +		.name	= "ti-ssp-spi",
> +		.owner	= THIS_MODULE,
> +	},
> +};
> +
> +static int __init ti_ssp_spi_init(void)
> +{
> +	return platform_driver_register(&ti_ssp_spi_driver);
> +}
> +subsys_initcall(ti_ssp_spi_init);
> +
> +static void __exit ti_ssp_spi_exit(void)
> +{
> +	platform_driver_unregister(&ti_ssp_spi_driver);
> +}
> +module_exit(ti_ssp_spi_exit);
> +
> +MODULE_DESCRIPTION("SSP SPI Master");
> +MODULE_AUTHOR("Cyril Chemparathy");
> +MODULE_LICENSE("GPL");
> +MODULE_ALIAS("platform:spi_ti_ssp");
> -- 
> 1.7.0.4
> 

  parent reply	other threads:[~2010-10-20 15:32 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2010-10-18 19:25 [PATCH v2 00/12] tnetv107x ssp driver stack Cyril Chemparathy
     [not found] ` <1287429922-18870-1-git-send-email-cyril-l0cyMroinI0@public.gmane.org>
2010-10-18 19:25   ` [PATCH v2 01/12] misc: add driver for sequencer serial port Cyril Chemparathy
     [not found]     ` <1287429922-18870-2-git-send-email-cyril-l0cyMroinI0@public.gmane.org>
2010-10-20 15:22       ` Grant Likely
2010-10-18 19:25   ` [PATCH v2 02/12] davinci: add tnetv107x ssp platform device Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 03/12] davinci: add ssp config for tnetv107x evm board Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 04/12] spi: add ti-ssp spi master driver Cyril Chemparathy
     [not found]     ` <1287429922-18870-5-git-send-email-cyril-l0cyMroinI0@public.gmane.org>
2010-10-20 15:32       ` Grant Likely [this message]
2010-10-18 19:25   ` [PATCH v2 05/12] davinci: add spi devices on tnetv107x evm Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 06/12] regulator: add driver for tps6524x regulator Cyril Chemparathy
     [not found]     ` <1287429922-18870-7-git-send-email-cyril-l0cyMroinI0@public.gmane.org>
2010-10-19  0:22       ` Mark Brown
2010-10-18 19:25   ` [PATCH v2 07/12] davinci: add tnetv107x evm regulators Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 08/12] gpio: add ti-ssp virtual gpio driver Cyril Chemparathy
     [not found]     ` <1287429922-18870-9-git-send-email-cyril-l0cyMroinI0@public.gmane.org>
2010-10-20 15:23       ` Grant Likely
2010-10-21 22:41       ` David Brownell
2010-10-21 23:40         ` Cyril Chemparathy
     [not found]           ` <4CC0CF7C.1030908-l0cyMroinI0@public.gmane.org>
2010-10-22  0:00             ` David Brownell
2010-10-18 19:25   ` [PATCH v2 09/12] davinci: add tnetv107x evm ti-ssp gpio device Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 10/12] backlight: add support for tps6116x controller Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 11/12] davinci: add tnetv107x evm backlight device Cyril Chemparathy
2010-10-18 19:25   ` [PATCH v2 12/12] davinci: add tnetv107x evm i2c eeprom device Cyril Chemparathy
2010-10-21 19:09   ` [PATCH v2 00/12] tnetv107x ssp driver stack Kevin Hilman

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=20101020153251.GC7285@angua.secretlab.ca \
    --to=grant.likely-s3s/wqlpoipyb63q8fvjnq@public.gmane.org \
    --cc=broonie-yzvPICuk2AATkU/dhu1WVueM+bqZidxxQQ4Iyu8u01E@public.gmane.org \
    --cc=cyril-l0cyMroinI0@public.gmane.org \
    --cc=davinci-linux-open-source-VycZQUHpC/PFrsHnngEfi1aTQe2KTcn/@public.gmane.org \
    --cc=dbrownell-Rn4VEauK+AKRv+LV9MX5uipxlwaOVQ5f@public.gmane.org \
    --cc=lrg-kDsPt+C1G03kYMGBc/C6ZA@public.gmane.org \
    --cc=spi-devel-general-5NWGOfrQmneRv+LV9MX5uipxlwaOVQ5f@public.gmane.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).