В предыдущей части нашего урока мы познакомились с аппаратной организацией АЦП в контроллере, а также изучили его регистры и их биты.
Теперь, думаю, можно приступить к проекту, который мы, чтобы не мучиться с настройкой дисплея (а дисплей мы возьмём символьный разрешением 4 x 20 символов), сделаем из проекта урока 151 с именем LL_I2C_LCD1602 и назовём его LL_ADC_REG_ONCE.
Откроем наш проект в Cube MX, включим 1-й канал ADC1
Если мы раскроем данный раздел, то увидим следующее
Это значит, что мы запускаем преобразование сигнала только с одного канала и интервал между выборками у нас 1,5 цикла.
У нас включилась ножка PA1
Перейдём в раздел Clock Configuration и настроим делители и множители следующим образом
Включим для ADC1 использование библиотеки LL
Сгенерируем проект, откроем его в Keil, настроим программатор на автоперезагрузку, отключим оптимизацию и добавим к дереву проектов файлы lcd.c и i2c_user.c.
Так как дисплей у нас 4-строчный, а не двух, как в донорском проекте, а также мы кое-что ещё подправили там за время прошедшее со времён того проекта, то перейдём в файл lcd.c и внесём некоторые поправки.
Ниже функции DelayMicro добавим функцию задержки в наносекундах
1 2 3 4 5 6 7 |
//------------------------------------------------ __STATIC_INLINE void DelayNano(__IO uint32_t nanos) { nanos = nanos * (SystemCoreClock / 1000000) / 9000; while (nanos--); } //------------------------------------------------ |
В функции sendhalfbyte удалим вот эту задержку
DelayMicro(1);
А вместо неё применим вот такую
1 2 |
LCD_WriteByteI2CLCD((portlcd|=0x04)|c); DelayNano(200); |
А вот эту задержку удалим совсем
DelayMicro(50);
В функции sendbyte добавим задержку вот здесь
1 2 |
hc=c>>4; DelayNano(100); |
В функции LCD_SetPos у нас и так 4 варианта, так что ничего добавлять не придётся.
Теперь внесём некоторые исправления в функцию инициализации LCD_ini, в которой для начала включим режим записи вот здесь
1 2 |
LCD_WriteByteI2CLCD(0); setwrite();//запись |
Вот эту задержку
sendhalfbyte(0x03);
DelayMicro(200);
меняем на 4500
1 2 |
sendhalfbyte(0x03); DelayMicro(4500); |
Вот в этой строке
sendbyte(0x28,0);//режим 4 бит, 2 линии (для нашего большого дисплея это 4 линии, шрифт 5х8
Пока выключаем дисплей
1 2 |
sendbyte(0x28,0);//режим 4 бит, 2 линии (для нашего большого дисплея это 4 линии, шрифт 5х8 sendbyte(0x08,0);//дисплей пока выключаем |
а включаем его здесь
1 2 3 |
sendbyte(0x06,0);// пишем влево LL_mDelay(1); sendbyte(0x0C,0);//дисплей включаем (D=1), курсоры никакие не нужны |
Идём в main.c и в функции main() удалим вот эту локальную переменную
uint16_t i;
а добавим вот такие
1 2 3 4 |
/* USER CODE BEGIN 1 */ __IO uint32_t wait_loop_index = 0; __IO uint16_t ADC_Data; __IO uint16_t ADC_mVolt; |
В символьном массиве добавим немного элементов
char str01[15];
Удалим инициализацию переменной
i=0;
Удалим вывод строк на дисплей
LCD_String(«String 1»);
LCD_SetPos(5,1);
LCD_String(«String 2»);
LL_mDelay(2000);
LCD_SetPos(5,1);
LCD_String(» «);
Вместо них добавим вот такие строки
1 2 3 4 |
LCD_ini(); LCD_String("ADC"); LCD_SetPos(0,1); LCD_String("Regular Once"); |
Из бесконечного цикла пока также удалим весь пользовательский код.
Перейдём к схеме. Дисплей к плате подключен так же, как и в уроке 151, только дисплей у нас немного другой и в качестве источника его питания мы будем использовать контакт 5v ST-Link. Также мы подключим в качестве делителя многооборотный подстроечный резистор на 10 килоом, расположив его на маленькой макетной плате. На крайние выводы данного резистора мы подключим питание 3,3 вольта с контроллера и общий провод, а с центрального контакта резистора будем изменяемое напряжение подавать на ножку PA1, настроенную как вход нашего АЦП
Подключим ST-Link к ПК, соберём наш код, прошьём контроллер и посмотрим на дисплей
Всё выводится, теперь мы уверены, что у нас работает хотя бы дисплей.
Поэтому вернёмся в проект и традиционно проанализируем код инициализации АЦП, автоматически сгенерированный с помощью Cube MX. В функции инициализации АЦП MX_ADC1_Init сначала мы видим запуск тактирования периферии ADC1, также запуск тактирования порта GPIOA и затем настройку ножки PA1.
Далее мы видим инициализацию полей некой структуры
1 2 |
ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT; ADC_InitStruct.SequencersScanMode = LL_ADC_SEQ_SCAN_DISABLE; |
Здесь сказано, что выравниваться результат преобразования будет вправо, и что у нас однократный режим.
Далее указатель на данную структуру передаётся в функцию библиотеки LL_ADC_Init, в которой данные настройки заносятся в бит ADC_CR1_SCAN регистра CR1 и в бит ADC_CR2_ALIGN регистра CR2
1 2 3 4 5 6 7 8 9 10 11 |
MODIFY_REG(ADCx->CR1, ADC_CR1_SCAN , ADC_InitStruct->SequencersScanMode ); MODIFY_REG(ADCx->CR2, ADC_CR2_ALIGN , ADC_InitStruct->DataAlignment ); |
Возвращаемся в функцию MX_ADC1_Init, в которой затем инициализируется поле другой структуры
1 |
ADC_CommonInitStruct.Multimode = LL_ADC_MULTI_INDEPENDENT; |
Указатель на данную структуру передаётся в другую библиотечную функцию LL_ADC_CommonInit, в которой проверяется вот это условие
1 |
if(ADC_CommonInitStruct->Multimode != LL_ADC_MULTI_INDEPENDENT) |
Оно у нас не выполнится, так как у нас равно, поэтому мы попадаем в отрицательную ветвь конструкции, в которой выполнится макрос, который очистит все биты битовой маски ADC_CR1_DUALMOD. В третьем параметре всё равно нули, поэтому ничего больше не произойдёт
1 2 3 4 |
MODIFY_REG(ADCxy_COMMON->CR1, ADC_CR1_DUALMOD, LL_ADC_MULTI_INDEPENDENT ); |
Опять возвращаемся в функцию , в которой теперь инициализируется следующая структура
1 2 3 4 5 |
ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE; ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_DISABLE; ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE; ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE; ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_NONE; |
Здесь происходят настройки программного старта АЦП (не от внешнего события), того, что мы не сканируем каналы по списку, производим одну конвертацию и не используем DMA.
Затем указатель на данную структуру передаётся в следующую библиотечную функцию LL_ADC_REG_Init, в которой также проверяется уже вот такое условие
1 |
if(ADC_REG_InitStruct->SequencerLength != LL_ADC_REG_SEQ_SCAN_DISABLE) |
Здесь также условие не выполняется и мы попадаем в ветку else конструкции, в которой очищается бит ADC_CR1_DISCEN и битовая маска ADC_CR1_DISCNUM регистра CR1, и ничего не устанавливается, так как в третьем параметре у нас везде нули
1 2 3 4 5 6 7 |
MODIFY_REG(ADCx->CR1, ADC_CR1_DISCEN | ADC_CR1_DISCNUM , ADC_REG_InitStruct->SequencerLength | LL_ADC_REG_SEQ_DISCONT_DISABLE ); |
Затем в регистре CR2 сбрасываем все биты битовой маски ADC_CR2_EXTSEL, биты ADC_CR2_CONT и ADC_CR2_DMA. В третьем параметре также все нули, поэтому никакие биты не устанавливаются
1 2 3 4 5 6 7 8 9 |
MODIFY_REG(ADCx->CR2, ADC_CR2_EXTSEL | ADC_CR2_CONT | ADC_CR2_DMA , ADC_REG_InitStruct->TriggerSource | ADC_REG_InitStruct->ContinuousMode | ADC_REG_InitStruct->DMATransfer ); |
Затем пармаетр SequencerLength нашей структуры передаётся в функцию LL_ADC_REG_SetSequencerLength, в которой сбрасываются все биты битового поля L регистра SQR1. В данном регистре ничего больше не устанавливается, так как в третьем параметре макроса также нули
1 |
MODIFY_REG(ADCx->SQR1, ADC_SQR1_L, SequencerNbRanks); |
Возвращаемся в функцию MX_ADC1_Init, в которой далее вызывается функция LL_ADC_REG_SetSequencerRanks, в которой в регистре (или в нескольких регистрах) SQRx, соответствующем выбранному каналу (или маске из нескольких каналов), очистится битовое поле SQx, также соответствующее данному каналу (или нескольким), а затем при помощи третьего параметра макроса установится количество преобразований для канала (или нескольких)
1 2 3 |
MODIFY_REG(*preg, ADC_CHANNEL_ID_NUMBER_MASK << (Rank & ADC_REG_RANK_ID_SQRX_MASK), (Channel & ADC_CHANNEL_ID_NUMBER_MASK) << (Rank & ADC_REG_RANK_ID_SQRX_MASK)); |
Затем идёт вызов функции LL_ADC_SetChannelSamplingTime, в которой в регистре SMPRx, соответствующем каналу, очищаются все биты битового поля SMPx, соответствующего данному каналу, а затем устанавливаются биты данного поля, соответствующие нужному интервалу между выборками (в нашем случае 1,5 цикла, что соответствует комбинации битов 000, то есть не устанавливается ничего)
1 2 3 |
MODIFY_REG(*preg, ADC_SMPR2_SMP0 << __ADC_MASK_SHIFT(Channel, ADC_CHANNEL_SMPx_BITOFFSET_MASK), SamplingTime << __ADC_MASK_SHIFT(Channel, ADC_CHANNEL_SMPx_BITOFFSET_MASK)); |
Вот и вся наша функция инициализации АЦП.
Вернёмся в функцию main() и для начала проинициализируем нулём переменную для хранения сырых данных АЦП
1 2 |
/* USER CODE BEGIN 2 */ ADC_Data = 0; |
Разрешим работу АЦП
1 2 |
LCD_ini(); LL_ADC_Enable(ADC1); |
Запустим калибровку АЦП, выждав перед этим необходимый таймаут с помощью нехитрой процедуры
1 2 3 4 5 6 7 |
LL_ADC_Enable(ADC1); wait_loop_index = ((LL_ADC_DELAY_ENABLE_CALIB_ADC_CYCLES * 32) >> 1); while(wait_loop_index != 0) { wait_loop_index--; } LL_ADC_StartCalibration(ADC1); |
С помощью вызова функции библиотеки LL LL_ADC_StartCalibration мы устанавливаем бит ADC_CR2_CAL в регистре CR2.
Дождёмся окончания калибровки
1 2 |
LL_ADC_StartCalibration(ADC1); while (LL_ADC_IsCalibrationOnGoing(ADC1) != 0) {} |
Здесь с помощью функции LL_ADC_IsCalibrationOnGoing мы дожидаемся сброса данного бита.
В бесконечном цикле запускаем преобразование сигнала
1 2 |
/* USER CODE BEGIN 3 */ LL_ADC_REG_StartConversionSWStart(ADC1); |
С помощью вызванной функции устанавливаются биты ADC_CR2_SWSTART и ADC_CR2_EXTTRIG регистра CR2.
Дождёмся окончания преобразования
1 2 |
LL_ADC_REG_StartConversionSWStart(ADC1); while (!LL_ADC_IsActiveFlag_EOS(ADC1)) {} |
Данная функция узнаёт состояние бита EOS регистра SR.
Дождавшись, очищаем данный флаг, как требует того техническая документация
1 2 |
while (!LL_ADC_IsActiveFlag_EOS(ADC1)) {} LL_ADC_ClearFlag_EOS(ADC1); |
С помощью специальной функции библиотеки считаем состояние регистра DR в переменную
1 2 |
LL_ADC_ClearFlag_EOS(ADC1); ADC_Data = LL_ADC_REG_ReadConversionData12(ADC1); |
С помощью нехитрого макроса превратим сырые считанные данные в миливольты, передав в макрос напряжение питание (я его измерил)
1 2 |
ADC_Data = LL_ADC_REG_ReadConversionData12(ADC1); ADC_mVolt = __LL_ADC_CALC_DATA_TO_VOLTAGE((uint32_t)3275, ADC_Data, LL_ADC_RESOLUTION_12B); |
Отобразим полученное значение напряжение на дисплее и подождём 0,1 секунды
1 2 3 4 5 |
ADC_mVolt = __LL_ADC_CALC_DATA_TO_VOLTAGE((uint32_t)3275, ADC_Data, LL_ADC_RESOLUTION_12B); sprintf(str01,"%.2fv",(float)ADC_mVolt/1000.); LCD_SetPos(0,3); LCD_String(str01); LL_mDelay(100); |
Вот и весь код.
Соберём его, прошьём контроллер и, вращая ось подстроечного резистора, посмотрим за изменениями напряжения
Также я сверял показания с образцовым мультиметром, подключив его также к нашей ножке. Показания, хоть сходились и не точь в точь, но были очень близки.
Итак, на данном уроке мы научились работать с АЦП контроллера STM32F1 с использованием библиотеки LL. Пусть мы пока использовали для этого самый простой режим работы АЦП, но зато на данном уроке мы очень много узнали по работе с аппаратной части модуля АЦП в нашем контроллере.
Всем спасибо за внимание!
Предыдущая часть Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Логический анализатор 16 каналов можно приобрести здесь
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий