В этом тексте я написал про то как самому написать System Software уровня HAL для ARM Cortex-M4 совместимого микроконтроллера.
Пролог
Некоторые компании сами пишут своё system software базового уровня. Как его только не называют: MCAL, HAL, SPL, драйвера. Пишут сами даже вопреки тому, что этот код бесплатно и в открытом виде дают производители всех микроконтроллеров. Такой эрзац нужен из-за недоверия к стороннему коду.
Поэтому программисты-микроконтроллеров годами пишут эти драйверы самого низкого уровня для того чтобы пользоваться всеми подсистемами вмонтированными внутрь микроконтроллеров: CLOCK, INTERRUPTS, GPIO, FLASH, RTC, UART, PWM, TIMER, WATCDOG, ADC, SDIO, USB, SWD, PDM, DAC, CAN, I2C, SPI, I2S, DMA.
Словом, для подготовки полного комплекта MCAL для очередного семейства MCU работы нужно проделать море...
По сути это код-переходник, программный-клей между удобной красивой функцией вида
bool i2s_write(uint8_t num, uint8_t* const data, size_t size);
и чтением и записью сырых физических регистров, что живут в карте памяти данного микроконтроллера. Ниже кода чем HAL просто не бывает... Разве, что Verilog. В HAL всё сводится к банальному чтению и записи нужных битов внутри регистров и прокручиванию какого-нибудь простенького конечного автомата. Установил bit(тик) и внутри MCU начала бушевать и дрязгать какая-н цифровая цепь.
Вот и настало время теперь так же голыми руками запустить и аппаратный I2S трансивер на чипке AT32F437 от Artery Technology.
Теория
I2S - это синхронный, последовательный 4хпроводной физический полнодуплексный интерфейс для передачи цифрового звука в пределах одной PCB. Вот шпаргалка по I2S
binding(и) - это функции, которые просто вызывают другие функции. В переводе на кухонный язык - программный клей. Binding(и) нужны, чтобы связать два совершенно разных API. Вот пример функции binding(а)
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;
}
Каков план?
Я сейчас не буду писать binding(и) для вызова Artery HAL кода функциями с другими именами. Я буду сам писать внутренности, свой вариант HAL для I2S.
Чтобы запустить I2S на любом микроконтроллере надо выполнить вот эти действия.
1--Определить и выбрать GPIO пины, которые аппаратно поддерживают I2S2. В Artery (как и у STM) это далеко не каждый пин на корпусе микросхемы.
2--Подключить тактирование на I2S трансивер.
3--Настроить пред делители тактовой частоты, чтобы выставить целевую частоту дискретизации для звуковых семплов.
4--Активировать прерывания I2S контроллера в ARM Cortex M4 процессоре
5--Прописать корректные значения в регистры I2S интерфейса.
6--Активировать прерывание окончания отправки, прерывание окончания приема, прерывание ошибок в карте регистров трансивера от Artery
7--Определить Си функцию обработчик прерывания для I2S2 как Си-функцию.
Обо всем этом обычно можно даже не догадываться, когда пишешь прошивку на основе HAL от производителя, однако это в самом деле надо проделывать. Иначе ничего не заработает.
Что надо из оборудования?
Для запуска аудио подсистемы надо достаточно много оборудования:
№ |
Оборудование |
Количество |
1 |
Логический анализатор |
1 |
9 |
Осциллограф |
1 |
10 |
щупы для осциллографа |
4 |
2 |
учебно-треннировочная электронная плата AT-Start-F437 |
1 |
3 |
Кабель USB-A-USB-micro |
1 |
4 |
DMM |
1 |
5 |
Отладочная плата с аудиокодеком, например WM8731 |
1 |
6 |
перемычки гнездо-гнездо |
10+ |
7 |
наушники с audio jack 3.5 |
1 |
8 |
микрофон с audio jack 3.5 |
1 |
Фаза1: Подать тактирование на I2S2 подсистему.
В цифровой электронике всё надо тактировать. Без тактирования ничего не будет работать. Даже регистры I2S не сможете прописать, если нет тактирования на I2S контроллере. Активация тактирования происходит в регистре APB1EN в подсистеме тактирования CRM. Буквально одним битом. Также надо подать тактирование на нужный GPIO.
Фаза2: Переключить GPIO пины на альтернативную функцию.
Перед тем как приступить к I2S вы должны досконально понимать GPIO периферию. Что значит каждый бит в карте памяти GPIO. В частности в регистре GPIOx_CFGR происходит назначение первого мультиплексора PINMUX на альтернативную функцию. Надо переключить 5 пинов на I2S2.
Фаза3 Определить GPIO пины на I2S2.
При этом самих альтернативных функций для каждого GPIO пина может быть десятки. Поэтому надо переключить второй PINMUX мультиплексор GPIO на конкретную альтернативную функцию. В данном случае это I2S2. И так для каждого провода в шине I2S.
Затем надо в UART CLI убедиться, что Mux регистр в самом деле прописался.
Фаза 4: Определить регистры I2S периферии в нужные значения.
В программировании MCU достаточно только правильно выставить регистры и электрическая схема I2S внутри микросхемы заработает сама собой. Настроек у I2S много, но вот основные
№ |
параметр |
количество бит |
1 |
роль на шине |
2 |
2 |
разрядность семпла |
2 |
3 |
предделители частоты |
10 |
4 |
прерывание на прием |
1 |
5 |
прерывание на отправку |
1 |
6 |
полярность защелкивания бита |
1 |
7 |
прерывание на ошибку |
1 |
8 |
вывод частоты тактирования за корпус |
1 |
Тут самое сложное и не очевидное - это правильно настроить частоту дискретизации звуковых отсчетов Fs. Чтобы просто установить желаемой частоту надо правильно прописать аж 4 отдельных битовых поля в разных регистрах: I2SMCLKOE, I2SCBN, I2SDIV, I2SODD. Тут видно ещё и статические предделители 4 и 8. Надо их тоже учитывать в коде прошивки.
Вот так выглядит сырая карта регистров. Всего 9 регистров по 32 бита каждый. Чтобы правильно заработал I2S надо правильно прописать всего-навсего 288 бит
Вот так это выглядит детализация битовых полей в карте регистров I2S
Фаза 5: Определить обработчик прерывания для I2S2
В I2S прерывания происходят после отправки каждого канала. То есть с частотой 2xFs. Или каждые 32 бита. Если вы испускаете звук с частотой 48kHz, то прерывания будут происходить с частотой 96kHz.
В нашем случае обработчик прерываний - это функция 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);
}
В массиве векторов прерываний у ARM Cortex-M4 в исполнении AT32F437 прерывание для I2S2 имеет индекс 36. Также необходимо, чтобы в массиве векторов прерываний был прописан flash адрес на функцию обработчик прерывания для I2S2 (SPI2_I2S2EXT_IRQHandler).
Вот код ядра драйвера I2S
#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;
}
Отладка
Для проверки I2S я собрал вот такой прототип. Это двух ярусная сборка. Основа - это учебно-треннировочная электронная плата AT-Start-F437. Сверху примонтирована электронная плата с аудио кодеком WM8731.
Вот так выглядит вся аппаратура для отладки I2S в натуре
Лог начальной загрузки показал, что I2S2 успешно стартовал
Теперь остается только тут же в UART-CLI консоли попросить прошивку включить тестовый синус сигнал.
Пристегнув логический анализатор I2S я увидел, что данные в самом деле отправляются.
Звук было слышно в наушниках даже не надевая их.
Аналогично с записью. Я просто включаю на смартфоне тон 2500Hz, набираю в UART-CLI команду записи 5.3ms звука, вычисляю в прошивке DFT и смотрю пиковую частоту.
Получилось как раз 2437 Hz. Ошибка меньше 1 %. Значит запись звука тоже работает волшебно. Успех!
Итоги
Удалось самому написать MCAL для I2S на Artery MCU семейства AT32F4xx. В этом оказывается нет ничего сложного. Просто механика. Главное внимательно читать спеку на микроконтроллер и уметь корректно интерпретировать карту регистров в I2S адресов.
При том в отладке System SW очень удружила UART-CLI для ручного вызова функций из API I2S драйвера и чтения состояния софта и железа. UART-Shell как лакмусовая бумажка показывает текущее состояние софта и железа.
В сухом остатке примерно так и инициализируются любая подсистема на любом микроконтроллере. Где-то чуть сложнее где-то чуть проще, но в целом это очень обыкновенная и упорная работа.
Акроним |
Расшивровка |
API |
application programming interface |
MCAL |
Microcontroller Abstraction Layer |
DFT |
Discrete Fourier transform |
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 |
Ссылки
№ |
Название |
1 |
|
2 |
|
3 |
|
4 |
Почему Нам Нужен UART-Shell? (или Добавьте в Прошивку Гласность) |
5 |
|
6 |
|
7 |
|
8 |
Комментарии (12)
rukhi7
28.07.2024 12:30+1Если вы испускаете звук с частотой 48kHz, то прерывания будут происходить с частотой 96kHz.
вот тут есть смысл ДМА прикрутить чтобы понизить частоту прерываний, чтобы сэмплы звука в буфер ДМА пачками копировать, а не по одному в регистры I2S юнита. Ждем продолжения!
Где-то чуть сложнее где-то чуть проще, но в целом это очень обыкновенная и упорная работа.
в этом предложении я бы заменил слово обыкновенная на "необыкновенно муторная", может даже нудная, успехов!
aabzel Автор
28.07.2024 12:30DMA ещё позволит активировать полный дуплекс. На прерываниях только полудуплекс.
aabzel Автор
28.07.2024 12:30я бы заменил слово обыкновенная на "необыкновенно муторная", может даже нудная, успехов!
При правильной организации работ никакой нудности не должно быть
1--Когда есть UART-CLI , то ситуация с отладкой заметно улучшается и в разработке появляется азарт
2--Для каждого регистра можно определить си-шное битовое поле, и вам не придется использовать ни одного оператора побитового сдвига и эти нечитаемые маски. С битовыми полями и объединениями код драйвера получится лаконичным и наглядным.
3--Код HAL очень легко пишется по методике TDD.
aabzel Автор
28.07.2024 12:30Когда пишешь HAL, обычно находишь огромное количество опечаток в datasheet(ах)
beefdeadbeef
28.07.2024 12:30+1В случае artery (да и не только) полезно иметь раскрытым рядом RM
на какой-нибудь stm32f1 и/или f4 -- очень многое просто совпадает
на регистровом уровне, и ляпы в документации cтановятся очевидны.
Тут можно полистать историю коммитов и получить некоторое
представление о сходстве/различии с stm32:
https://github.com/beefdeadbeef/libopencm3/commits/at32f4/
pistoletov
28.07.2024 12:30+1Я на стм32 делал воспроизведение wav файла из флешки через дма. Алу вообще отдыхает. Кинул блок данных когда половина воспроизвелась первую половинку данных меняем на свежую через прерывание DMA half complete. Помню что тогда намучился c последовательностью байт. Вот не помню уже в чем нюанс был. Кажись какой байт раньше толкать младший или старший
aabzel Автор
28.07.2024 12:30+1Вообще писать нормальный hal должны специальные организации.
По стандарту.
Чтобы у всех микроконтроллеров была одинаковая сигнатура функций типа i2s_write()
NutsUnderline
28.07.2024 12:30+3чем дальше от железа тем хуже понимание процессов в нем происходящих. использование hal подобно списанию кода из методички. Для начинающих хорошо, когда работает сразу, но если что то идет не так то приходиться углубляться и ловить открытия чудные. или наоборот - не хватает функционала для раскрытия всех возможностей железа - и "готовые" функции допиливаются
beefdeadbeef
28.07.2024 12:30+2Artery, говорите ? Их есть у меня:
https://github.com/beefdeadbeef/f4uac -- началось как usb'шная звуковая карта, сейчас это, xм, усилок ? В комплекте допиленная libopencm3 для at32f40x/f42x.
https://github.com/beefdeadbeef/ecmit -- работоспособная болванка с сетью -- libopencm3, lwip, bugurtos
pistoletov
О коллега по Artery. В планах тоже есть запуск i2s с DMA. Так что статья в тему. Сейчас запустил Ethernet на ней с freertos стеком
aabzel Автор
Братство Artery. Может пригодится текст Настройка сборки прошивок для микроконтроллеров Artery из Makefile