Введение
В прошлой публикации STM32, C++ и FreeRTOS. Разработка с нуля. Часть 1 я остановился на том,
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()
Настройк АЦП и DMA
//включаем потенциометр(Триммер) 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;
Сами файлы реализации классов:
adc.h
#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); };
adc.cpp
#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); }
adcdirector.h
#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; };
adcdirector.cpp
#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 ******************************************************************************/ void cAdcDirector::startConversion(void) { this->pAdc->startConversion(); }
Проверить работу можно было только под отладчиком, потому что вывода на индиктор у меня пока нет. Но перед этим нужно добавить создание нового экзмпляра класса в main()
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
Разработка: Переменные
И так АЦП вроде бы работает, настало время превратить груду этих единиц и ноликов в что-то понятное людям, а имеено в температуру и напряжение:
Для начала я продумал единый интерфейс для всех переменных. Здесь всего один виртуальный метод — собтсвенно расчет и один метод получения рассчитанного значения.

А далее нарисовал как могла бы выглядеть температура:

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

Класс температуры рализует метод расчета интерфейса. Но тут следует сделать ремарку, для расчета температуры используются заводские коэффициенты зашитые в микроконтроллере, и по идее, чтобы можно было портировать данный код на другую платформу надо бы сделать класс для доступа к энергонезависимым параметрам и передавать ссылку на этот класс классам, которым эти коэффициенты нужны, в данном случае температуре. Но времени у меня в обрез и делать городульку из-за трех коэффциентов очень не хотелось, поэтому спишем такой косяк на нехватку времени и оставим галочку в уме, что тут портация не выйдет(ну и ладно :)). Рализация всего этого дела выглядит так:
ivariable.h
#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; };
ivariable.cpp
#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; }
temperature.h
#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); };
temperature.cpp
#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; }
Тот же самый фокус повторяем с переменными Vdda и Trimmer (переменный резистор). Архитектура идентичная архитектуре класса cTemeperature.
Ну вот и реализовано основное требование проекта, надо признаться провозился я с ним дольше запланированного — почти 7 дней. Но это больше из-за непоняток с напряжением переменного резистора, очень долго тупил, почему не измеряется ничего. Хорошо додумался подсмотреть у разработчиков платы Olimex :) Осталась одна небольшая задача — вывод на индикатор. Я подумал, что ничего сложного не будет, поскольку тот мой последний проект 8-летней давности как раз был на PIC16 со встроенным драйвером индикатора, и уж индикатор то мне дастся очень просто. А как это получилось, я расскажу в заключительной части.
