fm_link.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. #include <linux/slab.h>
  2. #include <linux/interrupt.h>
  3. #include "fm_typedef.h"
  4. #include "fm_dbg.h"
  5. #include "fm_err.h"
  6. #include "fm_stdlib.h"
  7. #include "fm_link.h"
  8. #if (defined(MT6620_FM) || defined(MT6628_FM) || defined(MT6627_FM) || defined(MT6580_FM) || defined(MT6630_FM))
  9. #include "osal_typedef.h"
  10. #include "stp_exp.h"
  11. #include "wmt_exp.h"
  12. static struct fm_link_event *link_event;
  13. static struct fm_trace_fifo_t *cmd_fifo;
  14. static struct fm_trace_fifo_t *evt_fifo;
  15. static fm_s32 (*reset)(fm_s32 sta);
  16. static void WCNfm_wholechip_rst_cb(ENUM_WMTDRV_TYPE_T src,
  17. ENUM_WMTDRV_TYPE_T dst, ENUM_WMTMSG_TYPE_T type, void *buf, unsigned int sz)
  18. {
  19. /* To handle reset procedure please */
  20. ENUM_WMTRSTMSG_TYPE_T rst_msg;
  21. if (sz <= sizeof(ENUM_WMTRSTMSG_TYPE_T)) {
  22. memcpy((char *)&rst_msg, (char *)buf, sz);
  23. WCN_DBG(FM_WAR | LINK,
  24. "[src=%d], [dst=%d], [type=%d], [buf=0x%x], [sz=%d], [max=%d]\n", src, dst,
  25. type, rst_msg, sz, WMTRSTMSG_RESET_MAX);
  26. if ((src == WMTDRV_TYPE_WMT) && (dst == WMTDRV_TYPE_FM)
  27. && (type == WMTMSG_TYPE_RESET)) {
  28. if (rst_msg == WMTRSTMSG_RESET_START) {
  29. WCN_DBG(FM_WAR | LINK, "FM restart start!\n");
  30. if (reset)
  31. reset(1);
  32. } else if (rst_msg == WMTRSTMSG_RESET_END) {
  33. WCN_DBG(FM_WAR | LINK, "FM restart end!\n");
  34. if (reset)
  35. reset(0);
  36. }
  37. }
  38. } else {
  39. /*message format invalid */
  40. WCN_DBG(FM_WAR | LINK, "message format invalid!\n");
  41. }
  42. }
  43. fm_s32 fm_link_setup(void *data)
  44. {
  45. fm_s32 ret = 0;
  46. link_event = fm_zalloc(sizeof(struct fm_link_event));
  47. if (!link_event) {
  48. WCN_DBG(FM_ALT | LINK, "fm_zalloc(fm_link_event) -ENOMEM\n");
  49. return -1;
  50. }
  51. link_event->ln_event = fm_flag_event_create("ln_evt");
  52. if (!link_event->ln_event) {
  53. WCN_DBG(FM_ALT | LINK, "create mt6620_ln_event failed\n");
  54. fm_free(link_event);
  55. return -1;
  56. }
  57. fm_flag_event_get(link_event->ln_event);
  58. WCN_DBG(FM_NTC | LINK, "fm link setup\n");
  59. cmd_fifo = fm_trace_fifo_create("cmd_fifo");
  60. if (!cmd_fifo) {
  61. WCN_DBG(FM_ALT | LINK, "create cmd_fifo failed\n");
  62. ret = -1;
  63. goto failed;
  64. }
  65. evt_fifo = fm_trace_fifo_create("evt_fifo");
  66. if (!evt_fifo) {
  67. WCN_DBG(FM_ALT | LINK, "create evt_fifo failed\n");
  68. ret = -1;
  69. goto failed;
  70. }
  71. reset = data; /* get whole chip reset cb */
  72. mtk_wcn_wmt_msgcb_reg(WMTDRV_TYPE_FM, WCNfm_wholechip_rst_cb);
  73. return 0;
  74. failed:
  75. fm_trace_fifo_release(evt_fifo);
  76. fm_trace_fifo_release(cmd_fifo);
  77. fm_flag_event_put(link_event->ln_event);
  78. if (link_event)
  79. fm_free(link_event);
  80. return ret;
  81. }
  82. fm_s32 fm_link_release(void)
  83. {
  84. fm_trace_fifo_release(evt_fifo);
  85. fm_trace_fifo_release(cmd_fifo);
  86. fm_flag_event_put(link_event->ln_event);
  87. if (link_event)
  88. fm_free(link_event);
  89. WCN_DBG(FM_NTC | LINK, "fm link release\n");
  90. return 0;
  91. }
  92. /*
  93. * fm_ctrl_rx
  94. * the low level func to read a rigister
  95. * @addr - rigister address
  96. * @val - the pointer of target buf
  97. * If success, return 0; else error code
  98. */
  99. fm_s32 fm_ctrl_rx(fm_u8 addr, fm_u16 *val)
  100. {
  101. return 0;
  102. }
  103. /*
  104. * fm_ctrl_tx
  105. * the low level func to write a rigister
  106. * @addr - rigister address
  107. * @val - value will be writed in the rigister
  108. * If success, return 0; else error code
  109. */
  110. fm_s32 fm_ctrl_tx(fm_u8 addr, fm_u16 val)
  111. {
  112. return 0;
  113. }
  114. /*
  115. * fm_cmd_tx() - send cmd to FM firmware and wait event
  116. * @buf - send buffer
  117. * @len - the length of cmd
  118. * @mask - the event flag mask
  119. * @ cnt - the retry conter
  120. * @timeout - timeout per cmd
  121. * Return 0, if success; error code, if failed
  122. */
  123. fm_s32 fm_cmd_tx(fm_u8 *buf, fm_u16 len, fm_s32 mask, fm_s32 cnt, fm_s32 timeout,
  124. fm_s32 (*callback)(struct fm_res_ctx *result))
  125. {
  126. fm_s32 ret_time = 0;
  127. struct task_struct *task = current;
  128. struct fm_trace_t trace;
  129. if ((NULL == buf) || (len < 0) || (0 == mask)
  130. || (cnt > SW_RETRY_CNT_MAX) || (timeout > SW_WAIT_TIMEOUT_MAX)) {
  131. WCN_DBG(FM_ERR | LINK, "cmd tx, invalid para\n");
  132. return -FM_EPARA;
  133. }
  134. FM_EVENT_CLR(link_event->ln_event, mask);
  135. #ifdef FM_TRACE_ENABLE
  136. trace.type = buf[0];
  137. trace.opcode = buf[1];
  138. trace.len = len - 4;
  139. trace.tid = (fm_s32) task->pid;
  140. fm_memset(trace.pkt, 0, FM_TRACE_PKT_SIZE);
  141. fm_memcpy(trace.pkt, &buf[4], (trace.len > FM_TRACE_PKT_SIZE) ? FM_TRACE_PKT_SIZE : trace.len);
  142. #endif
  143. #ifdef FM_TRACE_ENABLE
  144. if (fm_true == FM_TRACE_FULL(cmd_fifo))
  145. FM_TRACE_OUT(cmd_fifo, NULL);
  146. FM_TRACE_IN(cmd_fifo, &trace);
  147. #endif
  148. /* send cmd to FM firmware */
  149. ret_time = mtk_wcn_stp_send_data(buf, len, FM_TASK_INDX);
  150. if (ret_time <= 0) {
  151. WCN_DBG(FM_EMG | LINK, "send data over stp failed[%d]\n", ret_time);
  152. return -FM_ELINK;
  153. }
  154. /* wait the response form FM firmware */
  155. ret_time = FM_EVENT_WAIT_TIMEOUT(link_event->ln_event, mask, timeout);
  156. if (!ret_time) {
  157. if (0 < cnt--) {
  158. WCN_DBG(FM_WAR | LINK, "wait even timeout, [retry_cnt=%d], pid=%d\n", cnt, task->pid);
  159. fm_print_cmd_fifo();
  160. fm_print_evt_fifo();
  161. return -FM_EFW;
  162. } else {
  163. WCN_DBG(FM_ALT | LINK, "fatal error, SW retry failed, reset HW\n");
  164. return -FM_EFW;
  165. }
  166. }
  167. FM_EVENT_CLR(link_event->ln_event, mask);
  168. if (callback)
  169. callback(&link_event->result);
  170. return 0;
  171. }
  172. fm_s32 fm_event_parser(fm_s32(*rds_parser) (struct rds_rx_t *, fm_s32))
  173. {
  174. fm_s32 len;
  175. fm_s32 i = 0;
  176. fm_u8 opcode = 0;
  177. fm_u16 length = 0;
  178. fm_u8 ch;
  179. fm_u8 rx_buf[RX_BUF_SIZE + 10] = { 0 }; /* the 10 bytes are protect gaps */
  180. static volatile fm_task_parser_state state = FM_TASK_RX_PARSER_PKT_TYPE;
  181. struct fm_trace_t trace;
  182. struct task_struct *task = current;
  183. len = mtk_wcn_stp_receive_data(rx_buf, RX_BUF_SIZE, FM_TASK_INDX);
  184. WCN_DBG(FM_DBG | LINK, "[len=%d],[CMD=0x%02x 0x%02x 0x%02x 0x%02x]\n", len, rx_buf[0],
  185. rx_buf[1], rx_buf[2], rx_buf[3]);
  186. while (i < len) {
  187. ch = rx_buf[i];
  188. switch (state) {
  189. case FM_TASK_RX_PARSER_PKT_TYPE:
  190. if (ch == FM_TASK_EVENT_PKT_TYPE) {
  191. if ((i + 5) < RX_BUF_SIZE) {
  192. WCN_DBG(FM_DBG | LINK,
  193. "0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n",
  194. rx_buf[i], rx_buf[i + 1], rx_buf[i + 2],
  195. rx_buf[i + 3], rx_buf[i + 4], rx_buf[i + 5]);
  196. } else {
  197. WCN_DBG(FM_DBG | LINK, "0x%02x 0x%02x\n", rx_buf[i], rx_buf[i + 1]);
  198. }
  199. state = FM_TASK_RX_PARSER_OPCODE;
  200. } else {
  201. WCN_DBG(FM_ALT | LINK, "event pkt type error (rx_buf[%d] = 0x%02x)\n", i, ch);
  202. }
  203. i++;
  204. break;
  205. case FM_TASK_RX_PARSER_OPCODE:
  206. i++;
  207. opcode = ch;
  208. state = FM_TASK_RX_PARSER_PKT_LEN_1;
  209. break;
  210. case FM_TASK_RX_PARSER_PKT_LEN_1:
  211. i++;
  212. length = ch;
  213. state = FM_TASK_RX_PARSER_PKT_LEN_2;
  214. break;
  215. case FM_TASK_RX_PARSER_PKT_LEN_2:
  216. i++;
  217. length |= (fm_u16) (ch << 0x8);
  218. #ifdef FM_TRACE_ENABLE
  219. trace.type = FM_TASK_EVENT_PKT_TYPE;
  220. trace.opcode = opcode;
  221. trace.len = length;
  222. trace.tid = (fm_s32) task->pid;
  223. fm_memset(trace.pkt, 0, FM_TRACE_PKT_SIZE);
  224. fm_memcpy(trace.pkt, &rx_buf[i], (length > FM_TRACE_PKT_SIZE) ? FM_TRACE_PKT_SIZE : length);
  225. if (fm_true == FM_TRACE_FULL(cmd_fifo))
  226. FM_TRACE_OUT(cmd_fifo, NULL);
  227. FM_TRACE_IN(cmd_fifo, &trace);
  228. #endif
  229. if (length > 0) {
  230. state = FM_TASK_RX_PARSER_PKT_PAYLOAD;
  231. } else if (opcode == CSPI_WRITE_OPCODE) {
  232. state = FM_TASK_RX_PARSER_PKT_TYPE;
  233. FM_EVENT_SEND(link_event->ln_event, FLAG_CSPI_WRITE);
  234. } else {
  235. state = FM_TASK_RX_PARSER_PKT_TYPE;
  236. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  237. }
  238. break;
  239. case FM_TASK_RX_PARSER_PKT_PAYLOAD:
  240. switch (opcode) {
  241. case FM_TUNE_OPCODE:
  242. if ((length == 1) && (rx_buf[i] == 1))
  243. FM_EVENT_SEND(link_event->ln_event, FLAG_TUNE_DONE);
  244. break;
  245. case FM_SOFT_MUTE_TUNE_OPCODE:
  246. if (length >= 2) {
  247. fm_memcpy(link_event->result.cqi, &rx_buf[i],
  248. (length > FM_CQI_BUF_SIZE) ? FM_CQI_BUF_SIZE : length);
  249. FM_EVENT_SEND(link_event->ln_event, FLAG_SM_TUNE);
  250. }
  251. break;
  252. case FM_SEEK_OPCODE:
  253. if ((i + 1) < RX_BUF_SIZE)
  254. link_event->result.seek_result = rx_buf[i] + (rx_buf[i + 1] << 8);
  255. FM_EVENT_SEND(link_event->ln_event, FLAG_SEEK_DONE);
  256. break;
  257. case FM_SCAN_OPCODE:
  258. /* check if the result data is long enough */
  259. if ((RX_BUF_SIZE - i) < (sizeof(fm_u16) * FM_SCANTBL_SIZE)) {
  260. WCN_DBG(FM_ALT | LINK,
  261. "FM_SCAN_OPCODE err, [tblsize=%d],[bufsize=%d]\n",
  262. (unsigned int)(sizeof(fm_u16) * FM_SCANTBL_SIZE),
  263. (unsigned int)(RX_BUF_SIZE - i));
  264. FM_EVENT_SEND(link_event->ln_event, FLAG_SCAN_DONE);
  265. return 0;
  266. } else if ((length >= FM_CQI_BUF_SIZE)
  267. && ((RX_BUF_SIZE - i) >= FM_CQI_BUF_SIZE)) {
  268. fm_memcpy(link_event->result.cqi, &rx_buf[i], FM_CQI_BUF_SIZE);
  269. FM_EVENT_SEND(link_event->ln_event, FLAG_CQI_DONE);
  270. } else {
  271. fm_memcpy(link_event->result.scan_result, &rx_buf[i],
  272. sizeof(fm_u16) * FM_SCANTBL_SIZE);
  273. FM_EVENT_SEND(link_event->ln_event, FLAG_SCAN_DONE);
  274. }
  275. break;
  276. case FSPI_READ_OPCODE:
  277. if ((i + 1) < RX_BUF_SIZE)
  278. link_event->result.fspi_rd = (rx_buf[i] + (rx_buf[i + 1] << 8));
  279. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  280. break;
  281. case CSPI_READ_OPCODE:
  282. {
  283. if ((i + 1) < RX_BUF_SIZE) {
  284. link_event->result.cspi_rd =
  285. (rx_buf[i] + (rx_buf[i + 1] << 8) +
  286. (rx_buf[i + 2] << 16) + (rx_buf[i + 3] << 24));
  287. }
  288. FM_EVENT_SEND(link_event->ln_event, FLAG_CSPI_READ);
  289. break;
  290. }
  291. case FM_HOST_READ_OPCODE:
  292. {
  293. if ((i + 1) < RX_BUF_SIZE) {
  294. link_event->result.cspi_rd =
  295. (rx_buf[i] + (rx_buf[i + 1] << 8) +
  296. (rx_buf[i + 2] << 16) + (rx_buf[i + 3] << 24));
  297. }
  298. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  299. break;
  300. }
  301. case RDS_RX_DATA_OPCODE:
  302. /* check if the rds data is long enough */
  303. if ((RX_BUF_SIZE - i) < length) {
  304. WCN_DBG(FM_ALT | LINK,
  305. "RDS RX err, [rxlen=%d],[bufsize=%d]\n",
  306. (fm_s32) length, (RX_BUF_SIZE - i));
  307. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  308. break;
  309. }
  310. /* copy rds data to rds buf */
  311. fm_memcpy(&link_event->result.rds_rx_result, &rx_buf[i], length);
  312. /*Handle the RDS data that we get */
  313. if (rds_parser)
  314. rds_parser(&link_event->result.rds_rx_result, length);
  315. else
  316. WCN_DBG(FM_WAR | LINK, "no method to parse RDS data\n");
  317. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  318. break;
  319. default:
  320. FM_EVENT_SEND(link_event->ln_event, (1 << opcode));
  321. break;
  322. }
  323. state = FM_TASK_RX_PARSER_PKT_TYPE;
  324. i += length;
  325. break;
  326. default:
  327. break;
  328. }
  329. }
  330. return 0;
  331. }
  332. fm_bool fm_wait_stc_done(fm_u32 sec)
  333. {
  334. return fm_true;
  335. }
  336. fm_s32 fm_force_active_event(fm_u32 mask)
  337. {
  338. fm_u32 flag;
  339. flag = FM_EVENT_GET(link_event->ln_event);
  340. WCN_DBG(FM_WAR | LINK, "before force active event, [flag=0x%08x]\n", flag);
  341. flag = FM_EVENT_SEND(link_event->ln_event, mask);
  342. WCN_DBG(FM_WAR | LINK, "after force active event, [flag=0x%08x]\n", flag);
  343. return 0;
  344. }
  345. extern fm_s32 fm_print_cmd_fifo(void)
  346. {
  347. #ifdef FM_TRACE_ENABLE
  348. struct fm_trace_t trace;
  349. fm_s32 i = 0;
  350. trace.time = 0;
  351. while (fm_false == FM_TRACE_EMPTY(cmd_fifo)) {
  352. fm_memset(trace.pkt, 0, FM_TRACE_PKT_SIZE);
  353. FM_TRACE_OUT(cmd_fifo, &trace);
  354. WCN_DBG(FM_ALT | LINK, "trace, type %d, op %d, len %d, tid %d, time %d\n",
  355. trace.type, trace.opcode, trace.len, trace.tid, jiffies_to_msecs(abs(trace.time)));
  356. i = 0;
  357. while ((trace.len > 0) && (i < trace.len) && (i < (FM_TRACE_PKT_SIZE - 8))) {
  358. WCN_DBG(FM_ALT | LINK, "trace, %02x %02x %02x %02x %02x %02x %02x %02x\n",
  359. trace.pkt[i], trace.pkt[i + 1], trace.pkt[i + 2], trace.pkt[i + 3],
  360. trace.pkt[i + 4], trace.pkt[i + 5], trace.pkt[i + 6], trace.pkt[i + 7]);
  361. i += 8;
  362. }
  363. WCN_DBG(FM_ALT | LINK, "trace\n");
  364. }
  365. #endif
  366. return 0;
  367. }
  368. extern fm_s32 fm_print_evt_fifo(void)
  369. {
  370. #ifdef FM_TRACE_ENABLE
  371. struct fm_trace_t trace;
  372. fm_s32 i = 0;
  373. trace.time = 0;
  374. while (fm_false == FM_TRACE_EMPTY(evt_fifo)) {
  375. fm_memset(trace.pkt, 0, FM_TRACE_PKT_SIZE);
  376. FM_TRACE_OUT(evt_fifo, &trace);
  377. WCN_DBG(FM_ALT | LINK, "%s: op %d, len %d, %d\n", evt_fifo->name, trace.opcode,
  378. trace.len, jiffies_to_msecs(abs(trace.time)));
  379. i = 0;
  380. while ((trace.len > 0) && (i < trace.len) && (i < (FM_TRACE_PKT_SIZE - 8))) {
  381. WCN_DBG(FM_ALT | LINK, "%s: %02x %02x %02x %02x %02x %02x %02x %02x\n",
  382. evt_fifo->name, trace.pkt[i], trace.pkt[i + 1], trace.pkt[i + 2],
  383. trace.pkt[i + 3], trace.pkt[i + 4], trace.pkt[i + 5],
  384. trace.pkt[i + 6], trace.pkt[i + 7]);
  385. i += 8;
  386. }
  387. WCN_DBG(FM_ALT | LINK, "%s\n", evt_fifo->name);
  388. }
  389. #endif
  390. return 0;
  391. }
  392. #endif
  393. fm_s32 fm_trace_in(struct fm_trace_fifo_t *thiz, struct fm_trace_t *new_tra)
  394. {
  395. if (new_tra == NULL) {
  396. pr_err("%s,invalid pointer\n", __func__);
  397. return -FM_EPARA;
  398. }
  399. if (thiz->len < thiz->size) {
  400. fm_memcpy(&(thiz->trace[thiz->in]), new_tra, sizeof(struct fm_trace_t));
  401. thiz->trace[thiz->in].time = jiffies;
  402. thiz->in = (thiz->in + 1) % thiz->size;
  403. thiz->len++;
  404. /* WCN_DBG(FM_DBG | RDSC, "add a new tra[len=%d]\n", thiz->len); */
  405. } else {
  406. WCN_DBG(FM_WAR | RDSC, "tra buf is full\n");
  407. return -FM_ENOMEM;
  408. }
  409. return 0;
  410. }
  411. fm_s32 fm_trace_out(struct fm_trace_fifo_t *thiz, struct fm_trace_t *dst_tra)
  412. {
  413. if (thiz->len > 0) {
  414. if (dst_tra) {
  415. fm_memcpy(dst_tra, &(thiz->trace[thiz->out]), sizeof(struct fm_trace_t));
  416. fm_memset(&(thiz->trace[thiz->out]), 0, sizeof(struct fm_trace_t));
  417. }
  418. thiz->out = (thiz->out + 1) % thiz->size;
  419. thiz->len--;
  420. /* WCN_DBG(FM_DBG | RDSC, "del a tra[len=%d]\n", thiz->len); */
  421. } else {
  422. WCN_DBG(FM_WAR | RDSC, "tra buf is empty\n");
  423. }
  424. return 0;
  425. }
  426. fm_bool fm_trace_is_full(struct fm_trace_fifo_t *thiz)
  427. {
  428. return (thiz->len == thiz->size) ? fm_true : fm_false;
  429. }
  430. fm_bool fm_trace_is_empty(struct fm_trace_fifo_t *thiz)
  431. {
  432. return (thiz->len == 0) ? fm_true : fm_false;
  433. }
  434. struct fm_trace_fifo_t *fm_trace_fifo_create(const fm_s8 *name)
  435. {
  436. struct fm_trace_fifo_t *tmp;
  437. tmp = fm_zalloc(sizeof(struct fm_trace_fifo_t));
  438. if (!tmp) {
  439. WCN_DBG(FM_ALT | MAIN, "fm_zalloc(fm_trace_fifo) -ENOMEM\n");
  440. return NULL;
  441. }
  442. fm_memcpy(tmp->name, name, (strlen(name) + 1));
  443. tmp->size = FM_TRACE_FIFO_SIZE;
  444. tmp->in = 0;
  445. tmp->out = 0;
  446. tmp->len = 0;
  447. tmp->trace_in = fm_trace_in;
  448. tmp->trace_out = fm_trace_out;
  449. tmp->is_full = fm_trace_is_full;
  450. tmp->is_empty = fm_trace_is_empty;
  451. WCN_DBG(FM_NTC | LINK, "%s created\n", tmp->name);
  452. return tmp;
  453. }
  454. fm_s32 fm_trace_fifo_release(struct fm_trace_fifo_t *fifo)
  455. {
  456. if (fifo) {
  457. WCN_DBG(FM_NTC | LINK, "%s released\n", fifo->name);
  458. fm_free(fifo);
  459. }
  460. return 0;
  461. }