Прошиваем Xilinx’овскую FPGAшку от ARM’a

Итак, передо мной ныне лежит задача на диссертацию родить графический ускоритель. Причем, может быть даже с 3д (А если не осилим — всегда можно афинными преобразованиями спрайтов ускорить небольшой сабсет OpenGL и сказать что так и планировалось). Про блэкджек и прочие атрибуты успешного проекта молчим — само собой разумеется. Итак, вдоволь наигравшись с симулятором, я решил что пора переползать на железо и оттебажить мой вериложный быдлокод на FPGA.

В наличии имелась вот такая борда от стартеркита, в составе которой есть крайне годная ПЛИСина, которую я и решил юзануть.

По самой борде можно сказать, что разводка на ней оказалась не очень удачная. На кой ляд такому решению GSM/GPS модуль, да еще и не самый удачный — не знаю. По счастью я его не покупал. Что касается софта, то родные ядра от стартеркита я даже не собирал. Беглый осмотр исходников показал, что народ не запаривался, даже свой mach-type не регистрировал — напихал специфичных для своей платы костылей в board-sam9260ek.c, и все.Для разработки я взял последнее стабильное ядро arm ветки (.39) и накатал на него патчи для at91. Оные необходимы чтобы появились дрова для дополнительного фарша периферии, типа нанда. Очередь за FPGA.

Разводка позволяет посадить ПЛИС на внешнюю шину, далее уже дело техники научить плис прикидываться статическим ОЗУ, в котором будут регистры моей графической железки.

Но каждый раз цеплять тормознутый JTAG кабель мне как-то совершенно не понравилось. Посему я открыл даташиты и кое что родил:

У Xilinx’овских FPGAшек есть несколько режимов загрузки конфига. Один из них — Slave Serial Mode, когда управляющий контроллер загружает данные по последовательному интерфейсу в FPGA. Для этого нам нужно выставить три mode провода в «1» (накинуть джамперы, в случае стартеркитовской борды) и совсем небольшое количество ног —

  • DIN — по этой ноге мы будем гадить данными в FPGA
  • CLK — этой ногой мы будем тактировать передачу.
  • DONE — по сигналу на этой ноге мы узнаем, что плисина сожрала конфигурацию, и готова к работе
  • PROG_B — эта нога ресетит плисину и вытирает из нее конфигурацию
  • INIT_B  — эта нога говорит нам о состоянии плисины, произошла ли ошибка, или еще чего.

После чтения схемы получаем примерно такую распиновку. Обращаю внимание, что на стартеркитовской борде нужно снять J22 тянущий PROG_B на землю.

  • DIN — PC7
  • PROG_B — PC9
  • CLK — PC6
  • DONE — PC4
  • INIT_B — хрен вам.

INIT_B стартеркитовцы вывели только наружу. По сути этим они лишают нас возможности читать наличие/отсутствие ошибки. Не так важно, но я бы предпочел иметь эту ногу в наличии. Можно конечно соединить ее с любым GPIO, при помоши куска провода в разъеме, но мне это не понравилось тем, что провод будет постоянно теряться. Немного покурив схему, я нашел, что нога EN_GSM которая врубает GSM модуль, отсутствующий на моей плате более никуда не заведена, на нее даже нет тестового пятака. Потому я откопал на плате нужное переходное отверстие, которые любезно тут не закрывали маской, и пробросил ее тонким проводком на INIT_B, чтобы не городить огород с внешними соединениями.

Теперь время софта. Немного подумав, я решил, что идеальный способ прошивки плиса выглядел бы так:

cat bitstream.bin > /dev/fpga0

Есть такая хреновина в линухе — character device. А еще есть готовая рыба ‘miscdevice’, которая упрощает создание последних. Так и родился драйвер xilinx-sscu (Xilinx Slave Serial Configuration Upload). Работает он до безобразия тупо, и пользует для пересыла прошивки обычный gpiolib API, и, наверное самое приятное, может иметь сколько угодно инстансов.

Итак, начнем с кода, который я наваял в бордоспецифичый файл (arch/arm/mach-at91/board-charlene.c)

static struct xsscu_data charlene_xsscu_pdata[] = {
  {
  .name="Xilinx XC3S500E Spartan",
  .sout=AT91_PIN_PC7,
  .prog_b=AT91_PIN_PC9,
  .clk=AT91_PIN_PC6,
  .done=AT91_PIN_PC4,
  .init_b=AT91_PIN_PC10,
  },
};
 
static int __init charlene_register_xsscu(struct xsscu_data* pdata, int count)
{
  int i,err;
  struct platform_device *pdev;
  for (i=0;idev.platform_data=&pdata[i];
   err = platform_device_add(pdev);
   if (err) break;
  }
  if (err) printk(KERN_INFO "Registration failed: %d\n",err);
  return err;
}

Оный код регистрирует одну или более плисин через механизм platform-device’ов. По сути говорит что на каких пинах висит, а так же вызывает at91-специфичный функции отрубающие в реактор переферийные блоки.

Далее сам платформ-драйвер.
include/linux/xilinx-sscu.h

#ifndef _XILINX_SSCU
#define _XILINX_SSCU
struct xsscu_data {
	char s *name;
	unsigned int clk;
	unsigned int sout;
	unsigned int init_b;
	unsigned int prog_b;
	unsigned int done;
};
 
enum {
	XSSCU_STATE_IDLE,
	XSSCU_STATE_UPLOADING,
	XSSCU_STATE_UPLOAD_DONE,
	XSSCU_STATE_DISABLED,
	XSSCU_STATE_PROG_ERROR,
};
 
struct xsscu_device_data {
	struct xsscu_data *pdata;
	int open;
	int state;
	char *read_ptr;
	char msg_buffer[128];
};
#endif

Ну и конечно drivers/char/xilinx-sscu.c

/*
 *  linux/drivers/char/xilinx-sscu.c
 *
 *  Copyright (C) 2011 Andrew 'Necromant' Andrianov <email_chewed_up>
 *
 * 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/device.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
 
#include <linux/platform_device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
 
#include <linux/types.h>
#include <linux/cdev.h>
 
#include <linux/xilinx-sscu.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <linux/delay.h>
 
#define DRVNAME "xilinx-sscu"
#define DEVNAME "fpga"
#define DRVVER	"0.1"
 
static int g_debug;
module_param(g_debug, int, 0);	/* and these 2 lines */
MODULE_PARM_DESC(g_debug, "Print lots of useless debug info.");
 
/* This delay is system specific. In my case (200Mhz ARM) I can safely
   define it to nothing to speed things up. But on a faster system you
   may want to define it to something, e.g. udelay(100) if the clk will
   get too fast and crew things up. I do not have a chance to check if
   it's needed on a faster system, so I left it here to be 100% sure.
   Have fun
*/
 
#define DELAY
 
#define DBG(fmt, ...)	if (g_debug) \
    printk(KERN_DEBUG "%s/%s: " fmt " \n", DRVNAME, __FUNCTION__, ##__VA_ARGS__)
#define INF(fmt, ...)	printk(KERN_INFO "%s: " fmt " \n", DRVNAME, ##__VA_ARGS__)
#define ERR(fmt, ...)	printk(KERN_ERR "%s: " fmt " \n", DRVNAME, ##__VA_ARGS__)
 
static inline char *xsscu_state2char(struct xsscu_device_data *dev_data)
{
	switch (dev_data->state) {
	case XSSCU_STATE_UPLOAD_DONE:
	case XSSCU_STATE_IDLE:
		if (gpio_get_value(dev_data->pdata->done))
			return "Online";
		else
			return "Unprogrammed/Error";
	case XSSCU_STATE_DISABLED:
		return "Offline";
	case XSSCU_STATE_PROG_ERROR:
		return "Bitstream error";
	default:
		return "Bug!";
	}
}
 
static int xsscu_open(struct inode *inode, struct file *file)
{
	struct miscdevice *misc;
	struct xsscu_device_data *dev_data;
	misc = file->private_data;
	dev_data = misc->this_device->platform_data;
	if (dev_data->open)
		return -EBUSY;
	dev_data->open++;
	DBG("Device %s opened", dev_data->pdata->name);
	sprintf(dev_data->msg_buffer,
		"DEVICE:\t%s\nINIT_B:\t%d\nDONE:\t%d\nSTATE:\t%s\n",
		dev_data->pdata->name,
		gpio_get_value(dev_data->pdata->init_b),
		gpio_get_value(dev_data->pdata->done),
		xsscu_state2char(dev_data)
	    );
	dev_data->read_ptr = dev_data->msg_buffer;
	return 0;
}
 
static int send_clocks(struct xsscu_data *p, int c)
{
 
	while (c--) {
		gpio_direction_output(p->clk, 0);
		DELAY;
		gpio_direction_output(p->clk, 1);
		DELAY;
		if (1 == gpio_get_value(p->done))
			return 0;
	}
	return 1;
}
 
static inline void xsscu_dbg_state(struct xsscu_data *p)
{
	DBG("INIT_B: %d | DONE: %d",
	    gpio_get_value(p->init_b), gpio_get_value(p->done));
}
 
static int xsscu_release(struct inode *inode, struct file *file)
{
	struct miscdevice *misc;
	struct xsscu_device_data *dev_data;
	int err = 0;
	misc = file->private_data;
	dev_data = misc->this_device->platform_data;
	dev_data->open--;
	switch (dev_data->state) {
	case XSSCU_STATE_UPLOADING:
		err = send_clocks(dev_data->pdata, 10000);
		dev_data->state = XSSCU_STATE_UPLOAD_DONE;
		break;
	case XSSCU_STATE_DISABLED:
		err = 0;
		break;
	}
 
	if (err) {
		ERR("DONE not HIGH or other programming error");
		dev_data->state = XSSCU_STATE_PROG_ERROR;
	}
	xsscu_dbg_state(dev_data->pdata);
	DBG("Device closed");
	/* We must still close the device, hence return ok */
	return 0;
}
 
static ssize_t xsscu_read(struct file *filp, char *buffer,
			  size_t length,
			  loff_t *offset)
{
	struct miscdevice *misc;
	struct xsscu_device_data *dev_data;
	int bytes_read = 0;
	misc = filp->private_data;
	dev_data = misc->this_device->platform_data;
 
	if (*dev_data->read_ptr == 0)
		return 0;
	while (length && *dev_data->read_ptr) {
		put_user(*(dev_data->read_ptr++), buffer++);
		length--;
		bytes_read++;
	}
	return bytes_read;
}
 
static int xsscu_reset_fpga(struct xsscu_data *p)
{
	int i = 50;
	DBG("Resetting FPGA...");
	gpio_direction_output(p->prog_b, 0);
	mdelay(1);
	gpio_direction_output(p->prog_b, 1);
	while (i--) {
		xsscu_dbg_state(p);
		if (gpio_get_value(p->init_b) == 1)
			return 0;
		mdelay(1);
	}
	ERR("FPGA reset failed");
	return 1;
}
 
static ssize_t xsscu_write(struct file *filp,
			   const char *buff, size_t len, loff_t * off)
{
	struct miscdevice *misc;
	struct xsscu_device_data *dev_data;
	int i;
	int k;
	i = 0;
	misc = filp->private_data;
	dev_data = misc->this_device->platform_data;
 
	if ((*off == 0)) {
		if (strncmp(buff, "disable", 7) == 0) {
			DBG("Disabling FPGA");
			gpio_direction_output(dev_data->pdata->prog_b, 0);
			dev_data->state = XSSCU_STATE_DISABLED;
			goto all_written;
		} else if (xsscu_reset_fpga(dev_data->pdata) != 0)
			return -EIO;
		/*Wait a little bit, before starting to clock the fpga,
		as the datasheet suggests */
		mdelay(1);
		gpio_direction_output(dev_data->pdata->clk, 0);
		dev_data->state = XSSCU_STATE_UPLOADING;
	}
	/* bitbang data */
	while (i < len) {
		for (k = 7; k >= 0; k--) {
			gpio_direction_output(dev_data->pdata->sout,
					      (buff[i] & (1 << k)));
			gpio_direction_output(dev_data->pdata->clk, 1);
			DELAY;
			gpio_direction_output(dev_data->pdata->clk, 0);
			DELAY;
		}
		i++;
	}
all_written:
	*off += len;
	return len;
}
 
static const struct file_operations xsscu_fileops = {
	.owner = THIS_MODULE,
	.write = xsscu_write,
	.read = xsscu_read,
	.open = xsscu_open,
	.release = xsscu_release,
	.llseek = no_llseek,
};
 
static int xsscu_create_miscdevice(struct platform_device *p, int id)
{
	struct miscdevice *mdev;
	struct xsscu_device_data *dev_data;
	char *nm;
	int err;
	mdev = kzalloc(sizeof(struct miscdevice), GFP_KERNEL);
	if (!mdev) {
		ERR("Misc device allocation failed");
		return -ENOMEM;
	}
	nm = kzalloc(64, GFP_KERNEL);
	if (!nm) {
		err = -ENOMEM;
		goto freemisc;
	}
	dev_data = kzalloc(sizeof(struct xsscu_device_data), GFP_KERNEL);
	if (!dev_data) {
		err = -ENOMEM;
		goto freenm;
	}
 
	snprintf(nm, 64, "fpga%d", id);
	mdev->name = nm;
	mdev->fops = &xsscu_fileops;
	mdev->minor = MISC_DYNAMIC_MINOR;
	err = misc_register(mdev);
	if (!err) {
		mdev->this_device->platform_data = dev_data;
		dev_data->pdata = p->dev.platform_data;
	}
 
	return err;
 
freenm:
	kfree(nm);
freemisc:
	kfree(mdev);
 
	return err;
}
 
static int xsscu_probe(struct platform_device *p)
{
	int err;
	int id;
	struct xsscu_data *pdata = p->dev.platform_data;
	/* some id magic */
	if (p->id == -1)
		id = 0;
	else
		id = p->id;
	DBG("Probing xsscu platform device with id %d", p->id);
	if (!pdata) {
		ERR("Missing platform_data, sorry dude");
		return -ENOMEM;
	}
	/* claim gpio pins */
	err = gpio_request(pdata->clk, "xilinx-sscu-clk") +
	    gpio_request(pdata->done, "xilinx-sscu-done") +
	    gpio_request(pdata->init_b, "xilinx-sscu-init_b") +
	    gpio_request(pdata->prog_b, "xilinx-sscu-prog_b") +
	    gpio_request(pdata->sout, "xilinx-sscu-sout");
	if (err) {
		ERR("Failed to claim required GPIOs, bailing out");
		return err;
	}
 
	gpio_direction_input(pdata->init_b);
	gpio_direction_input(pdata->done);
 
	err = xsscu_create_miscdevice(p, id);
	if (!err)
		INF("FPGA Device %s registered as /dev/fpga%d", pdata->name,
		    id);
	return err;
}
 
static struct platform_driver xsscu_driver = {
	.probe = xsscu_probe,
	.driver = {
		   .name = DRVNAME,
		   .owner = THIS_MODULE,
		   }
};
 
static int __init xsscu_init(void)
{
	INF("Xilinx Slave Serial Configuration Upload Driver " DRVVER);
	return platform_driver_register(&xsscu_driver);
}
 
static void __exit xsscu_cleanup(void)
{
	/* Normally you would not like to unload this driver. */
}
 
module_init(xsscu_init);
module_exit(xsscu_cleanup);
 
MODULE_AUTHOR("Andrew 'Necromant' Andrianov <necromant@necromant.ath.cx>");
MODULE_DESCRIPTION("Xilinx Slave Serial BitBang Uploader driver");
MODULE_LICENSE("GPL");

Теперь прочитав /dev/fpga0, можно узнать информацию о плисе (когда DONE в «1» — она готова к работе),
А записав туда битстрим — перепрошить. Для сброса достаточно записать в /dev/fpga0 какой-нибудь символ. ПЛИС ресетнется, не найдет синхронизационного слова в битстриме и будет его ждать.
Вот, собственно и все. Для удобства, прошивать можно и по ссх, с хост-компьютера

cat ./bitstream.bin | ssh board "cat > /dev/fpga0"
ssh board "cat /dev/fpga0"

По времени оно тоже работает очень даже ничего - у меня прошивка моего XC3S500E занимает около 6-7 секунд.
Осталось только причесать код и отправить патч в апстрим.
LKML# https://lkml.org/lkml/2011/11/19/119

Добавить комментарий