
Я уже рассказывал, как можно подключить DHT11 или DHT22 к STM32 с программной реализацией протокола. Теперь хотел бы поделится способом, где часть работы переложена на DMA и таймер
Программная реализация описана в статье Подключение DHT11 и DHT22 к STM32F103C8T6.
Обмен данными STM32F103 с DHT11(DHT22).
Более подробно описано в статье по ссылке выше, здесь продублирую для удобства некоторую информацию.
Датчик питается примерно от 3.3 до 5.5В. Линия данных (2 пин датчика) подтягивается к питанию.
Инициализация датчика DHT11 более 18 ms, для датчика DHT22 более 1 ms.
Датчик передает сигнал присутствия прижимая линию к 0 на 80 us и отпуская на 80 us.
Затем следуют 40 бит от старшего к младшему. Каждый бит начинается с прижатия линии к 0 на 50 us. Засекам этот промежуток и сравниваем с последующим промежутком, в котором линия будет поднята к питанию. Если линия поднята к питанию на время большее чем была прижата к 0 - значит мы получили 1, если меньше - значит пришел 0.
Что такое DMA в STM32?
Главной задачей DMA (Direct Memory Access), или в русском ПДП (Прямой Доступ к Памяти), является обмен информацией на аппаратном уровне между памятью мк и периферией.
DMA принимает указатель на память в мк (регистр CMARx) и на адрес регистра периферии (регистр CPARx). С помощью регистров CNDTR и CCR можно указать сколько данных хранится в памяти, размер этих данных, нужно ли обходить их смещая указатель, нужно ли обходить данные по кругу, а так же направление передачи данных от периферии или в нее.
Более подробно опишу в коде, и в отдельной статье.
Опрос датчика DHT11(DHT22) с DMA, PWM и TIMx.
Нога подтянута к питанию. Затем прижимаем ее к земле для инициализации на нужное время и отпускаем.
Настроим таймер на 500 ms. С помощью PWM (ШИМ) будем прижимать ногу к питанию, а передавать скважность PWM будет с помощью DMA. Затем замеряем с помощью таймера промежутки между спадами питания. Складируем с помощью DMA в массив. В конце обрабатываем наш массив и получаем данные температуры и влажности.
У STM32F103 есть 1 DMA.
Для подключения датчика я выбрал порт PB6, так как он толерантен к 5В и имеет привязку к таймеру и DMA (ПДП). Я использую резистор в 10К для подтяжки линии данных к питанию.
Нам нужны DMA Channel 7, который будет брать данные из массива, в котором будет скважность ШИМ, и отправлять в таймер. И Channel 4, который будет слушать второй канал TIM4 и складывать замеры в массив.
Дальше буду пояснять в комментариях к коду.
#define DHT_TIM_EN RCC_APB1ENR_TIM4EN #define DHT_TIM TIM4 #define DHT_TIM_IRQ TIM4_IRQHandler #define DHT_DMA DMA1 #define DHT_DMA_CH_PWM DMA1_Channel7 #define DHT_DMA_CH_IN DMA1_Channel4 #define DHT_DMA_IRQ DMA1_Channel4_IRQHandler // включить тактирование DMA #define DHT_DMA_ENABLE() RCC->AHBENR |= RCC_AHBENR_DMA1EN // выключить тактирование DMA #define DHT_DMA_DISABLE() RCC->AHBENR &= ~RCC_AHBENR_DMA1EN // проверить включено ли тактирование // ниже поясню #define DHT_DMA_ENABLED() ((RCC->AHBENR & RCC_AHBENR_DMA1EN) != 0) #define DHT_TIM_ENABLE() RCC->APB1ENR |= DHT_TIM_EN #define DHT_ENABLE_IRQ() NVIC_EnableIRQ(TIM4_IRQn);NVIC_EnableIRQ(DMA1_Channel4_IRQn) // размер массива для установки в CCR1 #define DHT_DELAY_SIZE 4 // размер буфера измерений канала 2 #define DHT_BUF_SIZE 42 uint16_t DHTTimDelay[DHT_DELAY_SIZE] = { 0xffff, 48199, 0xffff, 0xffff }; uint16_t DHTBuff[DHT_BUF_SIZE] = { 0 }; typedef struct { uint8_t HI; uint8_t HD; uint8_t TI; uint8_t TD; uint8_t crc; } temp_s; temp_s data; #define DHT_SIZE 5 // size of temp_s #define DHT_GPIO GPIOB #define DHT_PIN GPIO_Pin_6
С макросами понятно, просто вспомогательные макросы для удобства использования и переноса.
Бит у нас 40, но размер буфера 42, так как мы измеряем промежутки между заваливанием фронтов, то туда попадет еще и сигнал присутствия.
Число 48200 поясню позже.
Нога настраивается как альтернативная функция с открытым стоком для работы с таймером, что бы она могла прижимать к земле.
void initGPIO() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitTypeDef port; GPIO_StructInit(&port); port.GPIO_Mode = GPIO_Mode_AF_OD; port.GPIO_Pin = DHT_PIN; port.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT_GPIO, &port);
Дальше настройка таймера, DMA и обработчики прерываний
void DHT_DMA_Init(void) { DHT_DMA_CH_PWM->CCR = 0; // задаем адрес в памяти мк // отсюда DMA будет брать данные и передавать в таймер DHT_DMA_CH_PWM->CMAR = &DHTTimDelay[0]; // указатель на регистр периферии // сюда DMA будет писать данные взятые из памяти DHT_DMA_CH_PWM->CPAR = &DHT_TIM->CCR1; // размер массива, для того что бы DMA знал сколько раз он может перемещаться по массиву DHT_DMA_CH_PWM->CNDTR = DHT_DELAY_SIZE; // PL - устанавливаем самый высокий приоритет канала // MSIZE и PSIZE - указываем что размер в памяти и периферии данных 16 бит соответсвенно // MINC - говорит что по данным нужно перемещаться // CIRC - Сircular режим, то есть по достижении концы данных сбросить счетчик и начать сначала // DIR - направление передачи данных, из памяти в периферию DHT_DMA_CH_PWM->CCR = DMA_CCR7_PL | DMA_CCR7_MSIZE_0 | DMA_CCR7_PSIZE_0 | DMA_CCR7_MINC | DMA_CCR7_CIRC | DMA_CCR7_DIR; DHT_DMA->IFCR = DMA_IFCR_CGIF7 | DMA_IFCR_CHTIF7 | DMA_IFCR_CTCIF7 | DMA_IFCR_CTEIF7; // включаем канал DMA DHT_DMA_CH_PWM->CCR |= DMA_CCR1_EN; DHT_DMA_CH_IN->CCR = 0; // задаем адрес куда писать данные из таймера DHT_DMA_CH_IN->CMAR = &DHTBuff[0]; // откуда их брать DHT_DMA_CH_IN->CPAR = &DHT_TIM->CCR2; // сколько раз получить данные до того как сработает прерывание DHT_DMA_CH_IN->CNDTR = DHT_BUF_SIZE; // разрешить прерывание по достижении конца DHT_DMA_CH_IN->CCR = DMA_CCR4_PL | DMA_CCR4_MSIZE_0 | DMA_CCR4_PSIZE_0 | DMA_CCR4_MINC | DMA_CCR4_TCIE; DHT_DMA->IFCR = DMA_IFCR_CGIF4 | DMA_IFCR_CHTIF4 | DMA_IFCR_CTCIF4 | DMA_IFCR_CTEIF4; } void DHT_TIM_Init(void) { // включаем тактирование DMA и таймера DHT_DMA_ENABLE(); DHT_TIM_ENABLE(); // инициализируем DMA каналы DHT_DMA_Init(); // поделим нашу частоту на 720 // 71 999 999 / 720 примерно 99 999.999 тиков в секунду DHT_TIM->PSC = 720; // настроим до куда считать DHT_TIM->ARR = 49999; DHT_TIM->SR = 0; // включаем DMA на канале 2, разрешаем прерывание на 1 канале таймера DHT_TIM->DIER = TIM_DIER_UDE | TIM_DIER_CC1IE | TIM_DIER_CC2DE; // включаем режим PWM 2 и настраиваем что бы // данные приходящие на CH1 переходили на IC2 DHT_TIM->CCMR1 = TIM_CCMR1_OC1M | TIM_CCMR1_CC2S_1; DHT_TIM->CR1 = TIM_CR1_ARPE; DHT_TIM->EGR = TIM_EGR_UG; // указываем, что активным состоянием вывода таймера является 0 (земля) // и включаем ножку мк на работу с таймером // включаем ногу на захват для канала 2 и захват по спадению фронта DHT_TIM->CCER = TIM_CCER_CC1E | TIM_CCER_CC1P | TIM_CCER_CC2E | TIM_CCER_CC2P; DHT_TIM->CR1 |= TIM_CR1_CEN; DHT_ENABLE_IRQ(); } void DHT_DMA_IRQ(void) { if(DHT_DMA->ISR & DMA_ISR_TCIF4) { DHT_DMA->IFCR = DMA_IFCR_CTCIF4; // выключаем DMA DHT_DMA_DISABLE(); } } void DHT_TIM_IRQ(void) { if(DHT_TIM->SR & TIM_SR_CC1IF) { // при переполнении таймера перезапускаем слушающий DMA канал DHT_DMA_CH_IN->CCR &= ~DMA_CCR4_EN; DHT_DMA_CH_IN->CMAR = &DHTBuff[0]; DHT_DMA_CH_IN->CNDTR = DHT_BUF_SIZE; DHT_DMA_CH_IN->CCR |= DMA_CCR4_EN; DHT_TIM->SR &= ~TIM_SR_CC1IF; } }
Немного поясню работу.
У нас есть два канала таймера:
1 канал нужен что бы прижать линию данных к 0 для инициализации
2 канал считает когда приходит очередной бит
PWM у нас в режиме 2, то есть канал 1 неактивен пока CCR1 больше чем счетчик таймера. Активный канал прижимает линию к 0.
Сначала DMA пишет в CCR1 = 0xffff - то есть CCR1 всегда больше, а значит всегда активен, а значит на выходе всегда 1. Это нужно чтобы на датчик какое то время подавалось питание.
Затем DMA запишет новое значение в канал 1 таймера. Это будет 48199, таймер настроен на 49999. То есть пока счет будет меньше чем 48199 на линии продолжит оставаться 1. А в оставшиеся 49999 - 48199 = 1800 тиков таймера будет 0. То есть сигнал старт пойдет примерно через 500 us + 482 us, что хватает для вычисление температуры датчиком.
Режим PWM 2 и я выбрал чтобы прижатие к 0 было в конце, и сразу после него был сброс DMA канала 2, и начать принимать данные в массив с начала нового цикла таймера.
С учетом нашего предделителя в секунду у нас выполняется примерно 100 000 тиков, то есть за 1 ms проходит 100 тиков. 1800 / 100 = 18 ms. Именно по этому я поставил такое число. То есть это команда старта от мк к датчику DHT11.
Дальше DMA передаст число 0xffff, а значит таймер отпустит ногу от 0. В это же время, от датчика пойдет сигнал присутствия на линии и передача бит. Тут наш канал 2 начинает записывать в DTHBuf поочередно промежутки когда был переход в 0.
Первый переход в 0 будет от сигнала присутствия. Последний, по окончании передачи последнего бита. Итого 1 + 40 перед каждым битом + 1 в конце = 42. По этому массив у нас не из 40 байт а из 42.
Каждые 500 ms таймер сбрасывается. В прерывании повторно инициализируем 2 канал DMA.
По даташиту не рекомендуют опрашивать таймер чаще 1-го раза в 2 секунды. У меня на 1 опрос уходит 4 цикла работы таймера * 500 us.
В прерывании не желательно выполнять длительный код, по этому я выключаю DMA после того как данные получены, а в цикле приложения я проверяю, если DMA выключен - значит пришли данные. Привожу данные к нашей структуре и вывожу на экран, после того как данные приведены, можно запускать еще раз таймер.
Выключаю DMA чтобы не вышло так, что в момент перевода данных DMA не мог их переписать.
перевести данные из массива в структуру можно как то так
uint8_t DHTCalc(temp_s *data){ uint8_t cnt, i, b, k, n = 0; uint8_t* buf = data; for (b = 0; b < DHT_SIZE; ++b) { *buf = 0; for (i = 0; i < 8; ++i) { k = (b << 3) + i + 1; cnt = DHTBuff[k + 1] - DHTBuff[k]; *buf |= (cnt > 10) << (7 - i); } buf++; } if (data->crc != data->HD + data->HI + data->TD + data->TI) return ERROR; return SUCCESS; }
а опрашивать так
DHT_TIM_Init(); uint8_t status = 0; while (1) { // если DMA выключен, значит данные есть if (!DHT_DMA_ENABLED()) { // читаем пока выключен DMA status = DHTCalc(&data); // включаем DMA DHT_DMA_ENABLE(); if (status) { // выводим данные ClearLCDScreen(); snprintf(buf, 20, "Temp: %d.%d", data.TI, data.TD); PrintStr(buf); Cursor(1, 0); snprintf(buf, 20, "H: %d.%d", data.HI, data.HD); PrintStr(buf); } else { // что то пошло не так } } }
Для общего понимания возможностей DMA должно хватить.
Комментарии к статье: DMA (ПДП) и Таймер в STM32F103C8T6. Подключаем DHT11 (DHT22).
Спасибо! Очень помогли. Статьи четкие, содержательные, понятные. Буду заходить в надежде увидеть новые.... (например Модбасом :-) )