diff options
Diffstat (limited to 'firmware/target/arm/imx31/spi-imx31.c')
-rw-r--r-- | firmware/target/arm/imx31/spi-imx31.c | 419 |
1 files changed, 249 insertions, 170 deletions
diff --git a/firmware/target/arm/imx31/spi-imx31.c b/firmware/target/arm/imx31/spi-imx31.c index ac063f9b10..3f66257c95 100644 --- a/firmware/target/arm/imx31/spi-imx31.c +++ b/firmware/target/arm/imx31/spi-imx31.c | |||
@@ -38,19 +38,18 @@ static __attribute__((interrupt("IRQ"))) void CSPI3_HANDLER(void); | |||
38 | #endif | 38 | #endif |
39 | 39 | ||
40 | /* State data associatated with each CSPI module */ | 40 | /* State data associatated with each CSPI module */ |
41 | static struct spi_module_descriptor | 41 | static struct spi_module_desc |
42 | { | 42 | { |
43 | struct cspi_map * const base; | 43 | struct cspi_map * const base; /* CSPI module address */ |
44 | int enab; | 44 | struct spi_transfer_desc *head; /* Running job */ |
45 | struct spi_node *last; | 45 | struct spi_transfer_desc *tail; /* Most recent job added */ |
46 | enum IMX31_CG_LIST cg; | 46 | const struct spi_node *last_node; /* Last node used for module */ |
47 | enum IMX31_INT_LIST ints; | 47 | void (*handler)(void); /* Interrupt handler */ |
48 | int byte_size; | 48 | int rxcount; /* Independent copy of txcount */ |
49 | void (*handler)(void); | 49 | int8_t enab; /* Enable count */ |
50 | struct mutex m; | 50 | int8_t byte_size; /* Size of transfers in bytes */ |
51 | struct wakeup w; | 51 | int8_t cg; /* Clock-gating value */ |
52 | struct spi_transfer *trans; | 52 | int8_t ints; /* AVIC vector number */ |
53 | int rxcount; | ||
54 | } spi_descs[SPI_NUM_CSPI] = | 53 | } spi_descs[SPI_NUM_CSPI] = |
55 | /* Init non-zero members */ | 54 | /* Init non-zero members */ |
56 | { | 55 | { |
@@ -80,93 +79,224 @@ static struct spi_module_descriptor | |||
80 | #endif | 79 | #endif |
81 | }; | 80 | }; |
82 | 81 | ||
82 | /* Reset the module */ | ||
83 | static void spi_reset(struct spi_module_desc * const desc) | ||
84 | { | ||
85 | /* Reset by leaving it disabled */ | ||
86 | struct cspi_map * const base = desc->base; | ||
87 | base->conreg &= ~CSPI_CONREG_EN; | ||
88 | } | ||
89 | |||
90 | /* Write the context for the node and remember it to avoid unneeded reconfigure */ | ||
91 | static bool spi_set_context(struct spi_module_desc *desc, | ||
92 | struct spi_transfer_desc *xfer) | ||
93 | { | ||
94 | const struct spi_node * const node = xfer->node; | ||
95 | struct cspi_map * const base = desc->base; | ||
96 | |||
97 | if (desc->enab == 0) | ||
98 | return false; | ||
99 | |||
100 | if (node == desc->last_node) | ||
101 | return true; | ||
102 | |||
103 | /* Errata says CSPI should be disabled when writing PERIODREG. */ | ||
104 | base->conreg &= ~CSPI_CONREG_EN; | ||
105 | |||
106 | /* Switch the module's node */ | ||
107 | desc->last_node = node; | ||
108 | desc->byte_size = (((node->conreg >> 8) & 0x1f) + 1 + 7) / 8 - 1; | ||
109 | |||
110 | /* Set the wait-states */ | ||
111 | base->periodreg = node->periodreg & 0xffff; | ||
112 | |||
113 | /* Keep reserved and start bits cleared. Keep enabled bit. */ | ||
114 | base->conreg = | ||
115 | (node->conreg & ~(0xfcc8e000 | CSPI_CONREG_XCH | CSPI_CONREG_SMC)); | ||
116 | return true; | ||
117 | } | ||
118 | |||
119 | |||
120 | /* Fill the TX fifo. Returns the number of remaining words. */ | ||
121 | static int tx_fill_fifo(struct spi_module_desc * const desc, | ||
122 | struct cspi_map * const base, | ||
123 | struct spi_transfer_desc * const xfer) | ||
124 | { | ||
125 | int count = xfer->count; | ||
126 | int size = desc->byte_size; | ||
127 | |||
128 | while ((base->statreg & CSPI_STATREG_TF) == 0) | ||
129 | { | ||
130 | uint32_t word = 0; | ||
131 | |||
132 | switch (size & 3) | ||
133 | { | ||
134 | case 3: | ||
135 | word = *(unsigned char *)(xfer->txbuf + 3) << 24; | ||
136 | case 2: | ||
137 | word |= *(unsigned char *)(xfer->txbuf + 2) << 16; | ||
138 | case 1: | ||
139 | word |= *(unsigned char *)(xfer->txbuf + 1) << 8; | ||
140 | case 0: | ||
141 | word |= *(unsigned char *)(xfer->txbuf + 0); | ||
142 | } | ||
143 | |||
144 | xfer->txbuf += size + 1; /* Increment buffer */ | ||
145 | |||
146 | base->txdata = word; /* Write to FIFO */ | ||
147 | |||
148 | if (--count == 0) | ||
149 | break; | ||
150 | } | ||
151 | |||
152 | xfer->count = count; | ||
153 | |||
154 | return count; | ||
155 | } | ||
156 | |||
157 | /* Start a transfer on the SPI */ | ||
158 | static bool start_transfer(struct spi_module_desc * const desc, | ||
159 | struct spi_transfer_desc * const xfer) | ||
160 | { | ||
161 | struct cspi_map * const base = desc->base; | ||
162 | unsigned long intreg; | ||
163 | |||
164 | if (!spi_set_context(desc, xfer)) | ||
165 | return false; | ||
166 | |||
167 | base->conreg |= CSPI_CONREG_EN; /* Enable module */ | ||
168 | |||
169 | desc->rxcount = xfer->count; | ||
170 | |||
171 | intreg = (xfer->count < 8) ? | ||
172 | CSPI_INTREG_TCEN : /* Trans. complete: TX will run out in prefill */ | ||
173 | CSPI_INTREG_THEN; /* INT when TX half-empty */ | ||
174 | |||
175 | intreg |= (xfer->count < 4) ? | ||
176 | CSPI_INTREG_RREN : /* Must grab data on every word */ | ||
177 | CSPI_INTREG_RHEN; /* Enough data to wait for half-full */ | ||
178 | |||
179 | tx_fill_fifo(desc, base, xfer); | ||
180 | |||
181 | base->statreg = CSPI_STATREG_TC; /* Ack 'complete' */ | ||
182 | base->intreg = intreg; /* Enable interrupts */ | ||
183 | base->conreg |= CSPI_CONREG_XCH; /* Begin transfer */ | ||
184 | |||
185 | return true; | ||
186 | } | ||
187 | |||
83 | /* Common code for interrupt handlers */ | 188 | /* Common code for interrupt handlers */ |
84 | static void spi_interrupt(enum spi_module_number spi) | 189 | static void spi_interrupt(enum spi_module_number spi) |
85 | { | 190 | { |
86 | struct spi_module_descriptor *desc = &spi_descs[spi]; | 191 | struct spi_module_desc *desc = &spi_descs[spi]; |
87 | struct cspi_map * const base = desc->base; | 192 | struct cspi_map * const base = desc->base; |
88 | struct spi_transfer *trans = desc->trans; | 193 | unsigned long intreg = base->intreg; |
194 | struct spi_transfer_desc *xfer = desc->head; | ||
89 | int inc = desc->byte_size + 1; | 195 | int inc = desc->byte_size + 1; |
90 | 196 | ||
91 | if (desc->rxcount > 0) | 197 | /* Data received - empty out RXFIFO */ |
198 | while ((base->statreg & CSPI_STATREG_RR) != 0) | ||
92 | { | 199 | { |
93 | /* Data received - empty out RXFIFO */ | 200 | uint32_t word = base->rxdata; |
94 | while ((base->statreg & CSPI_STATREG_RR) != 0) | ||
95 | { | ||
96 | uint32_t word = base->rxdata; | ||
97 | 201 | ||
202 | if (desc->rxcount <= 0) | ||
203 | continue; | ||
204 | |||
205 | if (xfer->rxbuf != NULL) | ||
206 | { | ||
207 | /* There is a receive buffer */ | ||
98 | switch (desc->byte_size & 3) | 208 | switch (desc->byte_size & 3) |
99 | { | 209 | { |
100 | case 3: | 210 | case 3: |
101 | *(unsigned char *)(trans->rxbuf + 3) = word >> 24; | 211 | *(unsigned char *)(xfer->rxbuf + 3) = word >> 24; |
102 | case 2: | 212 | case 2: |
103 | *(unsigned char *)(trans->rxbuf + 2) = word >> 16; | 213 | *(unsigned char *)(xfer->rxbuf + 2) = word >> 16; |
104 | case 1: | 214 | case 1: |
105 | *(unsigned char *)(trans->rxbuf + 1) = word >> 8; | 215 | *(unsigned char *)(xfer->rxbuf + 1) = word >> 8; |
106 | case 0: | 216 | case 0: |
107 | *(unsigned char *)(trans->rxbuf + 0) = word; | 217 | *(unsigned char *)(xfer->rxbuf + 0) = word; |
108 | } | 218 | } |
109 | 219 | ||
110 | trans->rxbuf += inc; | 220 | xfer->rxbuf += inc; |
221 | } | ||
111 | 222 | ||
112 | if (--desc->rxcount < 4) | 223 | if (--desc->rxcount < 4) |
224 | { | ||
225 | if (desc->rxcount == 0) | ||
226 | { | ||
227 | /* No more to receive - stop RX interrupts */ | ||
228 | intreg &= ~(CSPI_INTREG_RHEN | CSPI_INTREG_RREN); | ||
229 | base->intreg = intreg; | ||
230 | } | ||
231 | else if (intreg & CSPI_INTREG_RHEN) | ||
113 | { | 232 | { |
114 | unsigned long intreg = base->intreg; | 233 | /* < 4 words expected - switch to RX ready */ |
115 | 234 | intreg &= ~CSPI_INTREG_RHEN; | |
116 | if (desc->rxcount <= 0) | 235 | intreg |= CSPI_INTREG_RREN; |
117 | { | 236 | base->intreg = intreg; |
118 | /* No more to receive - stop RX interrupts */ | ||
119 | intreg &= ~(CSPI_INTREG_RHEN | CSPI_INTREG_RREN); | ||
120 | base->intreg = intreg; | ||
121 | break; | ||
122 | } | ||
123 | else if (!(intreg & CSPI_INTREG_RREN)) | ||
124 | { | ||
125 | /* < 4 words expected - switch to RX ready */ | ||
126 | intreg &= ~CSPI_INTREG_RHEN; | ||
127 | base->intreg = intreg | CSPI_INTREG_RREN; | ||
128 | } | ||
129 | } | 237 | } |
130 | } | 238 | } |
131 | } | 239 | } |
132 | 240 | ||
133 | if (trans->count > 0) | 241 | if (xfer->count > 0) |
134 | { | 242 | { |
135 | /* Data to transmit - fill TXFIFO or write until exhausted */ | 243 | /* Data to transmit - fill TXFIFO or write until exhausted. */ |
136 | while ((base->statreg & CSPI_STATREG_TF) == 0) | 244 | if (tx_fill_fifo(desc, base, xfer) != 0) |
245 | return; | ||
246 | |||
247 | /* Out of data - stop TX interrupts, enable TC interrupt. */ | ||
248 | intreg &= ~CSPI_INTREG_THEN; | ||
249 | intreg |= CSPI_INTREG_TCEN; | ||
250 | base->intreg = intreg; | ||
251 | } | ||
252 | |||
253 | if ((intreg & CSPI_INTREG_TCEN) && (base->statreg & CSPI_STATREG_TC)) | ||
254 | { | ||
255 | /* Outbound transfer is complete. */ | ||
256 | intreg &= ~CSPI_INTREG_TCEN; | ||
257 | base->intreg = intreg; | ||
258 | base->statreg = CSPI_STATREG_TC; /* Ack 'complete' */ | ||
259 | } | ||
260 | |||
261 | if (intreg != 0) | ||
262 | return; | ||
263 | |||
264 | /* All interrupts are masked; we're done with current transfer. */ | ||
265 | for (;;) | ||
266 | { | ||
267 | struct spi_transfer_desc *next = xfer->next; | ||
268 | spi_transfer_cb_fn_type callback = xfer->callback; | ||
269 | xfer->next = NULL; | ||
270 | |||
271 | base->conreg &= ~CSPI_CONREG_EN; /* Disable module */ | ||
272 | |||
273 | if (next == xfer) | ||
137 | { | 274 | { |
138 | uint32_t word = 0; | 275 | /* Last job on queue */ |
276 | desc->head = NULL; | ||
139 | 277 | ||
140 | switch (desc->byte_size & 3) | 278 | if (callback != NULL) |
141 | { | 279 | callback(xfer); |
142 | case 3: | ||
143 | word = *(unsigned char *)(trans->txbuf + 3) << 24; | ||
144 | case 2: | ||
145 | word |= *(unsigned char *)(trans->txbuf + 2) << 16; | ||
146 | case 1: | ||
147 | word |= *(unsigned char *)(trans->txbuf + 1) << 8; | ||
148 | case 0: | ||
149 | word |= *(unsigned char *)(trans->txbuf + 0); | ||
150 | } | ||
151 | 280 | ||
152 | trans->txbuf += inc; | 281 | /* Callback may have restarted transfers. */ |
282 | } | ||
283 | else | ||
284 | { | ||
285 | /* Queue next job. */ | ||
286 | desc->head = next; | ||
153 | 287 | ||
154 | base->txdata = word; | 288 | if (callback != NULL) |
289 | callback(xfer); | ||
155 | 290 | ||
156 | if (--trans->count <= 0) | 291 | if (!start_transfer(desc, next)) |
157 | { | 292 | { |
158 | /* Out of data - stop TX interrupts */ | 293 | xfer = next; |
159 | base->intreg &= ~CSPI_INTREG_THEN; | 294 | xfer->count = -1; |
160 | break; | 295 | continue; /* Failed: try next */ |
161 | } | 296 | } |
162 | } | 297 | } |
163 | } | ||
164 | 298 | ||
165 | /* If all interrupts have been remasked - we're done */ | 299 | break; |
166 | if (base->intreg == 0) | ||
167 | { | ||
168 | base->statreg = CSPI_STATREG_TC | CSPI_STATREG_BO; | ||
169 | wakeup_signal(&desc->w); | ||
170 | } | 300 | } |
171 | } | 301 | } |
172 | 302 | ||
@@ -192,105 +322,50 @@ static __attribute__((interrupt("IRQ"))) void CSPI3_HANDLER(void) | |||
192 | } | 322 | } |
193 | #endif | 323 | #endif |
194 | 324 | ||
195 | /* Write the context for the node and remember it to avoid unneeded reconfigure */ | 325 | /* Initialize the SPI driver */ |
196 | static bool spi_set_context(struct spi_node *node, | ||
197 | struct spi_module_descriptor *desc) | ||
198 | { | ||
199 | struct cspi_map * const base = desc->base; | ||
200 | |||
201 | if ((base->conreg & CSPI_CONREG_EN) == 0) | ||
202 | return false; | ||
203 | |||
204 | if (node != desc->last) | ||
205 | { | ||
206 | /* Switch the module's node */ | ||
207 | desc->last = node; | ||
208 | desc->byte_size = (((node->conreg >> 8) & 0x1f) + 1 + 7) / 8 - 1; | ||
209 | |||
210 | /* Keep reserved and start bits cleared. Keep enabled bit. */ | ||
211 | base->conreg = | ||
212 | (node->conreg & ~(0xfcc8e000 | CSPI_CONREG_XCH | CSPI_CONREG_SMC)) | ||
213 | | CSPI_CONREG_EN; | ||
214 | /* Set the wait-states */ | ||
215 | base->periodreg = node->periodreg & 0xffff; | ||
216 | /* Clear out any spuriously-pending interrupts */ | ||
217 | base->statreg = CSPI_STATREG_TC | CSPI_STATREG_BO; | ||
218 | } | ||
219 | |||
220 | return true; | ||
221 | } | ||
222 | |||
223 | static void spi_reset(struct cspi_map * const base) | ||
224 | { | ||
225 | /* Reset */ | ||
226 | base->conreg &= ~CSPI_CONREG_EN; | ||
227 | base->conreg |= CSPI_CONREG_EN; | ||
228 | base->intreg = 0; | ||
229 | base->statreg = CSPI_STATREG_TC | CSPI_STATREG_BO; | ||
230 | } | ||
231 | |||
232 | /* Initialize each of the used SPI descriptors */ | ||
233 | void spi_init(void) | 326 | void spi_init(void) |
234 | { | 327 | { |
235 | int i; | 328 | unsigned i; |
236 | |||
237 | for (i = 0; i < SPI_NUM_CSPI; i++) | 329 | for (i = 0; i < SPI_NUM_CSPI; i++) |
238 | { | 330 | { |
239 | struct spi_module_descriptor * const desc = &spi_descs[i]; | 331 | struct spi_module_desc * const desc = &spi_descs[i]; |
240 | mutex_init(&desc->m); | 332 | ccm_module_clock_gating(desc->cg, CGM_ON_RUN_WAIT); |
241 | wakeup_init(&desc->w); | 333 | spi_reset(desc); |
334 | ccm_module_clock_gating(desc->cg, CGM_OFF); | ||
242 | } | 335 | } |
243 | } | 336 | } |
244 | 337 | ||
245 | /* Get mutually-exclusive access to the node */ | ||
246 | void spi_lock(struct spi_node *node) | ||
247 | { | ||
248 | mutex_lock(&spi_descs[node->num].m); | ||
249 | } | ||
250 | |||
251 | /* Release mutual exclusion */ | ||
252 | void spi_unlock(struct spi_node *node) | ||
253 | { | ||
254 | mutex_unlock(&spi_descs[node->num].m); | ||
255 | } | ||
256 | |||
257 | /* Enable the specified module for the node */ | 338 | /* Enable the specified module for the node */ |
258 | void spi_enable_module(struct spi_node *node) | 339 | void spi_enable_module(const struct spi_node *node) |
259 | { | 340 | { |
260 | struct spi_module_descriptor * const desc = &spi_descs[node->num]; | 341 | struct spi_module_desc * const desc = &spi_descs[node->num]; |
261 | |||
262 | mutex_lock(&desc->m); | ||
263 | 342 | ||
264 | if (++desc->enab == 1) | 343 | if (++desc->enab == 1) |
265 | { | 344 | { |
266 | /* First enable for this module */ | ||
267 | struct cspi_map * const base = desc->base; | ||
268 | |||
269 | /* Enable clock-gating register */ | 345 | /* Enable clock-gating register */ |
270 | ccm_module_clock_gating(desc->cg, CGM_ON_RUN_WAIT); | 346 | ccm_module_clock_gating(desc->cg, CGM_ON_RUN_WAIT); |
271 | /* Reset */ | 347 | /* Reset */ |
272 | spi_reset(base); | 348 | spi_reset(desc); |
273 | desc->last = NULL; | 349 | desc->last_node = NULL; |
274 | /* Enable interrupt at controller level */ | 350 | /* Enable interrupt at controller level */ |
275 | avic_enable_int(desc->ints, INT_TYPE_IRQ, INT_PRIO_DEFAULT, | 351 | avic_enable_int(desc->ints, INT_TYPE_IRQ, INT_PRIO_DEFAULT, |
276 | desc->handler); | 352 | desc->handler); |
277 | } | 353 | } |
278 | |||
279 | mutex_unlock(&desc->m); | ||
280 | } | 354 | } |
281 | 355 | ||
282 | /* Disabled the specified module for the node */ | 356 | /* Disable the specified module for the node */ |
283 | void spi_disable_module(struct spi_node *node) | 357 | void spi_disable_module(const struct spi_node *node) |
284 | { | 358 | { |
285 | struct spi_module_descriptor * const desc = &spi_descs[node->num]; | 359 | struct spi_module_desc * const desc = &spi_descs[node->num]; |
286 | |||
287 | mutex_lock(&desc->m); | ||
288 | 360 | ||
289 | if (desc->enab > 0 && --desc->enab == 0) | 361 | if (desc->enab > 0 && --desc->enab == 0) |
290 | { | 362 | { |
291 | /* Last enable for this module */ | 363 | /* Last enable for this module */ |
292 | struct cspi_map * const base = desc->base; | 364 | struct cspi_map * const base = desc->base; |
293 | 365 | ||
366 | /* Wait for outstanding transactions */ | ||
367 | while (*(void ** volatile)&desc->head != NULL); | ||
368 | |||
294 | /* Disable interrupt at controller level */ | 369 | /* Disable interrupt at controller level */ |
295 | avic_disable_int(desc->ints); | 370 | avic_disable_int(desc->ints); |
296 | 371 | ||
@@ -300,53 +375,57 @@ void spi_disable_module(struct spi_node *node) | |||
300 | /* Disable interface clock */ | 375 | /* Disable interface clock */ |
301 | ccm_module_clock_gating(desc->cg, CGM_OFF); | 376 | ccm_module_clock_gating(desc->cg, CGM_OFF); |
302 | } | 377 | } |
303 | |||
304 | mutex_unlock(&desc->m); | ||
305 | } | 378 | } |
306 | 379 | ||
307 | /* Send and/or receive data on the specified node */ | 380 | /* Send and/or receive data on the specified node */ |
308 | int spi_transfer(struct spi_node *node, struct spi_transfer *trans) | 381 | bool spi_transfer(struct spi_transfer_desc *xfer) |
309 | { | 382 | { |
310 | struct spi_module_descriptor * const desc = &spi_descs[node->num]; | 383 | bool retval; |
311 | int retval; | 384 | struct spi_module_desc * desc; |
312 | 385 | int oldlevel; | |
313 | if (trans->count <= 0) | ||
314 | return true; | ||
315 | |||
316 | mutex_lock(&desc->m); | ||
317 | 386 | ||
318 | retval = spi_set_context(node, desc); | 387 | if (xfer->count == 0) |
388 | return true; /* No data? No problem. */ | ||
319 | 389 | ||
320 | if (retval) | 390 | if (xfer->count < 0 || xfer->next != NULL || xfer->node == NULL) |
321 | { | 391 | { |
322 | struct cspi_map * const base = desc->base; | 392 | /* Can't pass a busy descriptor, requires a node and negative size |
323 | unsigned long intreg; | 393 | * is invalid to pass. */ |
324 | 394 | return false; | |
325 | desc->trans = trans; | 395 | } |
326 | desc->rxcount = trans->count; | ||
327 | |||
328 | /* Enable needed interrupts - FIFOs will start filling */ | ||
329 | intreg = CSPI_INTREG_THEN; | ||
330 | 396 | ||
331 | intreg |= (trans->count < 4) ? | 397 | oldlevel = disable_irq_save(); |
332 | CSPI_INTREG_RREN : /* Must grab data on every word */ | ||
333 | CSPI_INTREG_RHEN; /* Enough data to wait for half-full */ | ||
334 | 398 | ||
335 | base->intreg = intreg; | 399 | desc = &spi_descs[xfer->node->num]; |
336 | 400 | ||
337 | /* Start transfer */ | 401 | if (desc->head == NULL) |
338 | base->conreg |= CSPI_CONREG_XCH; | 402 | { |
403 | /* No transfers in progress; start interface. */ | ||
404 | retval = start_transfer(desc, xfer); | ||
339 | 405 | ||
340 | if (wakeup_wait(&desc->w, HZ) != OBJ_WAIT_SUCCEEDED) | 406 | if (retval) |
341 | { | 407 | { |
342 | base->intreg = 0; /* Stop SPI ints */ | 408 | /* Start ok: actually put it in the queue. */ |
343 | spi_reset(base); /* Reset module (esp. to empty FIFOs) */ | 409 | desc->head = xfer; |
344 | desc->last = NULL; /* Force reconfigure */ | 410 | desc->tail = xfer; |
345 | retval = false; | 411 | xfer->next = xfer; /* First, self-reference terminate */ |
346 | } | 412 | } |
413 | else | ||
414 | { | ||
415 | xfer->count = -1; /* Signal error */ | ||
416 | } | ||
417 | } | ||
418 | else | ||
419 | { | ||
420 | /* Already running: simply add to end and the final INT on the | ||
421 | * running transfer will pick it up. */ | ||
422 | desc->tail->next = xfer; /* Add to tail */ | ||
423 | desc->tail = xfer; /* New tail */ | ||
424 | xfer->next = xfer; /* Self-reference terminate */ | ||
425 | retval = true; | ||
347 | } | 426 | } |
348 | 427 | ||
349 | mutex_unlock(&desc->m); | 428 | restore_irq(oldlevel); |
350 | 429 | ||
351 | return retval; | 430 | return retval; |
352 | } | 431 | } |