drivers: i2s_nrfx: Fix write race condition
There is inherent race condition between i2s_nrfx_write() and I2S interrupt handler because I2S operates independently from the rest of the system. If software takes too long to supply next TX pointer then nRF I2S peripheral will simply resupply the previous buffer. The race window is rather short. The failed race executes as follows: 1. i2s_nrfx_write() checks state and loads next_tx_buffer_needed 2. I2S interrupt handler executes and calls data_handler() which notices empty TX queue and therefore sets next_tx_buffer_needed 3. i2s_nrfx_write() continues with the queue TX path (because the next_tx_buffer_needed was false when it was accessed) If next i2s_nrfx_write() executes before next I2S interrupt: 4a. i2s_nrfx_write() notices next_tx_buffer_needed is true and supplies the buffer directly to I2S peripheral. Previously queued buffer will remain in the queue until the just supplied buffer starts transmitting. Effectively swapping whole I2S block leads to clearly audible artifacts under normal circumstances. If next I2S interrupt executes before next i2s_nrfx_write(): 4b. data_handler() notices that buffer was reused and stops despite having a buffer available in TX queue Modify i2s_nrfx_write() to always queue the TX pointer first and only supply the buffer to nrfx if the queue was empty when interrupt handler executed. This prevents both the out-of-order TX and premature stop. Fixes: #63730 Signed-off-by: Tomasz Moń <tomasz.mon@nordicsemi.no>
This commit is contained in:
parent
8f7180bece
commit
6b8b49c64a
1 changed files with 26 additions and 10 deletions
|
@ -583,6 +583,7 @@ static int i2s_nrfx_write(const struct device *dev,
|
|||
void *mem_block, size_t size)
|
||||
{
|
||||
struct i2s_nrfx_drv_data *drv_data = dev->data;
|
||||
int ret;
|
||||
|
||||
if (!drv_data->tx_configured) {
|
||||
LOG_ERR("Device is not configured");
|
||||
|
@ -601,26 +602,41 @@ static int i2s_nrfx_write(const struct device *dev,
|
|||
return -EIO;
|
||||
}
|
||||
|
||||
ret = k_msgq_put(&drv_data->tx_queue,
|
||||
&mem_block,
|
||||
SYS_TIMEOUT_MS(drv_data->tx.cfg.timeout));
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
LOG_DBG("Queued TX %p", mem_block);
|
||||
|
||||
/* Check if interrupt wanted to get next TX buffer before current buffer
|
||||
* was queued. Do not move this check before queuing because doing so
|
||||
* opens the possibility for a race condition between this function and
|
||||
* data_handler() that is called in interrupt context.
|
||||
*/
|
||||
if (drv_data->state == I2S_STATE_RUNNING &&
|
||||
drv_data->next_tx_buffer_needed) {
|
||||
nrfx_i2s_buffers_t next = { .p_tx_buffer = mem_block };
|
||||
nrfx_i2s_buffers_t next = { 0 };
|
||||
|
||||
if (!get_next_tx_buffer(drv_data, &next)) {
|
||||
/* Log error because this is definitely unexpected.
|
||||
* Do not return error because the caller is no longer
|
||||
* responsible for releasing the buffer.
|
||||
*/
|
||||
LOG_ERR("Cannot reacquire queued buffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
drv_data->next_tx_buffer_needed = false;
|
||||
|
||||
LOG_DBG("Next TX %p", mem_block);
|
||||
LOG_DBG("Next TX %p", next.p_tx_buffer);
|
||||
|
||||
if (!supply_next_buffers(drv_data, &next)) {
|
||||
return -EIO;
|
||||
}
|
||||
} else {
|
||||
int ret = k_msgq_put(&drv_data->tx_queue,
|
||||
&mem_block,
|
||||
SYS_TIMEOUT_MS(drv_data->tx.cfg.timeout));
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
LOG_DBG("Queued TX %p", mem_block);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue