Введение
В прошлой публикации STM32, C++ и FreeRTOS. Разработка с нуля. Часть 1 я остановился на том, как уехал на озеро как были релизованы требования SR7, SR4 и SR6. Напомню, какие требования вообще есть для проекта:
SR0: Устройство должно измерять три параметра (иметь три переменных): Температуру микропроцессора, Напряжение VDDA, Напряжение с переменного резистора
SR1: Устройство должно выводить значение этих переменных на индикатор.
SR2: Единицы измерения для Температуры микропроцессора — градусы Цельсия, для остальных параметров — вольты.
SR3: При нажатии на кнопку 1, на индикаторе должен показываться экран со следующей измеряемой переменной,
SR4: При нажатии на кнопку 1 Светодиод 1 должен изменять свое состояние
SR5: При нажатии на кнопку 2, на индикаторе должен поменяться режим отображения переменных с постоянного показывания переменной на последовательное (менять экраны раз в 1.5 секунды) при следующем нажатии с последовательного на постоянное,
SR6: При нажатии на кнопку 2 светодиод 2 должен менять свое состояние.
SR7: Светодиод 3 должен моргать раз в 1 секунду.
Разработка: АЦП
Решив что я постиг все примудрости новых микроконтроллеров, я решил взять самое амбициозное требование SR0 — собственно это и есть основной функционал устройства — измерять 3 величины.
Для начала нужно было разобраться с АЦП. Решив взять этот блок с лету, особо не читая документацию на микроконтроллер, воооружившись специальным тулом Crt-C и Ctr-V, я нарисовал копию архитектур управления светодиодами и кнопок.
Но начав реализовывать сей чудный рисунок, который в общем-то очень даже рабочий, увлекся чтением документации и понял, что сделать можно вообще без активного объекта, используя канал DMA. Конечно такая архитектура уже будет процессоро-зависимой, по той простой причине, что не все микроконтроллеры имеют такой блок, но я подумал, что будет интересно и полезно показать, то, как можно немного все упростить, используя особенности микроконтроллера.
И переделал все вот так:
Все архитектура готова, и тут я завис. Оказалось что настроить АЦП немного сложнее чем порты, да и у меня упорно не измерялось напряжение с переменного резистора. Температура есть, Vdda есть, а с переменника никак. Настроить АЦП помог опять тот же ресурс, что помог мне сделать проект STM32L. ADC — Аналого-цифровой преобразователь и STM32L. Контроллер DMA. А разобраться с переменником демо-проект, скачанный с документацией для платы Olimex. Оказалось, что его просто надо было подключить отдельной ножкой PortD.Pin1 процессора. Как обычно, всю настройку железа я выкинул в __low_level_init()
//включаем потенциометр(Триммер) PORTD_PIN1 GPIOD->MODER |= GPIO_MODER_MODER1_0; GPIOD->PUPDR |= GPIO_PUPDR_PUPDR1_0; GPIOD->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR1; //настраиваем АЦП, 12 бит, канал 16- температурный сенсор, 17 - VDDA, // 22 - триммер в континиус режиме, //регулярные каналы, скан каналов, ожидание следующего измерения, пока не скинут // EOC, установка EOC после после серии измерений, см http://chipspace.ru/stm32l-discovery-adc/ // В итоге мы будем мерить последовательно каналы 16(температуру) и 17(vdda) и // 22(триммер)первое преобразование будет температура, 2- vdda, 3- триммер ADC1->CR2 |= (ADC_CR2_DELS_2 | ADC_CR2_CONT); ADC1->CR1 |= ADC_CR1_SCAN; //Порт GPIOE.7 как аналоговый вход - триммер GPIOE->MODER |= GPIO_MODER_MODER7; //3 измерения ADC1->SQR1 |= ADC_SQR1_L_1; //Выбираем ADC_IN 16 для 1 преобразования, стр 305 //Выбираем ADC_IN 17 для 2 преобразования, стр 305 //Выбираем ADC_IN 22 для 3 преобразования, стр 305 ADC1->SQR5 |= ADC_SQR5_SQ1_4 | ADC_SQR5_SQ2_0 | ADC_SQR5_SQ2_4 | ADC_SQR5_SQ3_1 | ADC_SQR5_SQ3_2 | ADC_SQR5_SQ3_4; //Выбираем время преобразование для 16 и 17 и 22 канала стр 301 и 279 ADC1->SMPR2 |= ADC_SMPR2_SMP16 | ADC_SMPR2_SMP17_2; ADC1->SMPR1 |= ADC_SMPR1_SMP22_2; // Включаем внутренние входы каналов измреения температурного сенсора и VDDA ADC->CCR |= ADC_CCR_TSVREFE; // DMA ADC1->CR2 |= (ADC_CR2_DMA | ADC_CR2_DDS); //Настройка DMA //Направление передачи данных - чтение из периферии, запись в память. DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; //Адрес периферии не инкрементируется после каждой пересылки. DMA1_Channel1->CCR &= ~DMA_CCR1_PINC; //Адрес памяти инкрементируется после каждой пересылки. DMA1_Channel1->CCR |= DMA_CCR1_MINC; //Размерность данных периферии - 16 бит. DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0; //Размерность данных памяти - 16 бит DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0; //Приоритет - очень высокий (Very High) DMA1_Channel1->CCR |= DMA_CCR1_PL; DMA1_Channel1->CCR |= DMA_CCR1_CIRC;
Сами файлы реализации классов:
#include "types.h" //Стандартные типы проекта #define SENSORTEMPERATURE_CHANNEL 0 #define VDDA_CHANNEL 1 #define TRIMMER_CHANNEL 2 class cAdc { public: explicit cAdc(const tU32 memoryBaseAddr, const tU8 measureCount); tBoolean switchOn(void); tBoolean startConversion(void); tBoolean isConversionReady(void); tF32 getValue(void) const; private: void initDma(const tU32 memoryBaseAddr, const tU8 measureCount); };
#include <stm32l1xx.h> // Регистры STM2 #include "adc.h" // Описание класса #include "susuassert.h" //for ASSERT #include "bitutil.h" //для макросов работы с битами SETBIT, CLRBIT #define ADC1_DR_ADDRESS ((tU32)0x40012458) /******************************************************************************* * Function: constructor * Description: инициализиурет канал DMA адресом в RAM, куда складывать данные * измерений и количеством измерений ******************************************************************************/ cAdc::cAdc(const tU32 memoryBaseAddr, const tU8 measureCount) { ASSERT(measureCount != 0); this->initDma(memoryBaseAddr, measureCount); } /******************************************************************************* * Function: switchOn * Description: Включает АЦП ******************************************************************************/ tBoolean cAdc::switchOn(void) { tBoolean result = FALSE; //Включаем АЦП, стр 299 CD00240194.pdf SETBIT(ADC1->CR2, ADC_CR2_ADON); result = tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_ADONS)); return result; } /******************************************************************************* * Function: startConversion() * Description: Запускает преобразование ******************************************************************************/ tBoolean cAdc::startConversion(void) { tBoolean result = FALSE; //Запускаем преобразование АЦП, стр 299 CD00240194.pdf SETBIT(ADC1->CR2, ADC_CR2_SWSTART); result = tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_STRT)); return result; } /******************************************************************************* * Function: getValue() * Description: читаем результат преобразования ******************************************************************************/ tF32 cAdc::getValue(void) const { tF32 result = ADC1->DR; return result; } /******************************************************************************* * Function: isConversionReady() * Description: готово ли преобразование? ******************************************************************************/ tBoolean cAdc::isConversionReady(void) { tBoolean result = tBoolean(CHECK_BIT_SET(ADC1->SR, ADC_SR_EOC)); return result; } /******************************************************************************* * Function: initDma() * Description: инициализирует канал DMA ******************************************************************************/ void cAdc::initDma(const tU32 memoryBaseAddr, const tU8 measureCount) { //Задаем адрес периферии - регистр результата преобразования АЦП для регулярных каналов. DMA1_Channel1->CPAR = ADC1_DR_ADDRESS; //Задаем адрес памяти - базовый адрес массива в RAM. DMA1_Channel1->CMAR = memoryBaseAddr; DMA1_Channel1->CNDTR = measureCount; //Включаем DMA SETBIT(DMA1_Channel1->CCR, DMA_CCR1_EN); }
#include "adc.h" //для класса cAdc #define MEASUR_NUMBER (tU8) 3 class cAdcDirector { public: explicit cAdcDirector(void); void startConversion(void); __IO uint16_t channelValue[MEASUR_NUMBER]; // для хранения преобразований private: cAdc *pAdc; };
#include "adcdirector.h" //Описание класса /******************************************************************************* * Function: constructor * Description: создает экземпляр АЦП, и передает ему адреса в RAM, куда АЦП с * помощью DMA будет скалдывать результат преобразований. ******************************************************************************/ cAdcDirector::cAdcDirector(void) { this->pAdc = new cAdc((tU32)&channelValue[0], MEASUR_NUMBER); this->pAdc->switchOn(); } /******************************************************************************* * Function: startConversion * Description: Запускаем АЦП на измерение, все данные сыплятся по DMA в массив * channelValue * Threading usage and Assumptions: ******************************************************************************/ void cAdcDirector::startConversion(void) { this->pAdc->startConversion(); }
Проверить работу можно было только под отладчиком, потому что вывода на индиктор у меня пока нет. Но перед этим нужно добавить создание нового экзмпляра класса в main()
void main( void ) { //задача ButtonControllera должна оповещать другие задачи о нажатии //на кнопку, и передавать её значение. Для этого заводим массив указателей на //задачи, которые надо оповещать static tTaskHandle tasksToNotifyFromButton[BUTTON_TASKS_NOTYFIED_NUM]; cAdcDirector *pAdcDirector = new cAdcDirector(); pAdcDirector->startConversion(); cLedsDirector *pLedsDirector = new cLedsDirector(); oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds"); tasksToNotifyFromButton[LEDS_TASK_HANDLE_INDEX] = pLedsDirector->taskHandle; cButtonsController *pButtonsController = new cButtonsController(tasksToNotifyFromButton, BUTTON_TASKS_NOTYFIED_NUM); oRTOS.taskCreate(pButtonsController, BUTTONSCONTROLLER_STACK_SIZE, BUTTONSCONTROLLER_PRIORITY, "Buttons"); oRTOS.startScheduler(); }
Запустил на отладку и получил следующую картинку: Как раз видно, что все 3 значения в массиве channelValue[] поменялись и выделены красным. Проверять значения не стал, но на вскидку — что-то примерно похожее.
По обыкновению проект был сохранен тут: АЦП, кнопки и светодиоды в IAR 6.50
Разработка: Переменные
И так АЦП вроде бы работает, настало время превратить груду этих единиц и ноликов в что-то понятное людям, а имеено в температуру и напряжение:
Для начала я продумал, единый интерфейс для всех переменных. Здесь всего один виртуальный метод — собтсвенно расчет и один метод получения рассчитанного значения.
А далее нарисовал как могла бы выглядеть температура:
А после этого подумалось, что хорошо бы еще иметь фильтр для сглаживания результатов измрений. А поскольку фильтр нужен всем переменным, то можно его сделать в виде интерфейса. В итоге был создан такой концепт:
Класс температуры рализует метод расчета интерфейса. Но тут следует сделать ремарку, для расчета температуры используются заводские коэффициенты зашитые в микроконтроллере, и по идее, чтобы можно было портировать данный код на другую платформу надо бы сделать класс для доступа к энергонезависимым параметрам и передавать ссылку на этот класс классам, которым эти коэффициенты нужны, в данном случае температуре. Но времени у меня в обрез и делать городульку из-за трех коэффциентов очень не хотелось, поэтому спишем такой косяк на нехватку времени и оставим галочку в уме, что тут портация не выйдет(ну и ладно :)). Рализация всего этого дела выглядит так:
#include "types.h" //Стандартные типы проекта #include "adcdirector.h" //для класса cAdcdirector class iVariable { public: explicit iVariable(const cAdcDirector *pAdcDirector); virtual tF32 calculate(void) = 0; tF32 getValue(void) const {return value;}; protected: const cAdcDirector *pAdcDirector; tF32 value; };
#include "ivariable.h" //Описание класса #include "susuassert.h" // for ASSERT /******************************************************************************* * Function: constructor * Description: ******************************************************************************/ iVariable::iVariable(const cAdcDirector *pAdcDirector) { ASSERT(pAdcDirector != NULL); this->pAdcDirector = pAdcDirector; this->value = 0.0F; }
#include "types.h" //Стандартные типы проекта class iFilter { public: explicit iFilter(void); virtual tF32 filter(const tF32 previousValue, const tF32 currentValue, tF32 filterConst); };
#include "susuassert.h" // for ASSERT #include "types.h" // для типов проекта #include "ifilter.h" // описание класса /******************************************************************************* * Function: constructor * Description: Задает порты и пины для 4-ех индикаторов ******************************************************************************/ iFilter::iFilter(void) { } /******************************************************************************* * Function: filter * Description: Функция фильтрации ******************************************************************************/ tF32 iFilter::filter(const tF32 previousValue, const tF32 currentValue, tF32 filterConst) { ASSERT(filterConst != 0); tF32 filteredValue = previousValue; filteredValue = filteredValue + (currentValue - filteredValue) / filterConst; return filteredValue; }
#include "types.h" //Стандартные типы проекта #include "adcdirector.h" //для класса cAdcdirector #include "ifilter.h" //для интрефейса iFilter #include "iVariable.h" //для интрефейса iVariable class cTemperature : public iVariable, private iFilter { public: explicit cTemperature(cAdcDirector *pAdcDirector); tF32 calculate(void); };
#include "temperature.h" //Описание класса //Разница 110С - 30С (температура в точках калибровки), см стр 289 #define DELTA_110_30 80.0F //процессор нагревается сам немного, поэтому коррекция на 28 градусов, а не на 30 :) #define DEGREE_30 28.0F //Адрес коэффицента калибровки 2 стр 102 CD00277537.pdf #define TS_CAL2_ADDR 0x1FF8007C //Адрес коэффицента калибровки 1 стр 102 CD00277537.pdf #define TS_CAL1_ADDR 0x1FF8007A //Адрес кода VDDA при 3.0 В #define VDDA_CAL_ADDR 0x1FF80076 #define FILTER_CONST 20.0F /******************************************************************************* * Function: constructor * Description: ******************************************************************************/ cTemperature::cTemperature(cAdcDirector *pAdcDirector) : iVariable(pAdcDirector) { } /******************************************************************************* * Function: calculate * Description: Расчет температуры ******************************************************************************/ tF32 cTemperature::calculate(void) { tF32 temperature = 0.0F; //измеренная температура по одному отсчету АЦП tF32 vdda = 0.0F; //значение кода vdda //коэффициенты калибровки температурного сенсора, см стр 289 CD00240193.pdf и //стр 102 CD00277537.pdf tF32 tsCal2 = (tF32)(*((tU32 *)(TS_CAL2_ADDR)) >> 16); tF32 tsCal1 = (tF32) (*((tU32 *)(TS_CAL1_ADDR ))); tF32 vddaCal = (tF32)(*((tU32 *)(VDDA_CAL_ADDR)) >> 16); temperature = (tF32)this->pAdcDirector->channelValue[SENSORTEMPERATURE_CHANNEL]; vdda = (tF32)this->pAdcDirector->channelValue[VDDA_CHANNEL]; //поскольку все коэффициенты были получены на производсве при 3.0 В VDDA, //нам необходимо сделать коррекцию на наше значение vdda, остальное //формула со см стр 289 CD00240193.pdf temperature = DELTA_110_30 * ((temperature * vddaCal)/vdda - tsCal1) / (tsCal2 - tsCal1) + DEGREE_30; this->value = this->filter(this->value, temperature, FILTER_CONST); return this->value; }
Теперь нужно сделать активный объект который будет переодически вызывать расчет температуры. Да ивообще в последствии будет контейнером для перменных, через него мы будем иметь доступ к переменным. Холст-кисть и вуаля:
Реализация проста до безобразия:
#include "iActiveObject.h" //Для интерфейса iActiveObject #include "temperature.h" //для класса cTemperature class cVariablesDirector : public iActiveObject { public: explicit cVariablesDirector(cAdcDirector* pAdcDirector); void run(void); cTemperature *pTemperature; };
#include "variablesdirector.h" // описание класса #include "frtoswrapper.h" // для oRTOS #include "susuassert.h" // для ASSERT #define VARIABLESDIRECTOR_DELAY (tU32)40/portTICK_PERIOD_MS /******************************************************************************* * Function: constructor * Description: включает АЦП ******************************************************************************/ cVariablesDirector::cVariablesDirector(cAdcDirector* pAdcDirector) { ASSERT(pAdcDirector != NULL); this->pTemperature = new cTemperature(pAdcDirector); } /******************************************************************************* * Function: run * Description: Задача расчета температуры ******************************************************************************/ void cVariablesDirector::run(void) { for(;;) { this->pTemperature->calculate(); oRTOS.taskDelay(VARIABLESDIRECTOR_DELAY); } }
Осталась самое малое — запустить и проверить. Перед этим конечно же надо создать еще одну задачу в main()
#include <stm32l1xx.h> // Регистры STM2 #include "ledsdirector.h" // Для класса cLedsDirector #include "buttonscontroller.h" // Для класса cButtonsController #include "types.h" // Для типов проекта #include "frtoswrapper.h" // для cRtos #include "variablesdirector.h" // Для cVariablesDirector #define LEDS_TASK_HANDLE_INDEX 0 #define BUTTON_TASKS_NOTYFIED_NUM 1 #define LEDSDIRECTOR_STACK_SIZE configMINIMAL_STACK_SIZE #define LEDSDIRECTOR_PRIORITY (tU32)2 #define BUTTONSCONTROLLER_STACK_SIZE 256//configMINIMAL_STACK_SIZE #define BUTTONSCONTROLLER_PRIORITY (tU32)3 #define VARIABLESDIRECTOR_STACK_SIZE (tU16) configMINIMAL_STACK_SIZE #define VARIABLESDIRECTOR_PRIORITY (tU32)2 // Не охота было заморачиваться с синглтоном, сделал oRTOS глобальным объектом // можно было конечно сделать сRTOS статическим, но че-то тоже заморочек много // зато просто, все равно всем нужен :) cRTOS oRTOS; .... void main( void ) { //задача ButtonControllera должна оповещать другие задачи о нажатии //на кнопку, и передавать её значение. Для этого заводим массив указателей на //задачи, которые надо оповещать static tTaskHandle tasksToNotifyFromButton[BUTTON_TASKS_NOTYFIED_NUM]; cAdcDirector *pAdcDirector = new cAdcDirector(); pAdcDirector->startConversion(); cVariablesDirector *pVariablesDirector = new cVariablesDirector(pAdcDirector); oRTOS.taskCreate(pVariablesDirector, VARIABLESDIRECTOR_STACK_SIZE, VARIABLESDIRECTOR_PRIORITY, "Var"); cLedsDirector *pLedsDirector = new cLedsDirector(); oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds"); tasksToNotifyFromButton[LEDS_TASK_HANDLE_INDEX] = pLedsDirector->taskHandle; cButtonsController *pButtonsController = new cButtonsController(tasksToNotifyFromButton, BUTTON_TASKS_NOTYFIED_NUM); oRTOS.taskCreate(pButtonsController, BUTTONSCONTROLLER_STACK_SIZE, BUTTONSCONTROLLER_PRIORITY, "Buttons"); oRTOS.startScheduler(); }
И опять проверка возможна только под отладчиком, поскольку выводить информацию пока что некуда. И вот загружаем ставим точку остановки на задаче переодического расчета температуры, жмем раз 40 F5(Run), чтобы фильтр устаканился и смотрим на значение температуры — выделно красным 23.68 С, ну по ощущениям так есть, ну возможно 23.62 С 🙂
Сохраняем проект, чтобы не забыть: Кнопки, Светодиоды, Температура на IAR 6.50
Тот же самый фокус повторяем с переменными Vdda и Trimmer (переменный резистор). Архитектура идентичная архитектуре класса cTemeperature.
А сам контейнер перменных и по совместительству активный объект — cVariableDirector стал выглядеть вот так:
Добавляем вызов расчета напряжений в cVariableDirector
#include "variablesdirector.h" // описание класса #include "frtoswrapper.h" // для oRTOS #include "susuassert.h" // для ASSERT #define VARIABLESDIRECTOR_DELAY (tU32)40/portTICK_PERIOD_MS /******************************************************************************* * Function: constructor * Description: включает АЦП ******************************************************************************/ cVariablesDirector::cVariablesDirector(cAdcDirector* pAdcDirector) { ASSERT(pAdcDirector != NULL); this->pTemperature = new cTemperature(pAdcDirector); this->pVdda = new cVdda(pAdcDirector); this->pTrimmer = new cTrimmer(pAdcDirector); } /******************************************************************************* * Function: run * Description: Задача расчета температуры ******************************************************************************/ void cVariablesDirector::run(void) { for(;;) { this->pTemperature->calculate(); this->pVdda->calculate(); this->pTrimmer->calculate(); oRTOS.taskDelay(VARIABLESDIRECTOR_DELAY); } }
Запускаем на отладку и получаем следующий результат(красные циферки атрибутов value): Как видно температура 23.5С, Vdda как и написано в документации 2.72, а напряжение на потенциометре 2.52 (но его можно менять, повервнут ролик резистора)
Ну вот и реализовано основное требование проекта, надо признаться провозился я с ним дольше запланированного — почти 7 дней. Но это больше из-за непоняток с напряжением переменного резистора, очень долго тупил, почему не измеряется ничего. Хорошо додумался подсмотреть у разработчиков платы Olimex 🙂 Осталась одна небольшая задача — вывод на индикатор. Я подумал, что ничего сложного не будет, поскольку тот мой последний проект 8-летней давности как раз был на PIC16 со встроенным драйвером индикатора, и уж индикатор то мне дастся очень просто. А как это получилось, я расскажу в заключительной части.
Да забыл совсем — сохранил же проект тут:
Кнопки, Cветодиоды, и все переменные на IAR 6.50
ссылка на оригинал статьи http://habrahabr.ru/post/261823/
Добавить комментарий