Launching I2S transceiver on Artery

In this text I wrote about how to write HAL-level System Software for an ARM Cortex-M4 compatible microcontroller.

Prologue

Some companies write their own base-level system software. It's called by many names: MCAL, HAL, SPL. They write it themselves even though this code is provided free and in open form by all microcontroller manufacturers.

That's why microcontroller programmers have been writing these low-level drivers for years in order to use all the subsystems built into microcontrollers: CLOCK, INTERRUPTS, GPIO, FLASH, RTC, UART, PWM, TIMER, WATCDOG, ADC, SDIO, USB, SWD, PDM, DAC, CAN, I2C, SPI, I2S, DMA.

In short, there is a lot of work to be done to prepare a complete MCAL set for the next MCU family…

In essence, this is a code-adapter, a software-glue between a convenient beautiful function of the form

bool i2s_write(uint8_t num, uint8_t* const data, size_t size);

and reading and writing raw physical registers that live in the memory map of this microcontroller. There is simply no code lower than HAL… Except maybe Verilog. In HAL, everything comes down to the banal reading and writing of the necessary bits inside the registers and scrolling some simple finite state machine. Set the bit (tick) and inside the MCU some digital circuit starts raging and squabbling.

Now it's time to launch the hardware I2S transceiver on the AT32F437 chip from Artery Technology with bare hands.

Theory

I2S – is a synchronous, serial 4-wire physical full-duplex interface for transmitting digital audio within a single PCB. Here is a cheat sheet for I2S

Cheat sheet for I2S

Cheat sheet for I2S

binding(s) – are functions that simply call other functions. In kitchen language, they are software glue. Bindings are needed to connect two completely different APIs. Here is an example of a function binding(A)

bool i2s_write(uint8_t num, uint16_t* const array, uint16_t words) {
    bool res = false;
    LOG_DEBUG(I2S, "Write,Wait,i2s:%u,Words:%u", num, words);
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->tx_done = false ;
        HAL_StatusTypeDef ret = 0;
        ret = HAL_I2S_Transmit_DMA(&Node->i2s_h, 
                                   array, 
                                   words);
        if(HAL_OK == ret) {
            res = true;
        } else {
            LOG_ERROR(I2S, "WrErr:%u=%s", ret, HalStatus2Str(ret));
        }
    } else {
        LOG_ERROR(I2S, "I2S%u,NodeErr", num);
    }
    return res;
}

What's the plan?

We will not write bindings to call Artery HAL code with functions with other names. We will write the internals ourselves, our own version of HAL for I2S.

To run I2S on any microcontroller you need to perform these steps.

1–Identify and select GPIO pins that support I2S2 in hardware. In Artery (as well as in STM) this is not every pin on the chip case.

2–Connect clocking to I2S transceiver.

3–Adjust the clock prescalers to set the target sampling frequency for the audio samples.

4–Enable I2S controller interrupts in ARM Cortex M4 processor

5–Write the correct values ​​into the I2S interface registers.

6–Enable end of send interrupt, end of receive interrupt, error interrupt in transceiver register map from Artery

7–Define the interrupt handler C function for I2S2 as a C function.

You usually don't even have to guess about all this when writing firmware based on the manufacturer's HAL, but it really needs to be done. Otherwise, nothing will work.

What equipment do you need?

To run the audio subsystem you need quite a lot of equipment:

No.

Equipment

Quantity

1

Logic analyzer

1

9

Oscilloscope

1

10

oscilloscope probes

4

2

AT-Start-F437 training electronic board

1

3

USB-A-USB- cablemicro

1

4

DMM

1

5

Debug board with audio codec, for example WM8731

1

6

socket-to-socket jumpers

10+

7

headphones with audio jack 3.5

1

8

microphone with audio jack 3.5

1

Phase 1: Apply clock to I2S2 subsystem.

In digital electronics, everything needs to be clocked. Without clocking, nothing will work. You won't even be able to write I2S registers if there is no clocking on the I2S controller. Clocking is activated in the register APB1EN in the clocking subsystem CRM. Literally one bit. You also need to supply clock to the desired GPIO.

page 45

page 45

Phase2: Switch GPIO pins to alternative function.

Before you start with I2S you should thoroughly understand GPIO peripherals. What each bit in the GPIO memory map means. In particular in the register GPIOx_CFGR the first multiplexer is assigned PINMUX to an alternative function. You need to switch 5 pins to I2S2.

Phase3 Define GPIO pins on I2S2.

At the same time, there can be dozens of alternative functions for each GPIO pin. Therefore, it is necessary to switch the second PINMUX GPIO multiplexer to a specific alternative function. In this case, it is I2S2. And so for each wire in the I2S bus.

Then you need to make sure in the UART CLI that the Mux register is actually registered.

Phase 4: Set the I2S peripheral registers to the desired values.

In MCU programming, it is enough to set the registers correctly and the I2S electrical circuit inside the chip will work by itself. I2S has many settings, but here are the main ones

No.

parameter

number of bits

1

role on the tire

2

2

sample bit depth

2

3

frequency prescalers

10

4

interruption to reception

1

5

interrupt on send

1

6

bit latch polarity

1

7

error interrupt

1

8

clock frequency output outside the case

1

The most difficult and not obvious thing here is to correctly set the sampling frequency of the audio samples Fs. To simply set the desired frequency, you need to correctly write as many as 4 separate bit fields in different registers: I2SMCLKOE, I2SCBN, I2SDIV, I2SODD. Here you can also see the static prescalers 4 and 8. They must also be taken into account in the firmware code.

This is what the raw register map looks like. There are only 9 registers of 32 bits each. In order for I2S to work correctly, you only need to correctly write 288 bits

This is what the bit field detailing in the I2S register map looks like

Phase 5: Define interrupt handler for I2S2

In I2S, interrupts occur after each channel is sent. That is, at a frequency of 2xFs. Or every 32 bits. If you emit sound at a frequency of 48kHz, then interrupts will occur at a frequency of 96kHz.

In our case, the interrupt handler is a function SPI2_I2S2EXT_IRQHandler


static bool I2sIRQHandler(uint8_t num) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->it_cnt++;
        Node->it_done = true;
        gpio_toggle(Node->PadDebug1.byte);

        res = i2s_interrupt_flag_get_ll(Node->I2Sx, I2S_FLAG_RDBF);
        if(res) {
            Node->rx_buff_full = true;
            if(Node->rec) {
                Node->Rx.cnt++;
                bool overflow = false;
                Node->Rx.index = inc_index(Node->Rx.index, Node->Rx.size, &overflow);
                Node->Rx.overflow += overflow;
                uint16_t word = i2s_data_receive_ll(Node->I2Sx);
                Node->Rx.array[Node->Rx.index] = (SampleType_t)word;
                i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_RX_FULL, true);
                if(overflow) {
                    if(I2S_STATE_IDLE == Node->state) {
                        Node->rec = false;
                    }
                }
            } else {
                i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_RX_FULL, false);
            }
        }

        res = i2s_interrupt_flag_get_ll(Node->I2Sx, I2S_FLAG_TDBE);
        if(res) {
            res = i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_TX_EMPTY, true);
            Node->tx_buff_empty = true; // 555
            Node->tx_buff_empty_cnt++;
            Node->Tx.cnt++;
            bool overflow = false;
            Node->Tx.index = inc_index(Node->Tx.index, Node->Tx.size, &overflow);
            Node->Tx.overflow += overflow;
            if(Node->Tx.array) {
                if(Node->play) {
                    i2s_data_transmit_ll(Node->I2Sx, Node->Tx.array[Node->Tx.index]);
                } else {
                    i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_TX_EMPTY, false);
                }
            } else {
                i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_TX_EMPTY, false);
            }
        }
    }
  ......
    return res;
}

void SPI2_I2S2EXT_IRQHandler(void) { 
  I2sIRQHandler(2); 
}

In the interrupt vector array of the ARM Cortex-M4 in the AT32F437 implementation, the interrupt for I2S2 has index 36. It is also necessary that the Alash address for the interrupt handler function for I2S2 (SPI2_I2S2EXT_IRQHandler) be registered in the interrupt vector array.

Here is the I2S driver core code
#include "i2s_mcal.h"

#include "clock.h"
#include "code_generator.h"
#include "gpio_mcal.h"
#include "i2s_custom_types.h"
#include "i2s_register_types.h"
#include "log.h"
#include "mcal_types.h"
#include "sys_config.h"

static I2sBitLen_t I2sDataFormatToArtery(I2sDataFormat_t data_format) {
    I2sBitLen_t data_length = I2SDBN_UNDEF;
    switch(data_format) {
    case I2S_DATA_FORMAT_8B:
        data_length = I2SDBN_UNDEF;
        break;
    case I2S_DATA_FORMAT_16B:
        data_length = I2SDBN_16BIT;
        break;
    case I2S_DATA_FORMAT_16B_EXTENDED:
        data_length = I2SDBN_16BIT;
        break;
    case I2S_DATA_FORMAT_24B:
        data_length = I2SDBN_24BIT;
        break;
    case I2S_DATA_FORMAT_32B:
        data_length = I2SDBN_32BIT;
        break;
    default:
        data_length = OPERSEL_UNDEF;
        break;
    }
    return data_length;
}

static I2sDataFormat_t I2sArteryToDataFormat(I2sBitLen_t data_length) {
    I2sDataFormat_t data_format = I2S_DATA_FORMAT_UNDEF;
    switch(data_length) {
    case I2SDBN_16BIT:
        data_format = I2S_DATA_FORMAT_16B;
        break;
    case I2SDBN_24BIT:
        data_format = I2S_DATA_FORMAT_24B;
        break;
    case I2SDBN_32BIT:
        data_format = I2S_DATA_FORMAT_32B;
        break;
    default:
        data_format = I2S_DATA_FORMAT_UNDEF;
        break;
    }
    return data_format;
}

static I2sOperation_t I2sRoleToArtery(I2sRole_t bus_role) {
    I2sOperation_t operation = OPERSEL_UNDEF;
    switch(bus_role) {
    case I2SMODE_SLAVE:
        operation = OPERSEL_SLAVE_RX;
        break;
    case I2SMODE_SLAVE_TX:
        operation = OPERSEL_SLAVE_TX;
        break;
    case I2SMODE_SLAVE_RX:
        operation = OPERSEL_SLAVE_RX;
        break;
    case I2SMODE_MASTER:
        operation = OPERSEL_MASTER_TX;
        break;
    case I2SMODE_MASTER_TX:
        operation = OPERSEL_MASTER_TX;
        break;
    case I2SMODE_MASTER_RX:
        operation = OPERSEL_MASTER_RX;
        break;
    default:
        operation = OPERSEL_MASTER_TX;
        break;
    }
    return operation;
}

static I2sRole_t I2sArteryToRole(I2sOperation_t operation) {
    I2sRole_t role = I2SMODE_UNDEF;
    switch(operation) {
    case OPERSEL_SLAVE_RX:
        role = I2SMODE_SLAVE_RX;
        break;
    case OPERSEL_SLAVE_TX:
        role = I2SMODE_SLAVE_TX;
        break;
    case OPERSEL_MASTER_TX:
        role = I2SMODE_MASTER_TX;
        break;
    case OPERSEL_MASTER_RX:
        role = I2SMODE_MASTER_RX;
        break;
    default:
        role = I2SMODE_UNDEF;
        break;
    }
    return role;
}

bool i2s_div_get(uint8_t num, uint16_t* division) {
    bool res = false;
    I2sDiv_t Div;
    Div.division = 0;
    const I2sInfo_t* Info = I2sGetInfo(num);
    if(Info) {
        if(division) {
            Div.division_7_0 = Info->I2Sx->SPI_I2SCLKP.I2SDIV1;
            Div.division_11_10 = Info->I2Sx->SPI_I2SCLKP.I2SDIV2;
            LOG_DEBUG(I2S, "I2S:%u,Div:%u", num, Div.division);
            *division = Div.division;
            res = true;
        }
    }
    return res;
}

static I2sStandart_t I2sStandardToArtery(Standard_t standard) {
    I2sStandart_t artery_std = I2SCLKPOL_UNDEF;
    switch(standard) {
    case I2S_STD_PHILIPS:
        artery_std = STDSEL_PHILIPS;
        break;
    case I2S_STD_MSB:
        artery_std = STDSEL_RIGHT_ALIGNED;
        break;
    case I2S_STD_LSB:
        artery_std = STDSEL_LEFT_ALIGNE;
        break;
    case I2S_STD_PCM_SHORT:
        artery_std = STDSEL_PCM;
        break;
    case I2S_STD_PCM_LONG:
        artery_std = STDSEL_PCM;
        break;
    default:
        break;
    }
    return artery_std;
}

const Reg32_t I2sReg[] = {
    {
        .valid = true,
        .name = "SPI_CTRL1",
        .offset = 0x00,
    },
    {
        .valid = true,
        .name = "SPI_CTRL2",
        .offset = 0x04,
    },
    {
        .valid = true,
        .name = "SPI_STS",
        .offset = 0x08,
    },
    {
        .valid = true,
        .name = "SPI_DT",
        .offset = 0x0C,
    },
    {
        .valid = true,
        .name = "SPI_CPOLY",
        .offset = 0x10,
    },
    {
        .valid = true,
        .name = "SPI_RCRC",
        .offset = 0x14,
    },
    {
        .valid = true,
        .name = "SPI_TCRC",
        .offset = 0x18,
    },
    {
        .valid = true,
        .name = "SPI_I2SCTRL",
        .offset = 0x1C,
    },
    {
        .valid = true,
        .name = "SPI_I2SCLKP",
        .offset = 0x20,
    },

};

const static I2sInfo_t I2sInfo[] = {
#ifdef HAS_I2S1
    {
        .num = 1,
        .valid = true,
        .I2Sx = SPI1,
        .clock_bus = BUS_APB2,
        .irq_n = SPI1_IRQn,
        .clock_type = CLOCK_PERIPH_CLOCK_I2S1,
    },
#endif

#ifdef HAS_I2S2
    {
        .num = 2,
        .valid = true,
        .I2Sx = (I2sRegMap_t*)0x40003800,
        .clock_bus = BUS_APB1,
        .irq_n = SPI2_I2S2EXT_IRQn,
        .clock_type = CLOCK_PERIPH_CLOCK_I2S2,
    },
#endif

#ifdef HAS_I2S3
    {
        .num = 3,
        .valid = true,
        .I2Sx = I2S3EXT,
        .clock_bus = BUS_APB1,
        .irq_n = SPI3_I2S3EXT_IRQn,
        .clock_type = CLOCK_PERIPH_CLOCK_I2S3,
    },
#endif

#ifdef HAS_I2S4
    {
        .num = 4,
        .valid = true,
        .I2Sx = SPI4,
        .clock_bus = BUS_APB2,
        .irq_n = SPI4_IRQn,
        .clock_type = CLOCK_PERIPH_CLOCK_I2S4,
    },
#endif

};

COMPONENT_GET_INFO(I2s)

uint32_t i2s_reg_cnt(void) {
    uint32_t cnt = ARRAY_SIZE(I2sReg);
    return cnt;
}

/*The prescaler of the CK depends on whether to provide the main clock for peripherals. To ensure that
the main clock is always 256 times larger than the audio sampling frequency, the channel bits should be
taken into account. When the main clock is needed, the CK should be divided by 8 (I2SCBN=0) or 4
(I2SCBN=1), then divided again by the same prescaler as that of the MCK, that is the final
communication clock; When the main clock is not needed, the prescaler of the CK is determined by
I2SDIV and I2SODD, shown in Figure 13-13.*/
static uint32_t I2sChannelBitToDivider(I2sChannelBitNum_t i2scbn) {
    uint32_t cbn_divider = 1;
    switch((uint32_t)i2scbn) {
    case I2SCBN_16BIT_WIDE: {
        cbn_divider = 8;
    } break;
    case I2SCBN_32BIT_WIDE: {
        cbn_divider = 4;
    } break;
    default:
        break;
    }
    return cbn_divider;
}

bool i2s_ctrl_ll(I2sRegMap_t* const I2Sx, bool on) {
    bool res = false;
    if(I2Sx) {
        I2Sx->SPI_I2SCTRL.I2SEN = on;
        I2Sx->SPI_I2SCTRL.I2SMSEL = I2SMSEL_I2S;
        res = true;
    }
    return res;
}

bool i2s_ctrl_l(I2sHandle_t* const Node, bool on) {
    bool res = false;
    if(Node) {
        res = i2s_ctrl_ll(Node->I2Sx, on);
    }
    return res;
}

bool i2s_ctrl(uint8_t num, bool on_off) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->I2Sx->SPI_I2SCTRL.I2SEN = on_off;
        Node->I2Sx->SPI_I2SCTRL.I2SMSEL = I2SMSEL_I2S;
        res = true;
    }
    return res;
}

bool i2s_dma_stop(uint8_t num) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->state = I2S_STATE_OFF;
        res = i2s_ctrl_l(Node, false);
        LOG_INFO(I2S, "Stop");
    }
    return res;
}

static I2sClockPolatity_t I2sClockPolarityToArtery(Cpol_t cpoll) {
    I2sClockPolatity_t clock_polar = I2SCLKPOL_UNDEF;
    switch(cpoll) {
    case I2S_CLOCK_POL_LOW:
        clock_polar = I2SCLKPOL_LOW;
        break;
    case I2S_CLOCK_POL_HIGH:
        clock_polar = I2SCLKPOL_HIGH;
        break;
    default:
        break;
    }
    return clock_polar;
}

bool i2s_data_transmit_ll(I2sRegMap_t* const I2Sx, uint16_t tx_data) {
    bool res = false;
    if(I2Sx) {
        I2Sx->SPI_DT.DT = tx_data;
        res = true;
    }
    return res;
}

uint16_t i2s_data_receive_ll(I2sRegMap_t* const I2Sx) {
    uint16_t word = 0;
    if(I2Sx) {
        word = (uint16_t)I2Sx->SPI_DT.DT;
    }
    return word;
}

bool i2s_standard_set(uint8_t num, Standard_t standard) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->I2Sx->SPI_I2SCTRL.STDSEL = I2sStandardToArtery(standard);
        res = true;
    }
    return res;
}

// see Figure 13-22 CK & MCK source in master mode
static uint32_t i2s_extra_divider_get_ll(I2sRegMap_t* const I2Sx) {
    uint32_t extra_div = 1;
    U32 i2smclkoe = I2Sx->SPI_I2SCLKP.I2SMCLKOE;
    if(i2smclkoe) {
        extra_div = I2sChannelBitToDivider(I2Sx->SPI_I2SCTRL.I2SCBN);
    } else {
        extra_div = 1;
    }
    return extra_div;
}

bool i2s_clock_polarity_set(uint8_t num, Cpol_t cpoll) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->I2Sx->SPI_I2SCTRL.I2SCLKPOL = I2sClockPolarityToArtery(cpoll);
        res = true;
    }
    return res;
}

static bool i2s_pinmux(uint8_t num, I2sRole_t bus_role) {
    bool res = false;
    LOG_INFO(I2S, "I2S_%u,Set,PinMux,BusRole:%s", num, I2sBusRole2Str(bus_role));
    const I2sConfig_t* Config = I2sGetConfig(num);
    if(Config) {
        res = true;
        switch((uint32_t)bus_role) {
        case I2SMODE_SLAVE: {
            res = false;
        } break;
        case I2SMODE_MASTER: {
            res = false;
        } break;

        case I2SMODE_SLAVE_RX:
        case I2SMODE_MASTER_RX: {
            res = gpio_init_one(&Config->GpioSdIn);
            res = gpio_deinit_one(Config->GpioSdOut.pad) && res;
        } break;

        case I2SMODE_SLAVE_TX:
        case I2SMODE_MASTER_TX: {
            res = gpio_init_one(&Config->GpioSdOut);
            res = gpio_deinit_one(Config->GpioSdIn.pad) && res;
        } break;
        default:
            res = false;
            break;
        }
    }
    return res;
}

bool i2s_bus_role_set(uint8_t num, I2sRole_t bus_role) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        res = i2s_pinmux(num, bus_role);
        Node->I2Sx->SPI_I2SCTRL.OPERSEL = I2sRoleToArtery(bus_role);
        res = true;
    }
    return res;
}

bool i2s_bus_role_get(uint8_t num, I2sRole_t* const bus_role) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        *bus_role = I2sArteryToRole(Node->I2Sx->SPI_I2SCTRL.OPERSEL);
        res = true;
    }
    return res;
}

static uint32_t I2sBitToDivider(I2sChannelBitNum_t i2s_cbn) {
    uint32_t bit_divider = 1;
    switch((uint32_t)i2s_cbn) {
    case I2SCBN_16BIT_WIDE:
        bit_divider = 16;
        break;
    case I2SCBN_32BIT_WIDE:
        bit_divider = 32;
        break;
    default:
        break;
    }
    return bit_divider;
}

static uint32_t I2sOddToDivider(I2sOddFactor_t i2s_odd) {
    uint32_t odd_divider = 1;
    switch((uint32_t)i2s_odd) {
    case I2SODD_EVEN:
        odd_divider = 2;
        break;
    case I2SODD_ODD:
        odd_divider = 3;
        break;
    default:
        break;
    }
    return odd_divider;
}

#define CHAN_CNT 2
// see 13.3.5 I2S_CLK controller
bool i2s_sample_freq_set(uint8_t num, AudioFreq_t audio_freq) {
    bool res = false;
    const I2sInfo_t* Info = I2sGetInfo(num);
    if(Info) {
        uint32_t base_freq_hz = clock_freq_get(Info->clock_bus);
        I2sHandle_t* Node = I2sGetNode(num);
        if(Node) {
            // see Figure 13-22 CK & MCK source in master mode
            uint32_t odd_div = I2sOddToDivider(Node->I2Sx->SPI_I2SCLKP.I2SODD);
            uint32_t extra_div = i2s_extra_divider_get_ll(Node->I2Sx);
            uint32_t bit_div = I2sBitToDivider(Node->I2Sx->SPI_I2SCTRL.I2SCBN);
            I2sDiv_t Div;
            uint32_t bit_ckock_hz = audio_freq * bit_div;
            Div.division = base_freq_hz / (bit_ckock_hz * CHAN_CNT * extra_div + odd_div);
            LOG_INFO(I2S, "BaseFreqHz:%u Hz,SampleFreq:%u Hz,Div:%u", base_freq_hz, audio_freq, Div.division);
            Node->I2Sx->SPI_I2SCLKP.I2SDIV1 = Div.division_7_0;
            Node->I2Sx->SPI_I2SCLKP.I2SDIV2 = Div.division_11_10;
            // Node->I2Sx->SPI_I2SCLKP.I2SODD = I2SODD_EVEN;
            res = true;
        }
    }

    return res;
}

// see 13.3.5 I2S_CLK controller
bool i2s_sample_freq_get(uint8_t num, uint32_t* const audio_freq) {
    bool res = false;
    const I2sInfo_t* Info = I2sGetInfo(num);
    if(Info) {
        uint32_t base_freq_hz = clock_freq_get(Info->clock_bus);

        I2sHandle_t* Node = I2sGetNode(num);
        if(Node) {
            uint16_t division = 0;
            res = i2s_div_get(num, &division);
            // see Figure 13-22 CK & MCK source in master mode
            uint32_t extra_div = i2s_extra_divider_get_ll(Node->I2Sx);
            uint32_t odd_div = I2sOddToDivider(Node->I2Sx->SPI_I2SCLKP.I2SODD);
            uint32_t bit_div = I2sBitToDivider(Node->I2Sx->SPI_I2SCTRL.I2SCBN);
            uint32_t clock_hz = 0;
            clock_hz = base_freq_hz / (2 * division * extra_div * bit_div + odd_div);
            LOG_PARN(I2S, "SCLK:%uHz,CK:%u Hz", base_freq_hz, clock_hz);
            *audio_freq = clock_hz;
            res = true;
        }
    }

    return res;
}

bool i2s_data_format_set(uint8_t num, I2sDataFormat_t data_format) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->I2Sx->SPI_I2SCTRL.I2SDBN = I2sDataFormatToArtery(data_format);
        Node->I2Sx->SPI_I2SCTRL.I2SCBN = I2SCBN_32BIT_WIDE;
        res = true;
    }

    return res;
}

bool i2s_config_tx(uint8_t num, I2sDataFormat_t word_size, uint8_t channels, AudioFreq_t audio_freq) {
    bool res = true;
    (void)channels;
    res = i2s_sample_freq_set(num, audio_freq) && res;
    res = i2s_data_format_set(num, word_size) && res;
    return res;
}

bool i2s_master_clock_ctrl(uint8_t num, MclkOut_t mclk_out) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        Node->I2Sx->SPI_I2SCLKP.I2SMCLKOE = (I2sMasterClockOut_t)mclk_out;
        res = true;
    }
    return res;
}

bool i2s_data_format_get(uint8_t num, I2sDataFormat_t* data_format) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        *data_format = I2sArteryToDataFormat(Node->I2Sx->SPI_I2SCTRL.I2SDBN);
        res = true;
    }

    return res;
}

uint8_t i2s_sample_size_get(uint8_t num) {
    uint8_t sample_size_bit = 0;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        switch(Node->I2Sx->SPI_I2SCTRL.I2SDBN) {
        case I2SDBN_16BIT:
            sample_size_bit = 16;
            break;
        case I2SDBN_24BIT:
            sample_size_bit = 26;
            break;
        case I2SDBN_32BIT:
            sample_size_bit = 32;
            break;
        default:
            sample_size_bit = 0;
            break;
        }
    }

    return sample_size_bit;
}

bool i2s_interrupt_ctrl_ll(I2sRegMap_t* const I2Sx, I2sInterrupt_t i2s_interrupt, bool on_off) {
    bool res = false;
    if(I2Sx) {
        switch(i2s_interrupt) {
        case I2S_INTERRUPT_ERROR: {
            I2Sx->SPI_CTRL2.ERRIE = on_off;
            res = true;
        } break;
        case I2S_INTERRUPT_RX_FULL: {
            I2Sx->SPI_CTRL2.RDBFIE = on_off;
            res = true;
        } break;
        case I2S_INTERRUPT_TX_EMPTY: {
            I2Sx->SPI_CTRL2.TDBEIE = on_off;
            res = true;
        } break;
        default: {
            res = false;
        } break;
        }
        res = true;
    }
    return res;
}

bool i2s_interrupt_ctrl_l(I2sHandle_t* Node, I2sInterrupt_t i2s_interrupt, bool on_off) {
    bool res = false;
    res = i2s_interrupt_ctrl_ll(Node->I2Sx, i2s_interrupt, on_off);
    return res;
}

bool i2s_interrupt_ctrl(uint8_t num, I2sInterrupt_t i2s_interrupt, bool on_off) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        res = i2s_interrupt_ctrl_l(Node, i2s_interrupt, on_off);
    }
    return res;
}

static bool i2s_clock_ctrl(ClockBus_t clock_bus, uint32_t clock_type, bool on_off) {
    bool res = false;
    LOG_INFO(I2S, "ClkBus:%u,Mask:0x%x", clock_bus, clock_type);
    if(on_off) {
        switch((uint32_t)clock_bus) {
        case BUS_APB1: {
            CRM.APB1EN.R |= clock_type;
        } break;
        case BUS_APB2: {
            CRM.APB2EN.R |= clock_type;
        } break;
        }
    } else {
        switch((uint32_t)clock_bus) {
        case BUS_APB1: {
            CRM.APB1EN.R &= ~clock_type;
        } break;
        case BUS_APB2: {
            CRM.APB2EN.R &= ~clock_type;
        } break;
        }
    }
    return res;
}

/*
 * samples mast be even
 */
bool i2s_api_read(uint8_t num, SampleType_t* const array, size_t samples) {
    bool res = false;
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        I2sRole_t bus_role = I2SMODE_UNDEF;
        res = i2s_bus_role_get(num, &bus_role);
        if(I2SMODE_MASTER_RX != bus_role) {
            res = i2s_bus_role_set(num, I2SMODE_MASTER_RX);
        }

        LOG_INFO(I2S, "I2S%u,Read:%u Sam", num, samples);
        Node->state = I2S_STATE_REC;
        Node->rec = true;
        Node->Rx.index = 0;
        Node->Rx.size = samples;
        Node->Rx.array = array;
        res = i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_RX_FULL, true);
        res = i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_TX_EMPTY, false);
        i2s_data_receive_ll(Node->I2Sx);
        res = i2s_ctrl_ll(Node->I2Sx, true);
        res = true;
    }
    return res;
}

bool i2s_api_write(uint8_t num, SampleType_t* const array, size_t size) {
    bool res = false;
    LOG_INFO(I2S, "Write,I2S_%u,Words:%u", num, size);
    I2sHandle_t* Node = I2sGetNode(num);
    if(Node) {
        I2sRole_t bus_role = I2SMODE_UNDEF;
        res = i2s_bus_role_get(num, &bus_role);
        if(I2SMODE_MASTER_TX != bus_role) {
            res = i2s_bus_role_set(num, I2SMODE_MASTER_TX);
        }

        Node->state = I2S_STATE_RUN;
        Node->Tx.array = array;
        Node->Tx.size = size * 2; /*1 sample 2 channel*/
        Node->Tx.index = 0;
        Node->play = true;
        res = i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_RX_FULL, false);
        res = i2s_interrupt_ctrl_ll(Node->I2Sx, I2S_INTERRUPT_TX_EMPTY, true);
        res = i2s_data_transmit_ll(Node->I2Sx, array[0]);
        res = i2s_ctrl_ll(Node->I2Sx, true);
        res = true;
    }
    return res;
}

bool i2s_init_one(uint8_t num) {
    bool res = false;
    const I2sConfig_t* Config = I2sGetConfig(num);
    if(Config) {
        LOG_WARNING(I2S, "%s", I2sConfigToStr(Config));
        const I2sInfo_t* Info = I2sGetInfo(num);
        if(Info) {
            I2sHandle_t* Node = I2sGetNode(num);
            if(Node) {
                res = i2s_clock_ctrl(Info->clock_bus, Info->clock_type, true);

                Node->I2Sx = Info->I2Sx;
                res = i2s_init_common(Config, Node);
                uint32_t base_freq_hz = clock_freq_get(Info->clock_bus);
                LOG_INFO(I2S, "BaseFreqHz:%u Hz", base_freq_hz);

                res = i2s_master_clock_ctrl(num, Config->mclk_out);
                res = i2s_standard_set(num, Config->standard);
                res = i2s_clock_polarity_set(num, Config->cpol);
                res = i2s_bus_role_set(num, Config->bus_role);
                res = i2s_data_format_set(num, Config->data_format);
                res = i2s_sample_freq_set(num, Config->audio_freq);

                if(Config->interrupt_on) {
                    res = i2s_interrupt_ctrl(num, I2S_INTERRUPT_ERROR, true);
                    res = i2s_interrupt_ctrl(num, I2S_INTERRUPT_RX_FULL, true);
                    res = i2s_interrupt_ctrl(num, I2S_INTERRUPT_TX_EMPTY, true);
                    NVIC_SetPriority(Info->irq_n, Config->irq_priority);
                    NVIC_EnableIRQ(Info->irq_n);
                }
                res = i2s_ctrl_ll(Node->I2Sx, false);
            }
        }
    }
    return res;
}

bool i2s_init_custom(void) {
    bool res = false;
    return res;
}

Debugging

To test I2S I assembled this prototype. It is a two-tier assembly. The base is a training electronic board AT-Start-F437An electronic board with an audio codec is mounted on top. WM8731.

This is what all the hardware for debugging I2S looks like in real life

The boot log showed that I2S2 started successfully.

Now all that remains is to ask the firmware to turn on the test sine signal right here in the UART-CLI console.

By attaching an I2S logic analyzer I could see that data was indeed being sent.

The sound could be heard through the headphones even without putting them on.

The same with recording. I just turn on the 2500Hz tone on my smartphone, type in the UART-CLI the command to record 5.3ms of sound, calculate the DFT in the firmware and look at the peak frequency.

It turned out to be exactly 2437 Hz. The error is less than 1%. So the sound recording also works magically. Success!

Results

I managed to write MCAL for I2S on Artery MCU of AT32F4xx family. It turns out there is nothing complicated about it. Just mechanics. The main thing is to carefully read the spec for the microcontroller and be able to correctly interpret the register map in I2S addresses.

In debugging System SW, UART-CLI was very helpful for manually calling functions from the I2S driver API and reading the state of the software and hardware. UART-Shell, like a litmus test, shows the current state of the software and hardware.

In the end, this is how any subsystem on any microcontroller is initialized. Somewhere it is a little more complicated, somewhere a little easier, but overall it is very ordinary and persistent work.

Acronym

Unpacking

API

application programming interface

MCAL

Microcontroller Abstraction Layer

GPIO

General-purpose input/output

UART

universal asynchronous receiver-transmitter

CLI

command-line interface

MCU

Microcontroller

ARM

Advanced RISC Machines

AT

Artery Technology

I2S

Inter-Integrated Circuit Sound

Links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *