STM32, C++ и FreeRTOS. Разработка с нуля. Часть 2
Введение
В прошлой публикации 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 // Регистры 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 // Регистры 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