2019-05-20 10:19:02 +03:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
2007-01-25 15:15:05 +03:00
|
|
|
/*
|
|
|
|
* Driver for Midiman Portman2x4 parallel port midi interface
|
|
|
|
*
|
|
|
|
* Copyright (c) by Levent Guendogdu <levon@feature-it.com>
|
|
|
|
*
|
|
|
|
* ChangeLog
|
|
|
|
* Jan 24 2007 Matthias Koenig <mkoenig@suse.de>
|
|
|
|
* - cleanup and rewrite
|
|
|
|
* Sep 30 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - source code cleanup
|
|
|
|
* Sep 03 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - fixed compilation problem with alsa 1.0.6a (removed MODULE_CLASSES,
|
|
|
|
* MODULE_PARM_SYNTAX and changed MODULE_DEVICES to
|
|
|
|
* MODULE_SUPPORTED_DEVICE)
|
|
|
|
* Mar 24 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - added 2.6 kernel support
|
|
|
|
* Mar 18 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - added parport_unregister_driver to the startup routine if the driver fails to detect a portman
|
|
|
|
* - added support for all 4 output ports in portman_putmidi
|
|
|
|
* Mar 17 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - added checks for opened input device in interrupt handler
|
|
|
|
* Feb 20 2004 Tobias Gehrig <tobias@gehrig.tk>
|
|
|
|
* - ported from alsa 0.5 to 1.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <linux/init.h>
|
|
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <linux/parport.h>
|
|
|
|
#include <linux/spinlock.h>
|
|
|
|
#include <linux/delay.h>
|
include cleanup: Update gfp.h and slab.h includes to prepare for breaking implicit slab.h inclusion from percpu.h
percpu.h is included by sched.h and module.h and thus ends up being
included when building most .c files. percpu.h includes slab.h which
in turn includes gfp.h making everything defined by the two files
universally available and complicating inclusion dependencies.
percpu.h -> slab.h dependency is about to be removed. Prepare for
this change by updating users of gfp and slab facilities include those
headers directly instead of assuming availability. As this conversion
needs to touch large number of source files, the following script is
used as the basis of conversion.
http://userweb.kernel.org/~tj/misc/slabh-sweep.py
The script does the followings.
* Scan files for gfp and slab usages and update includes such that
only the necessary includes are there. ie. if only gfp is used,
gfp.h, if slab is used, slab.h.
* When the script inserts a new include, it looks at the include
blocks and try to put the new include such that its order conforms
to its surrounding. It's put in the include block which contains
core kernel includes, in the same order that the rest are ordered -
alphabetical, Christmas tree, rev-Xmas-tree or at the end if there
doesn't seem to be any matching order.
* If the script can't find a place to put a new include (mostly
because the file doesn't have fitting include block), it prints out
an error message indicating which .h file needs to be added to the
file.
The conversion was done in the following steps.
1. The initial automatic conversion of all .c files updated slightly
over 4000 files, deleting around 700 includes and adding ~480 gfp.h
and ~3000 slab.h inclusions. The script emitted errors for ~400
files.
2. Each error was manually checked. Some didn't need the inclusion,
some needed manual addition while adding it to implementation .h or
embedding .c file was more appropriate for others. This step added
inclusions to around 150 files.
3. The script was run again and the output was compared to the edits
from #2 to make sure no file was left behind.
4. Several build tests were done and a couple of problems were fixed.
e.g. lib/decompress_*.c used malloc/free() wrappers around slab
APIs requiring slab.h to be added manually.
5. The script was run on all .h files but without automatically
editing them as sprinkling gfp.h and slab.h inclusions around .h
files could easily lead to inclusion dependency hell. Most gfp.h
inclusion directives were ignored as stuff from gfp.h was usually
wildly available and often used in preprocessor macros. Each
slab.h inclusion directive was examined and added manually as
necessary.
6. percpu.h was updated not to include slab.h.
7. Build test were done on the following configurations and failures
were fixed. CONFIG_GCOV_KERNEL was turned off for all tests (as my
distributed build env didn't work with gcov compiles) and a few
more options had to be turned off depending on archs to make things
build (like ipr on powerpc/64 which failed due to missing writeq).
* x86 and x86_64 UP and SMP allmodconfig and a custom test config.
* powerpc and powerpc64 SMP allmodconfig
* sparc and sparc64 SMP allmodconfig
* ia64 SMP allmodconfig
* s390 SMP allmodconfig
* alpha SMP allmodconfig
* um on x86_64 SMP allmodconfig
8. percpu.h modifications were reverted so that it could be applied as
a separate patch and serve as bisection point.
Given the fact that I had only a couple of failures from tests on step
6, I'm fairly confident about the coverage of this conversion patch.
If there is a breakage, it's likely to be something in one of the arch
headers which should be easily discoverable easily on most builds of
the specific arch.
Signed-off-by: Tejun Heo <tj@kernel.org>
Guess-its-ok-by: Christoph Lameter <cl@linux-foundation.org>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Lee Schermerhorn <Lee.Schermerhorn@hp.com>
2010-03-24 11:04:11 +03:00
|
|
|
#include <linux/slab.h>
|
2011-07-15 20:38:28 +04:00
|
|
|
#include <linux/module.h>
|
2007-01-25 15:15:05 +03:00
|
|
|
#include <sound/core.h>
|
|
|
|
#include <sound/initval.h>
|
|
|
|
#include <sound/rawmidi.h>
|
|
|
|
#include <sound/control.h>
|
|
|
|
|
|
|
|
#define CARD_NAME "Portman 2x4"
|
|
|
|
#define DRIVER_NAME "portman"
|
|
|
|
#define PLATFORM_DRIVER "snd_portman2x4"
|
|
|
|
|
|
|
|
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
|
|
|
|
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
|
2011-12-15 07:19:36 +04:00
|
|
|
static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
|
2007-01-25 15:15:05 +03:00
|
|
|
|
|
|
|
static struct platform_device *platform_devices[SNDRV_CARDS];
|
|
|
|
static int device_count;
|
|
|
|
|
2018-05-23 22:20:59 +03:00
|
|
|
module_param_array(index, int, NULL, 0444);
|
2007-01-25 15:15:05 +03:00
|
|
|
MODULE_PARM_DESC(index, "Index value for " CARD_NAME " soundcard.");
|
2018-05-23 22:20:59 +03:00
|
|
|
module_param_array(id, charp, NULL, 0444);
|
2007-01-25 15:15:05 +03:00
|
|
|
MODULE_PARM_DESC(id, "ID string for " CARD_NAME " soundcard.");
|
2018-05-23 22:20:59 +03:00
|
|
|
module_param_array(enable, bool, NULL, 0444);
|
2007-01-25 15:15:05 +03:00
|
|
|
MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
|
|
|
|
|
|
|
|
MODULE_AUTHOR("Levent Guendogdu, Tobias Gehrig, Matthias Koenig");
|
|
|
|
MODULE_DESCRIPTION("Midiman Portman2x4");
|
|
|
|
MODULE_LICENSE("GPL");
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* Chip specific
|
|
|
|
*********************************************************************/
|
|
|
|
#define PORTMAN_NUM_INPUT_PORTS 2
|
|
|
|
#define PORTMAN_NUM_OUTPUT_PORTS 4
|
|
|
|
|
|
|
|
struct portman {
|
|
|
|
spinlock_t reg_lock;
|
|
|
|
struct snd_card *card;
|
|
|
|
struct snd_rawmidi *rmidi;
|
|
|
|
struct pardevice *pardev;
|
|
|
|
int open_count;
|
|
|
|
int mode[PORTMAN_NUM_INPUT_PORTS];
|
|
|
|
struct snd_rawmidi_substream *midi_input[PORTMAN_NUM_INPUT_PORTS];
|
|
|
|
};
|
|
|
|
|
|
|
|
static int portman_free(struct portman *pm)
|
|
|
|
{
|
|
|
|
kfree(pm);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2012-12-06 21:35:27 +04:00
|
|
|
static int portman_create(struct snd_card *card,
|
|
|
|
struct pardevice *pardev,
|
|
|
|
struct portman **rchip)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
struct portman *pm;
|
|
|
|
|
|
|
|
*rchip = NULL;
|
|
|
|
|
|
|
|
pm = kzalloc(sizeof(struct portman), GFP_KERNEL);
|
|
|
|
if (pm == NULL)
|
|
|
|
return -ENOMEM;
|
|
|
|
|
|
|
|
/* Init chip specific data */
|
|
|
|
spin_lock_init(&pm->reg_lock);
|
|
|
|
pm->card = card;
|
|
|
|
pm->pardev = pardev;
|
|
|
|
|
|
|
|
*rchip = pm;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* HW related constants
|
|
|
|
*********************************************************************/
|
|
|
|
|
|
|
|
/* Standard PC parallel port status register equates. */
|
|
|
|
#define PP_STAT_BSY 0x80 /* Busy status. Inverted. */
|
|
|
|
#define PP_STAT_ACK 0x40 /* Acknowledge. Non-Inverted. */
|
|
|
|
#define PP_STAT_POUT 0x20 /* Paper Out. Non-Inverted. */
|
|
|
|
#define PP_STAT_SEL 0x10 /* Select. Non-Inverted. */
|
|
|
|
#define PP_STAT_ERR 0x08 /* Error. Non-Inverted. */
|
|
|
|
|
|
|
|
/* Standard PC parallel port command register equates. */
|
|
|
|
#define PP_CMD_IEN 0x10 /* IRQ Enable. Non-Inverted. */
|
|
|
|
#define PP_CMD_SELI 0x08 /* Select Input. Inverted. */
|
|
|
|
#define PP_CMD_INIT 0x04 /* Init Printer. Non-Inverted. */
|
|
|
|
#define PP_CMD_FEED 0x02 /* Auto Feed. Inverted. */
|
|
|
|
#define PP_CMD_STB 0x01 /* Strobe. Inverted. */
|
|
|
|
|
|
|
|
/* Parallel Port Command Register as implemented by PCP2x4. */
|
|
|
|
#define INT_EN PP_CMD_IEN /* Interrupt enable. */
|
|
|
|
#define STROBE PP_CMD_STB /* Command strobe. */
|
|
|
|
|
|
|
|
/* The parallel port command register field (b1..b3) selects the
|
|
|
|
* various "registers" within the PC/P 2x4. These are the internal
|
|
|
|
* address of these "registers" that must be written to the parallel
|
|
|
|
* port command register.
|
|
|
|
*/
|
|
|
|
#define RXDATA0 (0 << 1) /* PCP RxData channel 0. */
|
|
|
|
#define RXDATA1 (1 << 1) /* PCP RxData channel 1. */
|
|
|
|
#define GEN_CTL (2 << 1) /* PCP General Control Register. */
|
|
|
|
#define SYNC_CTL (3 << 1) /* PCP Sync Control Register. */
|
|
|
|
#define TXDATA0 (4 << 1) /* PCP TxData channel 0. */
|
|
|
|
#define TXDATA1 (5 << 1) /* PCP TxData channel 1. */
|
|
|
|
#define TXDATA2 (6 << 1) /* PCP TxData channel 2. */
|
|
|
|
#define TXDATA3 (7 << 1) /* PCP TxData channel 3. */
|
|
|
|
|
|
|
|
/* Parallel Port Status Register as implemented by PCP2x4. */
|
|
|
|
#define ESTB PP_STAT_POUT /* Echoed strobe. */
|
|
|
|
#define INT_REQ PP_STAT_ACK /* Input data int request. */
|
|
|
|
#define BUSY PP_STAT_ERR /* Interface Busy. */
|
|
|
|
|
|
|
|
/* Parallel Port Status Register BUSY and SELECT lines are multiplexed
|
|
|
|
* between several functions. Depending on which 2x4 "register" is
|
|
|
|
* currently selected (b1..b3), the BUSY and SELECT lines are
|
|
|
|
* assigned as follows:
|
|
|
|
*
|
|
|
|
* SELECT LINE: A3 A2 A1
|
|
|
|
* --------
|
|
|
|
*/
|
|
|
|
#define RXAVAIL PP_STAT_SEL /* Rx Available, channel 0. 0 0 0 */
|
|
|
|
// RXAVAIL1 PP_STAT_SEL /* Rx Available, channel 1. 0 0 1 */
|
|
|
|
#define SYNC_STAT PP_STAT_SEL /* Reserved - Sync Status. 0 1 0 */
|
|
|
|
// /* Reserved. 0 1 1 */
|
|
|
|
#define TXEMPTY PP_STAT_SEL /* Tx Empty, channel 0. 1 0 0 */
|
|
|
|
// TXEMPTY1 PP_STAT_SEL /* Tx Empty, channel 1. 1 0 1 */
|
|
|
|
// TXEMPTY2 PP_STAT_SEL /* Tx Empty, channel 2. 1 1 0 */
|
|
|
|
// TXEMPTY3 PP_STAT_SEL /* Tx Empty, channel 3. 1 1 1 */
|
|
|
|
|
|
|
|
/* BUSY LINE: A3 A2 A1
|
|
|
|
* --------
|
|
|
|
*/
|
|
|
|
#define RXDATA PP_STAT_BSY /* Rx Input Data, channel 0. 0 0 0 */
|
|
|
|
// RXDATA1 PP_STAT_BSY /* Rx Input Data, channel 1. 0 0 1 */
|
|
|
|
#define SYNC_DATA PP_STAT_BSY /* Reserved - Sync Data. 0 1 0 */
|
|
|
|
/* Reserved. 0 1 1 */
|
|
|
|
#define DATA_ECHO PP_STAT_BSY /* Parallel Port Data Echo. 1 0 0 */
|
|
|
|
#define A0_ECHO PP_STAT_BSY /* Address 0 Echo. 1 0 1 */
|
|
|
|
#define A1_ECHO PP_STAT_BSY /* Address 1 Echo. 1 1 0 */
|
|
|
|
#define A2_ECHO PP_STAT_BSY /* Address 2 Echo. 1 1 1 */
|
|
|
|
|
|
|
|
#define PORTMAN2X4_MODE_INPUT_TRIGGERED 0x01
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* Hardware specific functions
|
|
|
|
*********************************************************************/
|
|
|
|
static inline void portman_write_command(struct portman *pm, u8 value)
|
|
|
|
{
|
|
|
|
parport_write_control(pm->pardev->port, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline u8 portman_read_command(struct portman *pm)
|
|
|
|
{
|
|
|
|
return parport_read_control(pm->pardev->port);
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline u8 portman_read_status(struct portman *pm)
|
|
|
|
{
|
|
|
|
return parport_read_status(pm->pardev->port);
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline u8 portman_read_data(struct portman *pm)
|
|
|
|
{
|
|
|
|
return parport_read_data(pm->pardev->port);
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void portman_write_data(struct portman *pm, u8 value)
|
|
|
|
{
|
|
|
|
parport_write_data(pm->pardev->port, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void portman_write_midi(struct portman *pm,
|
|
|
|
int port, u8 mididata)
|
|
|
|
{
|
|
|
|
int command = ((port + 4) << 1);
|
|
|
|
|
|
|
|
/* Get entering data byte and port number in BL and BH respectively.
|
|
|
|
* Set up Tx Channel address field for use with PP Cmd Register.
|
|
|
|
* Store address field in BH register.
|
|
|
|
* Inputs: AH = Output port number (0..3).
|
|
|
|
* AL = Data byte.
|
|
|
|
* command = TXDATA0 | INT_EN;
|
|
|
|
* Align port num with address field (b1...b3),
|
|
|
|
* set address for TXDatax, Strobe=0
|
|
|
|
*/
|
|
|
|
command |= INT_EN;
|
|
|
|
|
|
|
|
/* Disable interrupts so that the process is not interrupted, then
|
|
|
|
* write the address associated with the current Tx channel to the
|
|
|
|
* PP Command Reg. Do not set the Strobe signal yet.
|
|
|
|
*/
|
|
|
|
|
|
|
|
do {
|
|
|
|
portman_write_command(pm, command);
|
|
|
|
|
|
|
|
/* While the address lines settle, write parallel output data to
|
|
|
|
* PP Data Reg. This has no effect until Strobe signal is asserted.
|
|
|
|
*/
|
|
|
|
|
|
|
|
portman_write_data(pm, mididata);
|
|
|
|
|
|
|
|
/* If PCP channel's TxEmpty is set (TxEmpty is read through the PP
|
|
|
|
* Status Register), then go write data. Else go back and wait.
|
|
|
|
*/
|
|
|
|
} while ((portman_read_status(pm) & TXEMPTY) != TXEMPTY);
|
|
|
|
|
|
|
|
/* TxEmpty is set. Maintain PC/P destination address and assert
|
|
|
|
* Strobe through the PP Command Reg. This will Strobe data into
|
|
|
|
* the PC/P transmitter and set the PC/P BUSY signal.
|
|
|
|
*/
|
|
|
|
|
|
|
|
portman_write_command(pm, command | STROBE);
|
|
|
|
|
|
|
|
/* Wait for strobe line to settle and echo back through hardware.
|
|
|
|
* Once it has echoed back, assume that the address and data lines
|
|
|
|
* have settled!
|
|
|
|
*/
|
|
|
|
|
|
|
|
while ((portman_read_status(pm) & ESTB) == 0)
|
|
|
|
cpu_relax();
|
|
|
|
|
|
|
|
/* Release strobe and immediately re-allow interrupts. */
|
|
|
|
portman_write_command(pm, command);
|
|
|
|
|
|
|
|
while ((portman_read_status(pm) & ESTB) == ESTB)
|
|
|
|
cpu_relax();
|
|
|
|
|
|
|
|
/* PC/P BUSY is now set. We must wait until BUSY resets itself.
|
|
|
|
* We'll reenable ints while we're waiting.
|
|
|
|
*/
|
|
|
|
|
|
|
|
while ((portman_read_status(pm) & BUSY) == BUSY)
|
|
|
|
cpu_relax();
|
|
|
|
|
|
|
|
/* Data sent. */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read MIDI byte from port
|
|
|
|
* Attempt to read input byte from specified hardware input port (0..).
|
|
|
|
* Return -1 if no data
|
|
|
|
*/
|
|
|
|
static int portman_read_midi(struct portman *pm, int port)
|
|
|
|
{
|
|
|
|
unsigned char midi_data = 0;
|
|
|
|
unsigned char cmdout; /* Saved address+IE bit. */
|
|
|
|
|
|
|
|
/* Make sure clocking edge is down before starting... */
|
|
|
|
portman_write_data(pm, 0); /* Make sure edge is down. */
|
|
|
|
|
|
|
|
/* Set destination address to PCP. */
|
|
|
|
cmdout = (port << 1) | INT_EN; /* Address + IE + No Strobe. */
|
|
|
|
portman_write_command(pm, cmdout);
|
|
|
|
|
|
|
|
while ((portman_read_status(pm) & ESTB) == ESTB)
|
|
|
|
cpu_relax(); /* Wait for strobe echo. */
|
|
|
|
|
|
|
|
/* After the address lines settle, check multiplexed RxAvail signal.
|
|
|
|
* If data is available, read it.
|
|
|
|
*/
|
|
|
|
if ((portman_read_status(pm) & RXAVAIL) == 0)
|
|
|
|
return -1; /* No data. */
|
|
|
|
|
|
|
|
/* Set the Strobe signal to enable the Rx clocking circuitry. */
|
|
|
|
portman_write_command(pm, cmdout | STROBE); /* Write address+IE+Strobe. */
|
|
|
|
|
|
|
|
while ((portman_read_status(pm) & ESTB) == 0)
|
|
|
|
cpu_relax(); /* Wait for strobe echo. */
|
|
|
|
|
|
|
|
/* The first data bit (msb) is already sitting on the input line. */
|
|
|
|
midi_data = (portman_read_status(pm) & 128);
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 6. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 1) & 64;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 5. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 2) & 32;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 4. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 3) & 16;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 3. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 4) & 8;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 2. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 5) & 4;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 1. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 6) & 2;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
|
|
|
|
/* Data bit 0. */
|
|
|
|
portman_write_data(pm, 0); /* Cause falling edge while data settles. */
|
|
|
|
midi_data |= (portman_read_status(pm) >> 7) & 1;
|
|
|
|
portman_write_data(pm, 1); /* Cause rising edge, which shifts data. */
|
|
|
|
portman_write_data(pm, 0); /* Return data clock low. */
|
|
|
|
|
|
|
|
|
|
|
|
/* De-assert Strobe and return data. */
|
|
|
|
portman_write_command(pm, cmdout); /* Output saved address+IE. */
|
|
|
|
|
|
|
|
/* Wait for strobe echo. */
|
|
|
|
while ((portman_read_status(pm) & ESTB) == ESTB)
|
|
|
|
cpu_relax();
|
|
|
|
|
|
|
|
return (midi_data & 255); /* Shift back and return value. */
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Checks if any input data on the given channel is available
|
|
|
|
* Checks RxAvail
|
|
|
|
*/
|
|
|
|
static int portman_data_avail(struct portman *pm, int channel)
|
|
|
|
{
|
|
|
|
int command = INT_EN;
|
|
|
|
switch (channel) {
|
|
|
|
case 0:
|
|
|
|
command |= RXDATA0;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
command |= RXDATA1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
/* Write hardware (assumme STROBE=0) */
|
|
|
|
portman_write_command(pm, command);
|
|
|
|
/* Check multiplexed RxAvail signal */
|
|
|
|
if ((portman_read_status(pm) & RXAVAIL) == RXAVAIL)
|
|
|
|
return 1; /* Data available */
|
|
|
|
|
|
|
|
/* No Data available */
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Flushes any input
|
|
|
|
*/
|
|
|
|
static void portman_flush_input(struct portman *pm, unsigned char port)
|
|
|
|
{
|
|
|
|
/* Local variable for counting things */
|
|
|
|
unsigned int i = 0;
|
|
|
|
unsigned char command = 0;
|
|
|
|
|
|
|
|
switch (port) {
|
|
|
|
case 0:
|
|
|
|
command = RXDATA0;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
command = RXDATA1;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
snd_printk(KERN_WARNING
|
|
|
|
"portman_flush_input() Won't flush port %i\n",
|
|
|
|
port);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Set address for specified channel in port and allow to settle. */
|
|
|
|
portman_write_command(pm, command);
|
|
|
|
|
|
|
|
/* Assert the Strobe and wait for echo back. */
|
|
|
|
portman_write_command(pm, command | STROBE);
|
|
|
|
|
|
|
|
/* Wait for ESTB */
|
|
|
|
while ((portman_read_status(pm) & ESTB) == 0)
|
|
|
|
cpu_relax();
|
|
|
|
|
|
|
|
/* Output clock cycles to the Rx circuitry. */
|
|
|
|
portman_write_data(pm, 0);
|
|
|
|
|
|
|
|
/* Flush 250 bits... */
|
|
|
|
for (i = 0; i < 250; i++) {
|
|
|
|
portman_write_data(pm, 1);
|
|
|
|
portman_write_data(pm, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Deassert the Strobe signal of the port and wait for it to settle. */
|
|
|
|
portman_write_command(pm, command | INT_EN);
|
|
|
|
|
|
|
|
/* Wait for settling */
|
|
|
|
while ((portman_read_status(pm) & ESTB) == ESTB)
|
|
|
|
cpu_relax();
|
|
|
|
}
|
|
|
|
|
|
|
|
static int portman_probe(struct parport *p)
|
|
|
|
{
|
|
|
|
/* Initialize the parallel port data register. Will set Rx clocks
|
|
|
|
* low in case we happen to be addressing the Rx ports at this time.
|
|
|
|
*/
|
|
|
|
/* 1 */
|
|
|
|
parport_write_data(p, 0);
|
|
|
|
|
|
|
|
/* Initialize the parallel port command register, thus initializing
|
|
|
|
* hardware handshake lines to midi box:
|
|
|
|
*
|
|
|
|
* Strobe = 0
|
|
|
|
* Interrupt Enable = 0
|
|
|
|
*/
|
|
|
|
/* 2 */
|
|
|
|
parport_write_control(p, 0);
|
|
|
|
|
|
|
|
/* Check if Portman PC/P 2x4 is out there. */
|
|
|
|
/* 3 */
|
|
|
|
parport_write_control(p, RXDATA0); /* Write Strobe=0 to command reg. */
|
|
|
|
|
|
|
|
/* Check for ESTB to be clear */
|
|
|
|
/* 4 */
|
|
|
|
if ((parport_read_status(p) & ESTB) == ESTB)
|
|
|
|
return 1; /* CODE 1 - Strobe Failure. */
|
|
|
|
|
|
|
|
/* Set for RXDATA0 where no damage will be done. */
|
|
|
|
/* 5 */
|
2020-05-07 09:48:55 +03:00
|
|
|
parport_write_control(p, RXDATA0 | STROBE); /* Write Strobe=1 to command reg. */
|
2007-01-25 15:15:05 +03:00
|
|
|
|
|
|
|
/* 6 */
|
|
|
|
if ((parport_read_status(p) & ESTB) != ESTB)
|
|
|
|
return 1; /* CODE 1 - Strobe Failure. */
|
|
|
|
|
|
|
|
/* 7 */
|
|
|
|
parport_write_control(p, 0); /* Reset Strobe=0. */
|
|
|
|
|
|
|
|
/* Check if Tx circuitry is functioning properly. If initialized
|
2020-10-05 22:12:23 +03:00
|
|
|
* unit TxEmpty is false, send out char and see if it goes true.
|
2007-01-25 15:15:05 +03:00
|
|
|
*/
|
|
|
|
/* 8 */
|
|
|
|
parport_write_control(p, TXDATA0); /* Tx channel 0, strobe off. */
|
|
|
|
|
|
|
|
/* If PCP channel's TxEmpty is set (TxEmpty is read through the PP
|
|
|
|
* Status Register), then go write data. Else go back and wait.
|
|
|
|
*/
|
|
|
|
/* 9 */
|
|
|
|
if ((parport_read_status(p) & TXEMPTY) == 0)
|
|
|
|
return 2;
|
|
|
|
|
|
|
|
/* Return OK status. */
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int portman_device_init(struct portman *pm)
|
|
|
|
{
|
|
|
|
portman_flush_input(pm, 0);
|
|
|
|
portman_flush_input(pm, 1);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* Rawmidi
|
|
|
|
*********************************************************************/
|
|
|
|
static int snd_portman_midi_open(struct snd_rawmidi_substream *substream)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int snd_portman_midi_close(struct snd_rawmidi_substream *substream)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snd_portman_midi_input_trigger(struct snd_rawmidi_substream *substream,
|
|
|
|
int up)
|
|
|
|
{
|
|
|
|
struct portman *pm = substream->rmidi->private_data;
|
|
|
|
unsigned long flags;
|
|
|
|
|
|
|
|
spin_lock_irqsave(&pm->reg_lock, flags);
|
|
|
|
if (up)
|
|
|
|
pm->mode[substream->number] |= PORTMAN2X4_MODE_INPUT_TRIGGERED;
|
|
|
|
else
|
|
|
|
pm->mode[substream->number] &= ~PORTMAN2X4_MODE_INPUT_TRIGGERED;
|
|
|
|
spin_unlock_irqrestore(&pm->reg_lock, flags);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snd_portman_midi_output_trigger(struct snd_rawmidi_substream *substream,
|
|
|
|
int up)
|
|
|
|
{
|
|
|
|
struct portman *pm = substream->rmidi->private_data;
|
|
|
|
unsigned long flags;
|
|
|
|
unsigned char byte;
|
|
|
|
|
|
|
|
spin_lock_irqsave(&pm->reg_lock, flags);
|
|
|
|
if (up) {
|
|
|
|
while ((snd_rawmidi_transmit(substream, &byte, 1) == 1))
|
|
|
|
portman_write_midi(pm, substream->number, byte);
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&pm->reg_lock, flags);
|
|
|
|
}
|
|
|
|
|
2017-01-05 19:28:39 +03:00
|
|
|
static const struct snd_rawmidi_ops snd_portman_midi_output = {
|
2007-01-25 15:15:05 +03:00
|
|
|
.open = snd_portman_midi_open,
|
|
|
|
.close = snd_portman_midi_close,
|
|
|
|
.trigger = snd_portman_midi_output_trigger,
|
|
|
|
};
|
|
|
|
|
2017-01-05 19:28:39 +03:00
|
|
|
static const struct snd_rawmidi_ops snd_portman_midi_input = {
|
2007-01-25 15:15:05 +03:00
|
|
|
.open = snd_portman_midi_open,
|
|
|
|
.close = snd_portman_midi_close,
|
|
|
|
.trigger = snd_portman_midi_input_trigger,
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Create and initialize the rawmidi component */
|
2012-12-06 21:35:27 +04:00
|
|
|
static int snd_portman_rawmidi_create(struct snd_card *card)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
struct portman *pm = card->private_data;
|
|
|
|
struct snd_rawmidi *rmidi;
|
|
|
|
struct snd_rawmidi_substream *substream;
|
|
|
|
int err;
|
|
|
|
|
|
|
|
err = snd_rawmidi_new(card, CARD_NAME, 0,
|
|
|
|
PORTMAN_NUM_OUTPUT_PORTS,
|
|
|
|
PORTMAN_NUM_INPUT_PORTS,
|
|
|
|
&rmidi);
|
|
|
|
if (err < 0)
|
|
|
|
return err;
|
|
|
|
|
|
|
|
rmidi->private_data = pm;
|
|
|
|
strcpy(rmidi->name, CARD_NAME);
|
|
|
|
rmidi->info_flags = SNDRV_RAWMIDI_INFO_OUTPUT |
|
|
|
|
SNDRV_RAWMIDI_INFO_INPUT |
|
|
|
|
SNDRV_RAWMIDI_INFO_DUPLEX;
|
|
|
|
|
|
|
|
pm->rmidi = rmidi;
|
|
|
|
|
|
|
|
/* register rawmidi ops */
|
|
|
|
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT,
|
|
|
|
&snd_portman_midi_output);
|
|
|
|
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT,
|
|
|
|
&snd_portman_midi_input);
|
|
|
|
|
|
|
|
/* name substreams */
|
|
|
|
/* output */
|
|
|
|
list_for_each_entry(substream,
|
|
|
|
&rmidi->streams[SNDRV_RAWMIDI_STREAM_OUTPUT].substreams,
|
|
|
|
list) {
|
|
|
|
sprintf(substream->name,
|
|
|
|
"Portman2x4 %d", substream->number+1);
|
|
|
|
}
|
|
|
|
/* input */
|
|
|
|
list_for_each_entry(substream,
|
|
|
|
&rmidi->streams[SNDRV_RAWMIDI_STREAM_INPUT].substreams,
|
|
|
|
list) {
|
|
|
|
pm->midi_input[substream->number] = substream;
|
|
|
|
sprintf(substream->name,
|
|
|
|
"Portman2x4 %d", substream->number+1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* parport stuff
|
|
|
|
*********************************************************************/
|
2007-10-19 10:54:26 +04:00
|
|
|
static void snd_portman_interrupt(void *userdata)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
unsigned char midivalue = 0;
|
|
|
|
struct portman *pm = ((struct snd_card*)userdata)->private_data;
|
|
|
|
|
|
|
|
spin_lock(&pm->reg_lock);
|
|
|
|
|
|
|
|
/* While any input data is waiting */
|
|
|
|
while ((portman_read_status(pm) & INT_REQ) == INT_REQ) {
|
|
|
|
/* If data available on channel 0,
|
|
|
|
read it and stuff it into the queue. */
|
|
|
|
if (portman_data_avail(pm, 0)) {
|
|
|
|
/* Read Midi */
|
|
|
|
midivalue = portman_read_midi(pm, 0);
|
|
|
|
/* put midi into queue... */
|
|
|
|
if (pm->mode[0] & PORTMAN2X4_MODE_INPUT_TRIGGERED)
|
|
|
|
snd_rawmidi_receive(pm->midi_input[0],
|
|
|
|
&midivalue, 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
/* If data available on channel 1,
|
|
|
|
read it and stuff it into the queue. */
|
|
|
|
if (portman_data_avail(pm, 1)) {
|
|
|
|
/* Read Midi */
|
|
|
|
midivalue = portman_read_midi(pm, 1);
|
|
|
|
/* put midi into queue... */
|
|
|
|
if (pm->mode[1] & PORTMAN2X4_MODE_INPUT_TRIGGERED)
|
|
|
|
snd_rawmidi_receive(pm->midi_input[1],
|
|
|
|
&midivalue, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
spin_unlock(&pm->reg_lock);
|
|
|
|
}
|
|
|
|
|
2012-12-06 21:35:27 +04:00
|
|
|
static void snd_portman_attach(struct parport *p)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
struct platform_device *device;
|
|
|
|
|
|
|
|
device = platform_device_alloc(PLATFORM_DRIVER, device_count);
|
2007-04-23 13:54:41 +04:00
|
|
|
if (!device)
|
2007-01-25 15:15:05 +03:00
|
|
|
return;
|
|
|
|
|
|
|
|
/* Temporary assignment to forward the parport */
|
|
|
|
platform_set_drvdata(device, p);
|
|
|
|
|
2007-04-23 13:54:41 +04:00
|
|
|
if (platform_device_add(device) < 0) {
|
2007-01-25 15:15:05 +03:00
|
|
|
platform_device_put(device);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Since we dont get the return value of probe
|
|
|
|
* We need to check if device probing succeeded or not */
|
|
|
|
if (!platform_get_drvdata(device)) {
|
|
|
|
platform_device_unregister(device);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* register device in global table */
|
|
|
|
platform_devices[device_count] = device;
|
|
|
|
device_count++;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snd_portman_detach(struct parport *p)
|
|
|
|
{
|
|
|
|
/* nothing to do here */
|
|
|
|
}
|
|
|
|
|
2016-02-17 15:56:27 +03:00
|
|
|
static int snd_portman_dev_probe(struct pardevice *pardev)
|
|
|
|
{
|
|
|
|
if (strcmp(pardev->name, DRIVER_NAME))
|
|
|
|
return -ENODEV;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2007-01-25 15:15:05 +03:00
|
|
|
static struct parport_driver portman_parport_driver = {
|
2016-02-17 15:56:27 +03:00
|
|
|
.name = "portman2x4",
|
|
|
|
.probe = snd_portman_dev_probe,
|
|
|
|
.match_port = snd_portman_attach,
|
|
|
|
.detach = snd_portman_detach,
|
|
|
|
.devmodel = true,
|
2007-01-25 15:15:05 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* platform stuff
|
|
|
|
*********************************************************************/
|
|
|
|
static void snd_portman_card_private_free(struct snd_card *card)
|
|
|
|
{
|
|
|
|
struct portman *pm = card->private_data;
|
|
|
|
struct pardevice *pardev = pm->pardev;
|
|
|
|
|
|
|
|
if (pardev) {
|
2016-02-17 15:56:27 +03:00
|
|
|
parport_release(pardev);
|
2007-01-25 15:15:05 +03:00
|
|
|
parport_unregister_device(pardev);
|
|
|
|
}
|
|
|
|
|
|
|
|
portman_free(pm);
|
|
|
|
}
|
|
|
|
|
2012-12-06 21:35:27 +04:00
|
|
|
static int snd_portman_probe(struct platform_device *pdev)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
struct pardevice *pardev;
|
|
|
|
struct parport *p;
|
|
|
|
int dev = pdev->id;
|
|
|
|
struct snd_card *card = NULL;
|
|
|
|
struct portman *pm = NULL;
|
|
|
|
int err;
|
2016-02-17 15:56:27 +03:00
|
|
|
struct pardev_cb portman_cb = {
|
|
|
|
.preempt = NULL,
|
|
|
|
.wakeup = NULL,
|
|
|
|
.irq_func = snd_portman_interrupt, /* ISR */
|
|
|
|
.flags = PARPORT_DEV_EXCL, /* flags */
|
|
|
|
};
|
2007-01-25 15:15:05 +03:00
|
|
|
|
|
|
|
p = platform_get_drvdata(pdev);
|
|
|
|
platform_set_drvdata(pdev, NULL);
|
|
|
|
|
|
|
|
if (dev >= SNDRV_CARDS)
|
|
|
|
return -ENODEV;
|
|
|
|
if (!enable[dev])
|
|
|
|
return -ENOENT;
|
|
|
|
|
2014-01-29 15:59:08 +04:00
|
|
|
err = snd_card_new(&pdev->dev, index[dev], id[dev], THIS_MODULE,
|
|
|
|
0, &card);
|
2008-12-28 18:45:02 +03:00
|
|
|
if (err < 0) {
|
2007-01-25 15:15:05 +03:00
|
|
|
snd_printd("Cannot create card\n");
|
2008-12-28 18:45:02 +03:00
|
|
|
return err;
|
2007-01-25 15:15:05 +03:00
|
|
|
}
|
|
|
|
strcpy(card->driver, DRIVER_NAME);
|
|
|
|
strcpy(card->shortname, CARD_NAME);
|
|
|
|
sprintf(card->longname, "%s at 0x%lx, irq %i",
|
|
|
|
card->shortname, p->base, p->irq);
|
|
|
|
|
2016-02-17 15:56:27 +03:00
|
|
|
portman_cb.private = card; /* private */
|
|
|
|
pardev = parport_register_dev_model(p, /* port */
|
|
|
|
DRIVER_NAME, /* name */
|
|
|
|
&portman_cb, /* callbacks */
|
|
|
|
pdev->id); /* device number */
|
2007-01-25 15:15:05 +03:00
|
|
|
if (pardev == NULL) {
|
|
|
|
snd_printd("Cannot register pardevice\n");
|
|
|
|
err = -EIO;
|
|
|
|
goto __err;
|
|
|
|
}
|
|
|
|
|
2016-02-17 15:56:27 +03:00
|
|
|
/* claim parport */
|
|
|
|
if (parport_claim(pardev)) {
|
|
|
|
snd_printd("Cannot claim parport 0x%lx\n", pardev->port->base);
|
|
|
|
err = -EIO;
|
|
|
|
goto free_pardev;
|
|
|
|
}
|
|
|
|
|
2021-06-08 17:05:36 +03:00
|
|
|
err = portman_create(card, pardev, &pm);
|
|
|
|
if (err < 0) {
|
2007-01-25 15:15:05 +03:00
|
|
|
snd_printd("Cannot create main component\n");
|
2016-02-17 15:56:27 +03:00
|
|
|
goto release_pardev;
|
2007-01-25 15:15:05 +03:00
|
|
|
}
|
|
|
|
card->private_data = pm;
|
|
|
|
card->private_free = snd_portman_card_private_free;
|
2016-03-02 14:40:30 +03:00
|
|
|
|
|
|
|
err = portman_probe(p);
|
|
|
|
if (err) {
|
|
|
|
err = -EIO;
|
|
|
|
goto __err;
|
|
|
|
}
|
2007-01-25 15:15:05 +03:00
|
|
|
|
2021-06-08 17:05:36 +03:00
|
|
|
err = snd_portman_rawmidi_create(card);
|
|
|
|
if (err < 0) {
|
2007-01-25 15:15:05 +03:00
|
|
|
snd_printd("Creating Rawmidi component failed\n");
|
|
|
|
goto __err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* init device */
|
2021-06-08 17:05:36 +03:00
|
|
|
err = portman_device_init(pm);
|
|
|
|
if (err < 0)
|
2007-01-25 15:15:05 +03:00
|
|
|
goto __err;
|
|
|
|
|
|
|
|
platform_set_drvdata(pdev, card);
|
|
|
|
|
|
|
|
/* At this point card will be usable */
|
2021-06-08 17:05:36 +03:00
|
|
|
err = snd_card_register(card);
|
|
|
|
if (err < 0) {
|
2007-01-25 15:15:05 +03:00
|
|
|
snd_printd("Cannot register card\n");
|
|
|
|
goto __err;
|
|
|
|
}
|
|
|
|
|
|
|
|
snd_printk(KERN_INFO "Portman 2x4 on 0x%lx\n", p->base);
|
|
|
|
return 0;
|
|
|
|
|
2016-02-17 15:56:27 +03:00
|
|
|
release_pardev:
|
|
|
|
parport_release(pardev);
|
|
|
|
free_pardev:
|
|
|
|
parport_unregister_device(pardev);
|
2007-01-25 15:15:05 +03:00
|
|
|
__err:
|
|
|
|
snd_card_free(card);
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
2012-12-06 21:35:27 +04:00
|
|
|
static int snd_portman_remove(struct platform_device *pdev)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
struct snd_card *card = platform_get_drvdata(pdev);
|
|
|
|
|
|
|
|
if (card)
|
|
|
|
snd_card_free(card);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static struct platform_driver snd_portman_driver = {
|
|
|
|
.probe = snd_portman_probe,
|
2012-12-06 21:35:27 +04:00
|
|
|
.remove = snd_portman_remove,
|
2007-01-25 15:15:05 +03:00
|
|
|
.driver = {
|
2012-07-02 12:50:24 +04:00
|
|
|
.name = PLATFORM_DRIVER,
|
2007-01-25 15:15:05 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/*********************************************************************
|
|
|
|
* module init stuff
|
|
|
|
*********************************************************************/
|
2007-06-27 13:52:39 +04:00
|
|
|
static void snd_portman_unregister_all(void)
|
2007-01-25 15:15:05 +03:00
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for (i = 0; i < SNDRV_CARDS; ++i) {
|
|
|
|
if (platform_devices[i]) {
|
|
|
|
platform_device_unregister(platform_devices[i]);
|
|
|
|
platform_devices[i] = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
platform_driver_unregister(&snd_portman_driver);
|
|
|
|
parport_unregister_driver(&portman_parport_driver);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int __init snd_portman_module_init(void)
|
|
|
|
{
|
|
|
|
int err;
|
|
|
|
|
2021-06-08 17:05:36 +03:00
|
|
|
err = platform_driver_register(&snd_portman_driver);
|
|
|
|
if (err < 0)
|
2007-01-25 15:15:05 +03:00
|
|
|
return err;
|
|
|
|
|
|
|
|
if (parport_register_driver(&portman_parport_driver) != 0) {
|
|
|
|
platform_driver_unregister(&snd_portman_driver);
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (device_count == 0) {
|
|
|
|
snd_portman_unregister_all();
|
|
|
|
return -ENODEV;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void __exit snd_portman_module_exit(void)
|
|
|
|
{
|
|
|
|
snd_portman_unregister_all();
|
|
|
|
}
|
|
|
|
|
|
|
|
module_init(snd_portman_module_init);
|
|
|
|
module_exit(snd_portman_module_exit);
|