Pull to refresh

Высокоскоростной SPI to Ethernet конвертер, при чем здесь DMA?

Level of difficultyMedium
Reading time13 min
Views4.5K

Чисто техническое, но не очень глубокое описание реализованной задачи с самыми простыми расчетами. Надеюсь, будет полезно соответствующим техническим специалистам или для тех, кто хочет понимать что-то про объем знаний необходимых для использования DMA над некоторым устройством периферии, например SPI.

Относительно использования Ethernet, дальше нескольких упоминаний речь не идет. Как-то к слову не пришлось, еще пока, не обессудьте.

Сначала сформулируем задачу.

У нас есть устройство, которое принимает примерно 2 Мега байта упакованных данных в секунду из эфира (например, но не важно откуда). У этого устройства есть штатный интерфейс управления, скажем UART со скоростью примерно 500 килобит в секунду, то есть примерно 50 килобайт в секунду и есть какой-то рабочий выход цифрового звука, который тоже нельзя использовать в качестве диагностического выхода (точнее можно, но очень сложно и лишь частично, скорости тоже не хватает). Нам надо протестировать работу этого устройства с точки зрения корректности приема основного трафика данных на выходе аппаратной части (в регистрах с принятыми 100х-килобайтными фреймами, они порядка 100–300-килобайт, потому что они приходят несколько раз в секунду).

Общая схема коммуникации устройств
Общая схема коммуникации устройств

Такое устройство по объему сравнимо с двумя спичечными коробками и даже меньше, поэтому нормальный диагностический-отладочный интерфейс к нему приделать невозможно, нам позволили вывести 4 (четыре!) контакта SPI шины для диагностики.

SPI сигналы, все четыре
SPI сигналы, все четыре

То есть у нас есть 4 контакта для сигналов которые изображены на рисунке: такты-синхроимпульсы – SPCK, сигналы данных в двух направлениях: MOSI, MISO, выбор слейва – NSS, с которым вообще говоря была отдельная история так как он не стабильно обрабатывается SPI контроллером на частоте синхронизации которая приближается к частоте работы процессора, но это отдельная история, на которую мы не будем сейчас отвлекаться.

Если кого-то интересуют конструктивные вопросы – во время диагностики, устройства крепятся на переходную плату, на которой длина проводников SPI шины не превышает 5см.

На общей схеме коммуникации устройств цветом выделены места, в которых добавляется соответствующее программное обеспечение для реализации указанной функции диагностики. Перечислим какое ПО мы должны написать, и где:

  1. Для Target Device мы должны написать драйвер SPI master

  2. Для нашего конвертера (SPI to Ethernet Converter) мы должны реализовать прошивку микроконтроллера полностью, с функциями трансляции данных из TCP/IP сокета в SPI и такую же трансляцию в обратном направлении. Также нам не помешает специальный сокет для управления и диагностики самого конвертера.

  3. Для управления конвертором и, через него тестируемым устройством (Target Device) на ПК мы должны реализовать программу, которая как минимум умеет.

    a. посылать команды на включение и выключение трансляции данных в диагностический (SPI) порт.

    b. посылать Команды на запрос состояния конвертера

    c. сохранять и/или визуализировать диагностические данные или данные потока полученные с тестируемого устройства

    d. вообще говоря, при таком объеме собираемого трафика – до 2 Мбайт в секунду, нужен какой-то специальный софт для анализа этого трафика, но такой софт лучше делать в виде отдельной компоненты или даже отдельного приложения и у нас такое приложение конечно появилось, но это совсем другая история.

 Собственно главный вопрос заключался в том, можно ли по SPI организовать полноценную дуплексную связь с ПК со скоростью 2 Мега байта в секунду?

Оказалось, что можно. Но тут не обойтись без DMA. Расскажу идею.

Дуплексная связь девайса с ПК со скоростью 2 Мега байта в секунду

Начнем с расчетов, в нашем случае очень важны конкретные цифры-числа! Итак, 2 Мегабайта в секунду это, примерно, 2 * 10 * 1000 * 1000 бит в секунду. Конечно, в байте 8 бит, но SPI контроллер требует наличие паузы между посылками байтов, ее длительность я предлагаю считать равной длительности двух бит, это в принципе очень близко к фактическим значениям, которые мы получили на практике. 20 Мега бит в секунду определяют тактовую частоту импульсов синхронизации SPI шины (частота SPCK импульсов на рисунке), количество бит нам напрямую дает количество периодов в секунду, поэтому наша искомая тактовая частота SPI шины тоже будет 20 Мега Герц.

Чтобы обеспечить такую частоту сигнала синхронизации SPI нам нужен процессор, у которого тактовая частота была бы еще в 3 (три, минимум!) раза больше, чем частота тактов SPI, хотя как известно желательно отойти от этого минимального значения для надежности.

Отступление для тех, кто знает и помнит про теорему Котельникова-Найквиста, нам не нужно исследовать какую-нибудь форму сигнала, нам надо в течение каждого периода тактовой частоты импульсов синхронизации SPI считать со входа значение бита. Там, насколько я помню, делается 3 выборки по фронту очередного тактового импульса. 

Слава богу на интерфейсной плате у нас был запаян Cortex M7 ARM с частотой аж 300 Мега Герц. Я еще помню времена, когда какой-нибудь Windows 3.1 работал на процессорах с меньшей частотой! В те времена этот микроконтроллер вполне мог претендовать на звание суперкомпьютер, шутка.

Нам надо принимать данные с последовательного синхронного порта SPI и отправлять эти данные в Ethernet порт, а точнее в один из открытых TCP/IP сокетов, которые у нас реализованы через LWIP над встроенным Ethernet контроллером внутри нашего ARM микроконтроллера, назовем этот сокет сокетом данных. И наоборот, данные, которые приходят из сокета данных, нам надо положить в буфер отправки данных по SPI. SPI у нас работает в слейв (slave) режиме, но устройство, которое подключено к нам по SPI как мастер постоянно передает нам байты таким образом и при таких настройках частоты SPI что мы получаем примерно 2 Мега байта данных в секунду (и, соответственно, должны отправлять 2Мбайта). Наш протокол обмена данными по SPI определяет что даже когда мастер-устройству нечего посылать нам в наше слейв-устройство, мастер-устройство обязано слать нам IDLE байты. Таким образом через SPI, по сути через 3-4 провода(не учитывая питание) организована полноценная дуплексная связь между двумя устройствами с возможностью передачи и приема 2 Мега байт в секунду в любую сторону (на самом деле система тестировалась даже на скорости до 2.5 Мега байт в секунду).

Задача для DMA

C SPI контроллером на нашем АРМ-процессоре есть одна проблема, в SPI контроллере не реализована буферизация. Получать принятые байты мы можем только включив соответствующие прерывания. Давайте посчитаем сколько мы получим вызовов функции прерывания в секунду при заданной скорости поступления байт-данных через SPI. Как мы помним нам надо принимать порядка 2-х миллионов байт в секунду, соответственно мы получим 2 миллиона срабатываний SPI прерывания на прием. Если бы мы знали сколько тактов процессору нужно на полное выполнение функции SPI прерывания мы бы смогли оценить, какой процент времени процессор будет проводить внутри SPI прерывания, то есть какую часть своей производительности процессор будет тратить только на копирование данных из регистра SPI в память. Если написать код такой функции, то легко можно оценить ее длительность:

void SPI0_Handler(void)
{
    register uint8_t tmp = SPI0->SPI_RDR;
    spiRcvBufPtr[spiIndx++ & (spiRcvBufSize - 1)] = tmp;
    spiCnt++;
}

Здесь мы читаем значение принятого байта из SPI регистра в переменную tmp, и сохраняем это значение в циркулярный буфер spiRcvBufPtr, длина которого кратна степени двойки поэтому мы можем применить операцию очистки старших разрядов для самого эффективного вычисления индекса в буфере при сохранении байта. Переменная счетчик spiCnt нужна для контроля что буфер не переполнился, она должна соответственно уменьшаться фоновой программой по мере обработки-вычитывания(не путать со Считыванием, я сам все время путаю, когда перечитываю) данных из буфера. Фоновая программа должна будет блокировать прерывание во время изменения значения переменной spiCnt.

Я не буду вдаваться в детали расчета длительности выполнения такой функции в тактах, я предлагаю вам поверить мне на слово, что эту длительность можно оценить примерно в 40 тактов, с небольшим запасом, в расчете на то, что функция может оказаться чуть более сложной.

Я надеюсь, вы помните, что наш процессор работает на частоте 300 МГерц, это значит у нас есть 300 миллионов тактов в секунду на все операции-вычисления, реализованные в коде, который послушно исполняет наш процессор. Если наша функция сохранения принятых SPI байтов в циркулярный буфер будет вызвана 2 миллиона раз в секунду, она съест 2 * 40 = 80 тактов из каждых 300 тактов процессора, это значит что: просто прерывания приема SPI заберут у процессора 80 / 300 * 100 % = 26.6% производительности. Но нам надо помнить, что кроме приема мы должны еще поддерживать функцию передачи и все вместе, вполне может скушать у нас до 50 % процентов производительности, а вы возможно помните что я выше упоминал о скорости 2.5 Мбайта в секунду по SPI, то есть вы понимаете к чему я веду! При такой скорости обмена по SPI процессор будет проводить большую часть времени в прерываниях SPI просто копируя байты из регистров в память и обратно.

Есть и другая сторона этой проблемы, дело в том, что наше прерывание приема будет происходить с периодом: 300 миллионов тактов / 2 миллиона байт = 150 тактов. Для тактов с частотой 300 МГерц, один такт равен 1 / (300 миллионов) = примерно 3.3 наносекунды, и значит 150 тактов равны половине микросекунды! То есть байт приходит к нам два раза в течение одной МИКРОсекунды (а вообще, пол микросекунды получается просто из того факта, что у нас трафик составляет 2 миллиона байт в секунду).

Это приведет к тому, что фоновая программа, а в качестве фоновой программы у нас выполняется код библиотеки LWIP, будет постоянно испытывать задержки-прерывания. Каждые пол микросекунды какая-то операция будет растягиваться, ожидая завершения случившегося прерывания SPI. Вряд ли LWIP сможет нормально работать в таких условиях с нарушенным балансом по времени между своими операциями.

Посмотрите, как это выглядит на временной диаграмме:

Временная диаграмма, прерывания/фоновоя работа
Временная диаграмма, прерывания/фоновоя работа

Кстати, обратите внимание, всегда есть какой-то период, на котором измеряется производительность или распределение занятости процессора по задачам. Этот период всегда должен быть привязан к периоду из предметной области задачи, которую вы решаете. В данном конкретном случае мы привязали его к периоду передачи одного байта через SPI шину. 

Как раз для того, чтобы убрать эти постоянные прерывания фоновой программы, которые могут отнимать у этой самой фоновой программы до 50% и более процентов процессорного времени мы используем функции DMA. 

В одной из моих предыдущих статей мы написали псевдокод, который определяет функцию DMA (собственно определяет программу действий контроллера DMA):

for (int i=0; i<Conf.Count;i++)
  {
  Wait(Conf.EnabledInteruptSignal);
  *Conf.DstPtr = *Conf.SrcPtr;
  Conf.DstPtr += Conf.IncrementDst;
  Conf.SrcPtr += Conf.IncrementSrc;
  }

Посмотрим, что нужно сделать чтобы эта функция контроллера DMA заменила код наших прерываний по SPI.

Это очень просто, мы должны всего лишь правильно проинициализировать поля структуры Conf. Для наглядности я буду использовать имена переменных, которые были определены в нашей функции прерывания выше, чтобы вы могли сопоставить два подхода:

Conf.Count = spiRcvBufSize;//размер буфера

Conf.SrcPtr = &SPI0->SPI_RDR;

Conf.IncrementSrc = 0;//всегда читаем один и тот же регистр
Conf.DstPtr = spiRcvBufPtr;//указатель на начало буфера, см. буфер из прерывания

Conf.IncrementDst = 1;//смещаемся на 1 байт после каждой транзакции

Conf.EnabledInteruptSignal = SignalEnum.SpiRcvInteruptFlag;//некоторая 
//предопределенная константа, которая определяет что надо запускать транзакцию по 
//активизации флага принятого байта в SPI

Вообще говоря, в структуре есть и другие настройки (ниже будет пример), но для общего понимания принципа, работы перечисленных полей вполне достаточно, мне кажется, но есть еще один принципиальный момент. Приведенная простейшая функция DMA определяет только однократное заполнение буфера, но код, основанный на прерываниях, был рассчитан на бесконечное (сколь угодно долгое) исполнение, поэтому мы должны рассмотреть более сложную функцию, которую реализует DMA.

while(1)
{
 Conf = DmaConf.ConfArr[DmaConf.StructIndex++];
 if(DmaConf.StructIndex == DmaConf.StructArrSize)
   DmaConf.StructIndex = 0;
 for (int i=0; i<Conf.Count;i++)
 {
  Wait(Conf.EnabledInteruptSignal);
  *Conf.DstPtr = *Conf.SrcPtr;
  Conf.DstPtr += Conf.IncrementDst;
  Conf.SrcPtr += Conf.IncrementSrc;
 }
}

Суть в том, что DMA контроллер можно настроить так, что он будет перезагружать структуру, по данным которой он работает из памяти. Интересно отметить, что это физически является еще одной операцией обращения к памяти, помимо двух операций (чтения-запись), которые выполняются при пересылке данных, то есть работа DMA контроллера с памятью может оказаться сложнее чем кому-то могло показаться при первом рассмотрении. 

Для того чтобы вы могли оценить сложность настройки двух каналов DMA для приема и передачи данных для SPI контроллера я приведу здесь код рабочей функции для такой настройки для процессора, про который мы беседовали, в том числе в этой статье:

void xdmac_build_lld_config( )
{
uint32_t xchid = XDMAC_RX_CH;
lld_view1 *plld_view1;
lld_view3 plld_view3mem; 
lld_view3 *plld_view3 = &plld_view3mem;
	uint32_t xdmaint;
    SPIManager::dmaBufFls = 0;
    SPIManager::prevDmaPtr = SPIManager::rxbuffer[1];
    SPIManager::currentDmaIndx = 1;
    SPIManager::nextDmaIndx = 2;

    plld_view1 = &dmaRcvDescriptors[0];
	/* Initialize and enable DMA controller */
	PMC_EnablePeripheral(ID_XDMAC);

	xdmaint = (XDMAC_CIE_BIE |
    XDMAC_CIE_LIE   |
	XDMAC_CIE_DIE   |
	XDMAC_CIE_FIE   |
	XDMAC_CIE_RBIE  |
	XDMAC_CIE_WBIE  |
	XDMAC_CIE_ROIE);
	/* Initialize channel config for receiver //transmitter */
    /** Next Descriptor Address number. */
    plld_view3 -> mbr_nda = 0;
    /** Microblock Control Member. */
    plld_view3 -> mbr_ubc = sizeof(SPIManager::rxbuffer[0]);
    /** Source Address Member. */
    plld_view3 -> mbr_sa = (uint32_t)&(SPI_MASTER_BASE->SPI_RDR);
    /** Destination Address Member. */
    plld_view3 -> mbr_da = (uint32_t)SPIManager::rxbuffer;
    /** Configuration Register. */
    plld_view3 -> mbr_cfg = XDMAC_CC_TYPE_PER_TRAN |
    XDMAC_CC_MBSIZE_SINGLE |
    XDMAC_CC_DSYNC_PER2MEM |
    XDMAC_CC_CSIZE_CHK_1 |
    XDMAC_CC_DWIDTH_BYTE|
    XDMAC_CC_SIF_AHB_IF1 |
    XDMAC_CC_DIF_AHB_IF0 |
    XDMAC_CC_SAM_FIXED_AM |
    XDMAC_CC_DAM_INCREMENTED_AM |
    XDMAC_CC_PERID(SPI0_XDMAC_RX_CH_NUM);
    /** Block Control Member. */
    plld_view3 -> mbr_bc = 0;
    /** Data Stride Member. */
    plld_view3 -> mbr_ds = 0;
    /** Source Microblock Stride Member. */
    plld_view3 -> mbr_sus = 0;
    /** Destination Microblock Stride Member. */
    plld_view3 -> mbr_dus = 0;

    uint32_t i;
    for (i=0; i < SPIManager::descriptorCnt ; i++)
    {

	    plld_view1->mbr_sa = (uint32_t)SPIManager::rxbuffer[i];
	    plld_view1->mbr_da = (uint32_t )SPIManager::rxbuffer[i];
	    
	    plld_view1->mbr_ubc = XDMAC_CNDC_NDVIEW_NDV0;
	    plld_view1->mbr_ubc |= XDMAC_CNDC_NDE_DSCR_FETCH_EN;
	    plld_view1->mbr_ubc |= XDMAC_CNDC_NDSUP_SRC_PARAMS_UNCHANGED;
	    plld_view1->mbr_ubc |= XDMAC_CNDC_NDDUP_DST_PARAMS_UPDATED;
	    plld_view1->mbr_ubc <<= 24;
	    plld_view1->mbr_ubc |= sizeof(SPIManager::rxbuffer[0]);
	    plld_view1->mbr_nda = (uint32_t )&dmaRcvDescriptors[i+1];
	    plld_view1 = &dmaRcvDescriptors[i+1];
    }

    plld_view1 = &dmaRcvDescriptors[SPIManager::descriptorCnt - 1];
    plld_view1->mbr_nda = (uint32_t )&dmaRcvDescriptors[0];
    plld_view1->mbr_ubc = XDMAC_CNDC_NDVIEW_NDV0;
    plld_view1->mbr_ubc |= XDMAC_CNDC_NDE_DSCR_FETCH_EN;
    plld_view1->mbr_ubc |= XDMAC_CNDC_NDSUP_SRC_PARAMS_UNCHANGED;
    plld_view1->mbr_ubc |= XDMAC_CNDC_NDDUP_DST_PARAMS_UPDATED;
    plld_view1->mbr_ubc <<= 24;
    plld_view1->mbr_ubc |= sizeof(SPIManager::rxbuffer[0]);
    XDMAC_SetDescriptorAddr(XDMAC, xchid, (uint32_t )&dmaRcvDescriptors[1], 0);
    XDMAC_SetDescriptorControl(XDMAC, xchid, XDMAC_CNDC_NDE_DSCR_FETCH_EN |
    XDMAC_CNDC_NDSUP_SRC_PARAMS_UNCHANGED |
    XDMAC_CNDC_NDDUP_DST_PARAMS_UPDATED |
    XDMAC_CNDC_NDVIEW_NDV0);
    xdmac_configure_transfer(XDMAC,xchid, plld_view3);

	XDMAC_EnableChannelIt(XDMAC, xchid, xdmaint);
    SCB_CleanInvalidateDCache();
	XDMAC_EnableChannel(XDMAC, xchid);
    
//transmitter 
    xchid = XDMAC_TX_CH;
    plld_view1 = &dmaTrnsDescriptors[0];
	/* Initialize channel config for transmitter */
    /** Next Descriptor Address number. */
    plld_view3 -> mbr_nda = 0;
    /** Microblock Control Member. */
    plld_view3 -> mbr_ubc = sizeof(txbuffer);
    /** Source Address Member. */
    plld_view3 -> mbr_sa = (uint32_t)txbuffer;
    /** Destination Address Member. */
    plld_view3 -> mbr_da = (uint32_t)&(SPI_MASTER_BASE->SPI_TDR);
    /** Configuration Register. */
    plld_view3 -> mbr_cfg = XDMAC_CC_TYPE_PER_TRAN |
	XDMAC_CC_MBSIZE_SINGLE |
	XDMAC_CC_DSYNC_MEM2PER |
	XDMAC_CC_CSIZE_CHK_1 |
	XDMAC_CC_DWIDTH_BYTE |
	XDMAC_CC_SIF_AHB_IF0 |
	XDMAC_CC_DIF_AHB_IF1 |
	XDMAC_CC_SAM_INCREMENTED_AM |
	XDMAC_CC_DAM_FIXED_AM |
	XDMAC_CC_PERID(SPI0_XDMAC_TX_CH_NUM);
    /** Block Control Member. */
    plld_view3 -> mbr_bc = 0;
    /** Data Stride Member. */
    plld_view3 -> mbr_ds = 0;
    /** Source Microblock Stride Member. */
    plld_view3 -> mbr_sus = 0;
    /** Destination Microblock Stride Member. */
    plld_view3 -> mbr_dus = 0;

//	for (int i=0; i < descriptorCnt ; i++)
	{
        int i=0;
    	plld_view1->mbr_sa = (uint32_t)txbuffer;
    	plld_view1->mbr_da = (uint32_t)&(SPI_MASTER_BASE->SPI_TDR);
    	plld_view1->mbr_ubc = XDMAC_CNDC_NDVIEW_NDV1;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDE_DSCR_FETCH_DIS;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDSUP_SRC_PARAMS_UPDATED;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDDUP_DST_PARAMS_UNCHANGED;
    	plld_view1->mbr_ubc <<= 24;
    	plld_view1->mbr_ubc |= sizeof(txbuffer);
        plld_view1->mbr_nda = (uint32_t)&dmaTrnsDescriptors[i+1];
    	
        plld_view1 = (lld_view1*)plld_view1->mbr_nda;
	}

    plld_view1 = &dmaTrnsDescriptors[SPIManager::descriptorCnt - 1];
    	plld_view1->mbr_ubc = XDMAC_CNDC_NDVIEW_NDV1;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDE_DSCR_FETCH_DIS;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDSUP_SRC_PARAMS_UPDATED;
    	plld_view1->mbr_ubc |= XDMAC_CNDC_NDDUP_DST_PARAMS_UNCHANGED;
    	plld_view1->mbr_ubc <<= 24;
    	plld_view1->mbr_ubc |= sizeof(txbuffer);
    	plld_view1->mbr_nda = (uint32_t)&dmaTrnsDescriptors[0];
    XDMAC_SetDescriptorAddr(XDMAC, xchid, (uint32_t )&dmaTrnsDescriptors[0], 0);
    XDMAC_SetDescriptorControl(XDMAC, xchid, XDMAC_CNDC_NDE_DSCR_FETCH_EN |
    XDMAC_CNDC_NDSUP_SRC_PARAMS_UPDATED |
    XDMAC_CNDC_NDDUP_DST_PARAMS_UNCHANGED |
    XDMAC_CNDC_NDVIEW_NDV1);
    xdmac_configure_transfer(XDMAC,xchid, plld_view3);

	XDMAC_EnableChannelIt(XDMAC, xchid, xdmaint);
	//END of Transmiter init

   	XDMAC_EnableGIt(XDMAC, XDMAC_RX_CH);
   	/*Enable XDMAC interrupt */
   	NVIC_ClearPendingIRQ(XDMAC_IRQn);
   	NVIC_SetPriority( XDMAC_IRQn ,0);
   	NVIC_EnableIRQ(XDMAC_IRQn);
}

Код, конечно, не совершенный, но в embedded разработке, как нигде актуально правило: «Работает?! Не трогай!».

Здесь lld-структуры определены в datasheet на процессор в разделе:

35.6 Linked List Descriptor Operation

Table 35-2. Channel Next Descriptor View 0–3 Structures

и скорее всего (я уже не помню) были взяты из каких-то официальных или не очень официальных примеров кода для работы с периферией:

/**
 * \brief Structure for storing parameters for DMA view2 that can be
 * performed by the DMA Master transfer.
 */
typedef struct {
	/** Next Descriptor Address number. */
	uint32_t mbr_nda;
	/** Microblock Control Member. */
	uint32_t mbr_ubc;
	/** Source Address Member. */
	uint32_t mbr_sa;
	/** Destination Address Member. */
	uint32_t mbr_da;
	/** Configuration Register. */
//	uint32_t mbr_cfg;
} lld_view1;

/**
 * \brief Structure for storing parameters for DMA view3 that can be
 * performed by the DMA Master transfer.
 */
typedef struct {
	/** Next Descriptor Address number. */
	uint32_t mbr_nda;
	/** Microblock Control Member. */
	uint32_t mbr_ubc;
	/** Source Address Member. */
	uint32_t mbr_sa;
	/** Destination Address Member. */
	uint32_t mbr_da;
	/** Configuration Register. */
	uint32_t mbr_cfg;
	/** Block Control Member. */
	uint32_t mbr_bc;
	/** Data Stride Member. */
	uint32_t mbr_ds;
	/** Source Microblock Stride Member. */
	uint32_t mbr_sus;
	/** Destination Microblock Stride Member. */
	uint32_t mbr_dus;
} lld_view3;

Если у вас не пропало желание применять DMA в своих разработках вы определенно заслуживаете звания «разработчик-архитектор программных Embedded решений», и вам можно пожелать актуальных-подробных примеров кода.

При этом мне вспоминается одна малоизвестная цитата из старого советского фильма: «Остров погибших кораблей», там какой-то не молодой женатый дяденька грустно говорит счастливой невесте перед самой ее свадьбой: «Поздравить вас не с чем…»

А у меня есть ощущение, что я должен с кем-то поделиться этими знаниями, иначе я не смогу спокойно умереть.

И на этой не очень оптимистической ноте, я пожалуй и закончу свой очередной рассказ.

Всем всего хорошего, не упустите лучшие моменты жизни за расчетами тактов процессора.

Сёргий.

На тему Embedded разработки у меня можно также почитать:

Можно ли использовать DMA вместо memcpy в Linux

Какие бывают Cortex-M7 ARM-ы, периферия, шины, память, … DMA

RTOS или не RTOS вот в чем вопрос 2, или Windows тоже RTOS?

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments34

Articles