Продолжаем работать с АЦП (ADC) контроллера STM32F1 с использованием библиотеки LL. Также работать мы пока будем с регулярным каналом.
Только теперь мы, используя регулярный канал с однократным преобразованием, попробуем привлечь к процедуре опроса АЦП и конверсии периферию DMA, что позволит нам делать очень интересные вещи.
Также теперь мы будем использовать режим SCAN, который сканирует сразу несколько каналов в заданной последовательности. Где именно задаётся последовательность эта, мы уже говорили. Тем не менее мы в процедуре инициализации всё это увидим.
Схема наша немного теперь изменится, потому что использовать мы будем сразу 4 канала, поэтому мы повесим 4 резистора, регулируемые ножки которых мы соединим с входами наших каналов, получив тем самым 4 примитивных независимых регулируемых источника напряжения.
Я образно набросал фрагмент нашей схемы, выбрав не совсем такой, но подобный контроллер
А живьём наша схема имеет следующий вид:
Проект нашего урока мы сделаем из проекта урока 185 с именем LL_ADC_REG_ONCE и назовём его LL_ADC_REG_DMA.
Откроем наш проект в Cube MX и в свойствах ADC1 включим ещё 3 канала
Соответственно, у нас включатся также и 3 ножки порта
Включаем также последовательное преобразование. Только его просто так не включишь, для этого нужно выставить число преобразований — 4 и Scan Conversion Mode включится сам
Добавим времени на преобразование и выберем соответствующие каналы в последовательность
Включим также в соответствующей закладке настроек ADC1 канал DMA, в настройках которого оставляем всё по умолчанию
Также не забываем в настройках проекта задействовать на периферию DMA библиотеку LL
Сгенерируем проект, откроем его в Keil, настроим программатор на автоперезагрузку, отключим оптимизацию и добавим к дереву проектов файлы lcd.c и i2c_user.c.
В файле main.c в функции MX_ADC1_Init посмотрим, какие изменения теперь у нас произошли теперь в инициализации ADC. Причём практически все настройки DMA у нас находятся здесь. В функции инициализации DMA у нас будут только включение тактирования периферии DMA, включение глобальных прерываний и настройка приоритета. Мало того, в функции инициализации ADC всё начинается с настройки именно DMA после настройки ножке, а уж затем настраивается непосредственно ADC.
Настройки DMA тут, в принципе, стандартные, мы не будем их подробно рассматривать
1 2 3 4 5 6 7 8 9 |
/* ADC1 DMA Init */ /* ADC1 Init */ LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY); LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_LOW); LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL); LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT); LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT); LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD); LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_HALFWORD); |
Причём настройки здесь не все, часть инициализации DMA (изменение приоритета, включение другого режима, настройку адресов) нам придётся проделать затем отдельно.
Далее нам интересна вот эта настройка, с помощью которой включится бит SCAN в регистре CR1
1 |
ADC_InitStruct.SequencersScanMode = LL_ADC_SEQ_SCAN_ENABLE; |
Изменения нас ждут также здесь
1 |
ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_ENABLE_4RANKS; |
Здесь включатся биты L1 и L0 в регистре SQR1, которые будут означать, что сканироваться будут именно 4 канала ADC.
При помощи следующей строки кода инициализации у нас включится бит DMA в регистре CR2
1 |
ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_UNLIMITED; |
А далее стандартная настройка четырёх каналов ADC
1 2 3 4 5 6 7 8 9 10 11 12 |
/** Configure Regular Channel */ LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, LL_ADC_CHANNEL_1); LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_13CYCLES_5); /** Configure Regular Channel */ LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_2, LL_ADC_CHANNEL_2); LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_13CYCLES_5); /** Configure Regular Channel */ LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_3, LL_ADC_CHANNEL_3); LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_13CYCLES_5); /** Configure Regular Channel */ LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_4, LL_ADC_CHANNEL_4); LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_13CYCLES_5); |
В результате данной настройки в битовых полях SMP3:SMP0 регистра SMPR2 выставятся биты 1.
Вот и все изменения.
Добавим глобальный 4-элементный массив для хранения данных, считанных из ADC, а также заведём флаг
1 2 3 |
/* USER CODE BEGIN PV */ __IO uint16_t ADC_Data[4] = {0}; __IO uint8_t fl_adc; |
В функции main() удалим, соответственно теперь вот эту локальную переменную
__IO uint16_t ADC_Data;
Другую переменную того же типа превратим теперь в массив, а в символьный массив добавим ещё элементов
__IO uint16_t ADC_mVolt[4];
char str01[20];
Удалим инициализацию переменной
ADC_Data = 0;
Внесём изменения в настройки DMA, а также настроим там адреса приёмника и источника
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
LCD_ini(); LL_DMA_ConfigTransfer(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY | LL_DMA_MODE_CIRCULAR | LL_DMA_PERIPH_NOINCREMENT | LL_DMA_MEMORY_INCREMENT | LL_DMA_PDATAALIGN_HALFWORD | LL_DMA_MDATAALIGN_HALFWORD | LL_DMA_PRIORITY_HIGH ); LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, LL_ADC_DMA_GetRegAddr(ADC1, LL_ADC_DMA_REG_REGULAR_DATA), (uint32_t)&ADC_Data, LL_DMA_DIRECTION_PERIPH_TO_MEMORY); |
Здесь возникает резонный вопрос, а почему бы данные настройки сразу не включить в Cube? Оказывается, не получится. Ничего не будет работать и всё повиснет, видимо, здесь важен какой-то порядок и слишком высокий приоритет не даст настроить ADC.
Настроим также длину пакета DMA, включим локальные прерывания по событию окончания приёма-передачи и по событию ошибки и включим канал
1 2 3 4 5 |
LL_DMA_DIRECTION_PERIPH_TO_MEMORY); LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 4); LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableIT_TE(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); |
Здесь немного допишем
LCD_String("Regular Once DMA");
Выведем ещё одну строку на дисплей
1 2 3 |
LCD_String("Regular Once DMA"); LCD_SetPos(0,2); LCD_String("Scan Conversion"); |
Добавим также функцию для обработки прерывания от DMA, в которой взведём наш пользовательский флаг
1 2 3 4 5 6 7 |
/* USER CODE BEGIN 4 */ //------------------------------------------------------ void ADC_DMA_TransferComplete_Callback() { fl_adc = 1; } //------------------------------------------------------ |
В файле stm32f1xx_it.c добавим прототип на данную функцию
1 2 |
/* USER CODE BEGIN PFP */ void ADC_DMA_TransferComplete_Callback(void); |
Затем в обработчике прерываний от DMA DMA1_Channel1_IRQHandler в случае установленного флага окончания передачи вызовем нашу функцию, а в случае ошибки только сбросим флаг
1 2 3 4 5 6 7 8 9 10 |
/* USER CODE BEGIN DMA1_Channel1_IRQn 0 */ if(LL_DMA_IsActiveFlag_TC1(DMA1) == 1) { ADC_DMA_TransferComplete_Callback(); LL_DMA_ClearFlag_TC1(DMA1); } if(LL_DMA_IsActiveFlag_TE1(DMA1) == 1) { LL_DMA_ClearFlag_TE1(DMA1); } |
Вернёмся в функцию main() файла main.c и в бесконечном цикле удалим ожидание и сброс флага окончания преобразования, а также считывание показаний
while (!LL_ADC_IsActiveFlag_EOS(ADC1)) {}
LL_ADC_ClearFlag_EOS(ADC1);
ADC_Data = LL_ADC_REG_ReadConversionData12(ADC1);
Дождёмся здесь установки своего флага и сбросим его
1 2 3 |
LL_ADC_REG_StartConversionSWStart(ADC1); while (!fl_adc) {} fl_adc = 0; |
Удалим следующую строку
ADC_mVolt = __LL_ADC_CALC_DATA_TO_VOLTAGE((uint32_t)3275, ADC_Data, LL_ADC_RESOLUTION_12B);
Вместо этого считаем и преобразуем в вольты весь наш массив
1 2 3 4 5 |
fl_adc = 0; for(uint8_t i=0; i<4; i++) { ADC_mVolt[i] = __LL_ADC_CALC_DATA_TO_VOLTAGE((uint32_t)3275, ADC_Data[i], LL_ADC_RESOLUTION_12B); } |
Удалим следующую строку
sprintf(str01,«%.2fv»,(float)ADC_mVolt/1000.);
А вместо этого выведем на дисплей показатели всех наших каналов ADC
1 2 3 4 |
ADC_mVolt[i] = __LL_ADC_CALC_DATA_TO_VOLTAGE((uint32_t)3275, ADC_Data[i], LL_ADC_RESOLUTION_12B); } sprintf(str01,"%.2f %.2f %.2f %.2f ", (float)ADC_mVolt[0]/1000., (float)ADC_mVolt[1]/1000., (float)ADC_mVolt[2]/1000., (float)ADC_mVolt[3]/1000.); |
Проверим работу нашего кода на практической схеме, собрав код и прошив контроллер, а также изменяя положения движков наших подстроечных резисторов
Всё работает отлично!
Таким образом, на данном уроке мы научились пользоваться режимом последовательного преобразования каналов с применением периферии DMA, что позволило нам получить четыре независимых измерителя напряжения сигнала на одном ADC. Практически, можно и больше, но для урока, думаю, и этого достаточно.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий