/***************************************************************************** * aft_core_prot.c * * WANPIPE(tm) AFT CORE Hardware Support - Protocol/API * * Authors: Nenad Corbic * * Copyright: (c) 2003-2008 Sangoma Technologies 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. * ============================================================================*/ #include "wanpipe_includes.h" #include "wanpipe_defines.h" #include "wanpipe.h" #if defined (__LINUX__) # include "wanproc.h" # include "if_wanpipe.h" #endif #include "wanpipe_abstr.h" #include "if_wanpipe_common.h" /* Socket Driver common area */ #include "sdlapci.h" #include "aft_core.h" #include "wanpipe_iface.h" #include "wanpipe_tdm_api.h" #include "sdla_tdmv_dummy.h" #if defined(AFT_XMTP2_API_SUPPORT) /* This call back is not used by xmtp2. It is here for complete sake * It might be used in the future */ int wp_xmtp2_callback (void *prot_ptr, unsigned char *data, int len) { private_area_t *chan = (private_area_t*)prot_ptr; #if 0 void * tx_skb; unsigned char *buff; int err; #endif if (!chan || !data || len <= 0){ if (WAN_NET_RATELIMIT()) { DEBUG_EVENT("%s:%d: Assert prot=%p data=%p len=%d\n", __FUNCTION__,__LINE__,chan,data,len); } return -1; } DEBUG_EVENT("%s:%d: TX CALL BACK CALLED prot=%p data=%p len=%d\n", __FUNCTION__,__LINE__,chan,data,len); return -1; } #endif #if defined(AFT_RTP_SUPPORT) enum { WAN_TDM_RTP_NO_CHANGE, WAN_TDM_RTP_CALL_START, WAN_TDM_RTP_CALL_STOP }; void aft_rtp_unconfig(sdla_t *card) { wan_rtp_chan_t *rtp_chan; int i; netskb_t *skb; card->wandev.rtp_len=0; card->rtp_conf.rtp_ip=0; card->rtp_conf.rtp_sample=0; if (card->wandev.rtp_dev) { dev_put(card->wandev.rtp_dev); card->wandev.rtp_dev=NULL; } for (i=0;i<32;i++) { rtp_chan=&card->wandev.rtp_chan[i]; if (rtp_chan->rx_skb) { wan_skb_free(rtp_chan->rx_skb); rtp_chan->rx_skb=NULL; } if (rtp_chan->tx_skb) { wan_skb_free(rtp_chan->tx_skb); rtp_chan->rx_skb=NULL; } } while ((skb=wan_skb_dequeue(&card->u.aft.rtp_tap_list))) { wan_skb_free(skb); } } int aft_rtp_config(sdla_t *card) { netdevice_t *dev; card->wandev.rtp_tap=NULL; if (!card->rtp_conf.rtp_ip || !card->rtp_conf.rtp_sample) { return 1; } if (card->rtp_conf.rtp_sample < 10 || card->rtp_conf.rtp_sample > 150) { DEBUG_ERROR("%s: Error: Invalid RTP Sample %d [Min=10 Max=150ms]\n", card->devname,card->rtp_conf.rtp_sample); goto aft_rtp_init_exit; } DEBUG_EVENT("%s: RTP TAP [ %d.%d.%d.%d:%d %dms %s %02X:%02X:%02X:%02X:%02X:%02X ]\n", card->devname, NIPQUAD(card->rtp_conf.rtp_ip), card->rtp_conf.rtp_port, card->rtp_conf.rtp_sample, card->rtp_conf.rtp_devname, card->rtp_conf.rtp_mac[0], card->rtp_conf.rtp_mac[1], card->rtp_conf.rtp_mac[2], card->rtp_conf.rtp_mac[3], card->rtp_conf.rtp_mac[4], card->rtp_conf.rtp_mac[5]); card->wandev.rtp_len = (card->rtp_conf.rtp_sample * 8) + sizeof(wan_rtp_pkt_t); if ((card->wandev.rtp_dev=wan_dev_get_by_name(card->rtp_conf.rtp_devname)) == NULL){ DEBUG_EVENT("%s: Failed to open rtp tx device %s\n", card->devname, card->rtp_conf.rtp_devname); goto aft_rtp_init_exit; } dev=(netdevice_t*)card->wandev.rtp_dev; memcpy(card->rtp_conf.rtp_local_mac,dev->dev_addr,dev->addr_len); card->rtp_conf.rtp_local_ip=wan_get_ip_address(card->wandev.rtp_dev,WAN_LOCAL_IP); if (card->rtp_conf.rtp_local_ip == 0) { goto aft_rtp_init_exit; } card->wandev.rtp_tap=&aft_rtp_tap; memset(card->wandev.rtp_chan,0,sizeof(card->wandev.rtp_chan)); return 0; aft_rtp_init_exit: aft_rtp_unconfig(card); DEBUG_EVENT("%s: Failed to configure rtp tap!\n",card->devname); return -1; } __inline void aft_rtp_tap_chan(sdla_t *card, u8 *data, u32 len, netskb_t **skb_q, u32 *timestamp, u8 call_status, u32 chan) { wan_rtp_pkt_t *pkt; u8 *buf; netskb_t *skb; u32 ts; if ((skb=*skb_q) == NULL) { *skb_q=wan_skb_alloc(card->wandev.rtp_len+128); if (!*skb_q) { return; } skb=*skb_q; pkt = (wan_rtp_pkt_t*)wan_skb_put(skb,sizeof(wan_rtp_pkt_t)); memset(pkt,0,sizeof(wan_rtp_pkt_t)); pkt->rtp_hdr.version=2; if (IS_T1_CARD(card)) { pkt->rtp_hdr.pt=0; } else { pkt->rtp_hdr.pt=1; } DEBUG_TEST("%s: RTP(%d) SKB Allocated Len=%d \n", card->devname,chan,card->wandev.rtp_len); } pkt = (wan_rtp_pkt_t*)wan_skb_data(skb); if (call_status == WAN_TDM_RTP_CALL_START) { pkt->rtp_hdr.seq=0; pkt->rtp_hdr.ts=0; *timestamp=0; } buf=wan_skb_put(skb,len); memcpy(buf,data,len); ts=htonl(pkt->rtp_hdr.ts); ts+=len; pkt->rtp_hdr.ts = htonl(ts); if (wan_skb_len(skb) >= card->wandev.rtp_len || call_status==WAN_TDM_RTP_CALL_STOP) { netskb_t *nskb; u16 seq; pkt->rtp_hdr.ts = *timestamp; wan_ip_udp_setup(card, &card->rtp_conf, chan, wan_skb_data(skb), wan_skb_len(skb)-sizeof(wan_rtp_pkt_t)); nskb=wan_skb_clone(skb); if (nskb) { nskb->next = nskb->prev = NULL; nskb->dev = card->wandev.rtp_dev; nskb->protocol = htons(ETH_P_802_2); wan_skb_reset_mac_header(nskb); wan_skb_reset_network_header(nskb); wan_skb_queue_tail(&card->u.aft.rtp_tap_list,nskb); if (!wan_test_bit(CARD_PORT_TASK_DOWN,&card->wandev.critical)){ wan_set_bit(AFT_RTP_TAP_Q,&card->u.aft.port_task_cmd); WAN_TASKQ_SCHEDULE((&card->u.aft.port_task)); } DEBUG_TEST("%s: RTP(%d) SKB Tx on dev %s \n", card->devname,chan,nskb->dev->name); } wan_skb_trim(skb,sizeof(wan_rtp_pkt_t)); pkt = (wan_rtp_pkt_t*)wan_skb_data(skb); seq=htons(pkt->rtp_hdr.seq); seq++; pkt->rtp_hdr.seq=htons(seq); *timestamp = htonl(ts); DEBUG_TEST("Chan=%d Seq=%d TS=%d\n",chan,seq,ts); pkt->rtp_hdr.ts = htonl(ts); } } void aft_rtp_tap(void *card_ptr, u8 chan, u8* rx, u8* tx, u32 len) { sdla_t *card = (sdla_t *)card_ptr; u8 call_status=WAN_TDM_RTP_NO_CHANGE; wan_rtp_chan_t *rtp_chan; u32 span; if (!card || !rx || !tx ) { if (WAN_NET_RATELIMIT()) { DEBUG_ERROR("%s: Internal Error: rtp tap invalid pointers chan %d\n", __FUNCTION__,chan); } return; } if (!card->rtp_conf.rtp_ip || !card->rtp_conf.rtp_sample || !card->wandev.rtp_len || !card->wandev.rtp_dev) { if (WAN_NET_RATELIMIT()) { DEBUG_EVENT("%s: RTP Tap Not configured %d\n", card->devname,chan); } return; } if (chan >= 32) { if (WAN_NET_RATELIMIT()) { DEBUG_ERROR("%s: Internal Error: rtp tap chan out of range %d\n", card->devname,chan); } return; } span = card->tdmv_conf.span_no-1; rtp_chan = &card->wandev.rtp_chan[chan]; if (wan_test_bit(chan,&card->wandev.rtp_tap_call_map)) { if (!wan_test_and_set_bit(chan,&card->wandev.rtp_tap_call_status)) { /* Start of the call */ call_status=WAN_TDM_RTP_CALL_START; DEBUG_TEST("%s: CALL Start on ch %d\n", card->devname,chan); } } else { if (!wan_test_bit(chan,&card->wandev.rtp_tap_call_status)) { /* Call not up */ return; } wan_clear_bit(chan,&card->wandev.rtp_tap_call_status); call_status=WAN_TDM_RTP_CALL_STOP; DEBUG_TEST("%s: CALL Stop on ch %d\n",card->devname,chan); } aft_rtp_tap_chan(card, rx, len, &rtp_chan->rx_skb, &rtp_chan->rx_ts, call_status, (span<<4|(chan+1))); aft_rtp_tap_chan(card, tx, len, &rtp_chan->tx_skb, &rtp_chan->tx_ts, call_status, (span<<4|(chan+1))+2000); } #endif /**SECTION************************************************************* * * Protocol API Support Functions * **********************************************************************/ int protocol_init (sdla_t *card, netdevice_t *dev, private_area_t *chan, wanif_conf_t* conf) { chan->common.protocol = conf->protocol; DEBUG_TEST("%s: Protocol init 0x%X PPP=0x0%x FR=0x0%X\n", wan_netif_name(dev), chan->common.protocol, WANCONFIG_PPP, WANCONFIG_FR); #ifndef CONFIG_PRODUCT_WANPIPE_GENERIC DEBUG_EVENT("%s: AFT Driver doesn't directly support any protocols!\n", chan->if_name); return -1; #else if (chan->common.protocol == WANCONFIG_PPP || chan->common.protocol == WANCONFIG_CHDLC){ struct ifreq ifr; struct if_settings ifsettings; wanpipe_generic_register(card, dev, wan_netif_name(dev)); chan->common.prot_ptr = dev; if (chan->common.protocol == WANCONFIG_CHDLC){ DEBUG_EVENT("%s: Starting Kernel CISCO HDLC protocol\n", chan->if_name); ifsettings.type = IF_PROTO_CISCO; }else{ DEBUG_EVENT("%s: Starting Kernel Sync PPP protocol\n", chan->if_name); ifsettings.type = IF_PROTO_PPP; } ifr.ifr_data = (caddr_t)&ifsettings; if (wp_lite_set_proto(dev, &ifr)){ wanpipe_generic_unregister(dev); return -EINVAL; } }else if (chan->common.protocol == WANCONFIG_GENERIC){ chan->common.prot_ptr = dev; }else{ DEBUG_EVENT("%s:%s: Unsupported protocol %d\n", card->devname,chan->if_name,chan->common.protocol); return -EPROTONOSUPPORT; } #endif return 0; } int protocol_stop (sdla_t *card, netdevice_t *dev) { private_area_t *chan=wan_netif_priv(dev); int err = 0; if (!chan) return 0; return err; } int protocol_shutdown (sdla_t *card, netdevice_t *dev) { private_area_t *chan=wan_netif_priv(dev); if (!chan) return 0; #ifndef CONFIG_PRODUCT_WANPIPE_GENERIC return 0; #else if (chan->common.protocol == WANCONFIG_PPP || chan->common.protocol == WANCONFIG_CHDLC){ chan->common.prot_ptr = NULL; wanpipe_generic_unregister(dev); }else if (chan->common.protocol == WANCONFIG_GENERIC){ DEBUG_EVENT("%s:%s Protocol shutdown... \n", card->devname, chan->if_name); } #endif return 0; } void protocol_recv(sdla_t *card, private_area_t *chan, netskb_t *skb) { #ifdef CONFIG_PRODUCT_WANPIPE_GENERIC if (chan->common.protocol == WANCONFIG_PPP || chan->common.protocol == WANCONFIG_CHDLC){ wanpipe_generic_input(chan->common.dev, skb); return 0; } #if defined(__LINUX__) if (chan->common.protocol == WANCONFIG_GENERIC){ skb->protocol = htons(ETH_P_HDLC); skb->dev = chan->common.dev; wan_skb_reset_mac_header(skb); netif_rx(skb); return 0; } #endif #endif #if defined(__LINUX__) skb->protocol = htons(ETH_P_IP); skb->dev = chan->common.dev; wan_skb_reset_mac_header(skb); netif_rx(skb); #elif defined(__FreeBSD__) wan_skb_set_csum(skb,0); if (wan_iface.input && wan_iface.input(chan->common.dev, skb) != 0){ WAN_NETIF_STATS_INC_RX_DROPPED(&chan->common); //++chan->if_stats.rx_dropped; wan_skb_free(skb); return; } #else DEBUG_EVENT("%s: Action not supported (IP)!\n", card->devname); wan_skb_free(skb); #endif return; } #ifdef CONFIG_PRODUCT_WANPIPE_ANNEXG int bind_annexg(netdevice_t *dev, netdevice_t *annexg_dev) { wan_smp_flag_t smp_flags=0; private_area_t* chan = wan_netif_priv(dev); sdla_t *card = chan->card; if (!chan) return -EINVAL; if (chan->common.usedby != ANNEXG) return -EPROTONOSUPPORT; if (chan->annexg_dev) return -EBUSY; wan_spin_lock_irq(&card->wandev.lock,&smp_flags); chan->annexg_dev = annexg_dev; wan_spin_unlock_irq(&card->wandev.lock,&smp_flags); return 0; } netdevice_t * un_bind_annexg(wan_device_t *wandev, netdevice_t *annexg_dev) { struct wan_dev_le *devle; netdevice_t *dev; wan_smp_flag_t smp_flags=0; sdla_t *card = wandev->priv; WAN_LIST_FOREACH(devle, &card->wandev.dev_head, dev_link){ private_area_t* chan; dev = WAN_DEVLE2DEV(devle); if (dev == NULL || (chan = wan_netif_priv(dev)) == NULL) continue; if (!chan->annexg_dev || chan->common.usedby != ANNEXG) continue; if (chan->annexg_dev == annexg_dev){ wan_spin_lock_irq(&card->wandev.lock,&smp_flags); chan->annexg_dev = NULL; wan_spin_unlock_irq(&card->wandev.lock,&smp_flags); return dev; } } return NULL; } void get_active_inactive(wan_device_t *wandev, netdevice_t *dev, void *wp_stats_ptr) { private_area_t* chan = wan_netif_priv(dev); wp_stack_stats_t *wp_stats = (wp_stack_stats_t *)wp_stats_ptr; if (chan->common.usedby == ANNEXG && chan->annexg_dev){ if (IS_FUNC_CALL(lapb_protocol,lapb_get_active_inactive)){ lapb_protocol.lapb_get_active_inactive(chan->annexg_dev,wp_stats); } } if (chan->common.state == WAN_CONNECTED){ wp_stats->fr_active++; }else{ wp_stats->fr_inactive++; } } int get_map(wan_device_t *wandev, netdevice_t *dev, struct seq_file* m, int* stop_cnt) { private_area_t* chan = wan_netif_priv(dev); if (!(dev->flags&IFF_UP)){ return m->count; } if (chan->common.usedby == ANNEXG && chan->annexg_dev){ if (IS_FUNC_CALL(lapb_protocol,lapb_get_map)){ return lapb_protocol.lapb_get_map(chan->annexg_dev, m); } } PROC_ADD_LINE(m, "%15s:%s:%c:%s:%c\n", chan->label, wandev->name,(wandev->state == WAN_CONNECTED) ? '*' : ' ', dev->name,(chan->common.state == WAN_CONNECTED) ? '*' : ' '); return m->count; } #endif #ifdef AFT_TDM_API_SUPPORT int aft_tdm_api_init(sdla_t *card, private_area_t *chan, wanif_conf_t *conf) { int err=0; int chan_no=0; wanpipe_tdm_api_span_t *span; if (chan->common.usedby != TDM_VOICE_API && chan->common.usedby != TDM_VOICE_DCHAN && !chan->wp_api_op_mode) { return 0; } if (chan->tdmv_zaptel_cfg) { return 0; } if (!card->tdm_api_span) { card->tdm_api_span=wan_malloc(sizeof(wanpipe_tdm_api_span_t)); if (!card->tdm_api_span) { return -ENOMEM; } memset(card->tdm_api_span,0,sizeof(wanpipe_tdm_api_span_t)); } span = (wanpipe_tdm_api_span_t*)card->tdm_api_span; if (IS_TE1_CARD(card)) { if (IS_T1_CARD(card)){ chan_no=(u8)chan->first_time_slot+1; } else { chan_no=(u8)chan->first_time_slot; } } else if (IS_BRI_CARD(card)) { if (chan->dchan_time_slot >= 0) { chan_no = 3; } else { chan_no = (u8)(chan->first_time_slot % 2)+1; } } else { chan_no=(u8)chan->first_time_slot+1; } if (conf->hdlc_streaming) { wan_clear_bit(chan_no, &span->timeslot_map); } else { wan_set_bit(chan_no, &span->timeslot_map); } DEBUG_TEST("%s: Setting Timeslot Map chan =%i map=0x%X\n",chan->if_name,chan_no,span->timeslot_map); chan->wp_tdm_api_dev = &span->chans[chan_no]; /* Initilaize TDM API Parameters */ chan->wp_tdm_api_dev->chan = chan; chan->wp_tdm_api_dev->card = card; strncpy(chan->wp_tdm_api_dev->name,chan->if_name,WAN_IFNAME_SZ); if (conf->hdlc_streaming) { chan->wp_tdm_api_dev->hdlc_framing=1; } aft_core_tdmapi_event_init(chan); chan->wp_tdm_api_dev->cfg.rx_disable = 0; chan->wp_tdm_api_dev->cfg.tx_disable = 0; chan->wp_tdm_api_dev->cfg.tx_queue_sz = MAX_AFT_DMA_CHAINS; chan->wp_tdm_api_dev->tdm_chan = chan_no; if (IS_TE1_CARD(card)) { if (IS_T1_CARD(card)){ chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_MULAW; }else{ chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_ALAW; } if (IS_E1_CARD(card)){ chan->wp_tdm_api_dev->active_ch = conf->active_ch; }else{ chan->wp_tdm_api_dev->active_ch = conf->active_ch << 1; } } else if (IS_BRI_CARD(card)) { if (chan->dchan_time_slot >= 0) { wan_set_bit(chan->wp_tdm_api_dev->tdm_chan,&chan->wp_tdm_api_dev->active_ch); } else { wan_set_bit(chan->wp_tdm_api_dev->tdm_chan,&chan->wp_tdm_api_dev->active_ch); if (chan->num_of_time_slots == 2) { wan_set_bit(chan->wp_tdm_api_dev->tdm_chan+1,&chan->wp_tdm_api_dev->active_ch); } } if (card->fe.fe_cfg.tdmv_law == WAN_TDMV_MULAW){ chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_MULAW; } else { chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_ALAW; } } else { if (card->fe.fe_cfg.tdmv_law == WAN_TDMV_MULAW){ chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_MULAW; } else { chan->wp_tdm_api_dev->cfg.hw_tdm_coding=WP_ALAW; } chan->wp_tdm_api_dev->active_ch = conf->active_ch << 1; } DEBUG_TDMAPI("%s: TDM API ACTIVE CH 0x%08X CHAN=%d\n", chan->if_name, chan->wp_tdm_api_dev->active_ch,chan->wp_tdm_api_dev->tdm_chan); chan->wp_tdm_api_dev->cfg.idle_flag = conf->u.aft.idle_flag; chan->wp_tdm_api_dev->cfg.rbs_tx_bits = conf->u.aft.rbs_cas_idle; chan->wp_tdm_api_dev->tdm_span = card->tdmv_conf.span_no; DEBUG_TDMAPI("%s: TDM API ACTIVE CH 0x%08X SPAN=%d CHAN=%d\n", chan->if_name, chan->wp_tdm_api_dev->active_ch, chan->wp_tdm_api_dev->tdm_span,chan->wp_tdm_api_dev->tdm_chan); chan->wp_tdm_api_dev->dtmfsupport = card->u.aft.tdmv_hw_tone; DEBUG_TDMAPI("%s: SPAN=%d, CHAN=%d Chunk=%d Period=%d Mtu=%d\n", chan->if_name, chan->wp_tdm_api_dev->tdm_span, chan->wp_tdm_api_dev->tdm_chan, chan->tdm_api_chunk, chan->tdm_api_period, chan->mtu); DEBUG_TDMAPI("%s: conf->mtu=%d\n", chan->if_name, conf->mtu); DEBUG_TDMAPI("%s: tdm_api_chunk=%d, tdm_api_period=%d\n", chan->if_name, chan->tdm_api_chunk, chan->tdm_api_period); chan->wp_tdm_api_dev->operation_mode=chan->wp_api_op_mode; if (chan->wp_api_op_mode == WP_TDM_OPMODE_CHAN) { chan->wp_tdm_api_dev->operation_mode = WP_TDM_OPMODE_CHAN; chan->wp_tdm_api_dev->cfg.hw_mtu_mru = 8; chan->wp_tdm_api_dev->cfg.usr_period = chan->tdm_api_period; chan->wp_tdm_api_dev->cfg.usr_mtu_mru = chan->tdm_api_chunk; if (chan->wp_tdm_api_dev->cfg.usr_mtu_mru < 160) { chan->tdm_api_period=20; chan->wp_tdm_api_dev->cfg.usr_period=20; chan->tdm_api_chunk=160; chan->wp_tdm_api_dev->cfg.usr_mtu_mru=160; } } else { chan->wp_tdm_api_dev->operation_mode = WP_TDM_OPMODE_SPAN; chan->wp_tdm_api_dev->cfg.hw_mtu_mru = chan->tdm_api_chunk; chan->wp_tdm_api_dev->cfg.usr_period = chan->tdm_api_period; chan->wp_tdm_api_dev->cfg.usr_mtu_mru = chan->mtu; /* Overwite the chan number based on group number */ chan->wp_tdm_api_dev->tdm_chan = (u8)chan->if_cnt; } if (chan->usedby_cfg == MTP1_API) { chan->wp_tdm_api_dev->operation_mode = WP_TDM_OPMODE_MTP1; } /* Set the API interface mode. Used on windows to * indicate legacy API interface */ chan->wp_tdm_api_dev->api_mode = chan->wp_api_iface_mode; if (chan->if_cnt == 1) { DEBUG_EVENT("%s: Memory: TDM API %d \n", card->devname, sizeof(wanpipe_tdm_api_dev_t)); } DEBUG_TDMAPI("%s: Chunk=%i, Period=%i, MTU=%i\n", card->devname,chan->tdm_api_chunk, chan->tdm_api_period,chan->wp_tdm_api_dev->cfg.usr_mtu_mru); err=wanpipe_tdm_api_reg(chan->wp_tdm_api_dev); if (err){ if (span->usage == 0) { wan_free(span); card->tdm_api_span=NULL; } return err; } span->usage++; wan_set_bit(0,&chan->wp_tdm_api_dev->init); return err; } #endif int aft_tdm_api_free(sdla_t *card, private_area_t *chan) { #ifdef AFT_TDM_API_SUPPORT int err=0; wanpipe_tdm_api_span_t *span; span = (wanpipe_tdm_api_span_t*)card->tdm_api_span; if (!span) { return 0; } if (chan->wp_tdm_api_dev && wan_test_bit(0,&chan->wp_tdm_api_dev->init)){ wan_clear_bit(0,&chan->wp_tdm_api_dev->init); wan_clear_bit(chan->wp_tdm_api_dev->tdm_chan, &span->timeslot_map); err=wanpipe_tdm_api_unreg(chan->wp_tdm_api_dev); if (err){ wan_set_bit(0,&chan->wp_tdm_api_dev->init); return err; } chan->wp_tdm_api_dev=NULL; span->usage--; if (span->usage < 0) { span->usage = 0; } if (span->usage == 0) { wan_free(span); card->tdm_api_span=NULL; } } #endif return 0; }