Продолжаем работать с микросхемой сетевого физического уровня LAN8720, а также библиотекой стека протоколов LWIP.
Мы уже рассмотрели работу TCP-клиента и TCP-сервера на данной микросхеме, используя RAW API, а также используя для этого отладочную плату STM32F4-Discovery. Теперь нам всем, я думаю, будет интересно наши сервер и клиент между собою соединить, то есть использовать только контроллеры и микросхемы LAN. Вот этим мы сегодня и займёмся. Мы в результате полностью отключимся от ПК, то есть не будем использовать даже USART. На сервере мы для вывода информации будем использовать символьный дисплей LCD2004 на контроллере HD44780, с которым мы очень хорошо знакомы из прошлых уроков. Подключим мы его с помощью известного также нам переходника к шине I2C. А к клиенту мы подключим датчик температуры DS18B20 и будем его показания температуры отправлять через сеть на сервер. Вот такие вообщем планы. Сначала я планировал отправлять просто какие-то строки и принимать их также на сервере и мониторить с помощью USART, но это оказалось слишком просто и неинтересно. Поэтому я вот так вот решил разнообразить урок и тем самым приблизить его, так сказать, к реальным жизненно-бытовым условиям.
Вся наша схема на практике будет выглядеть приблизительно вот так:
Только к схеме ещё не присоединено питание к платам, чтобы не мешались лишние провода, а так, если присоединить, в принципе, всё уже работает и температура передаётся. Поэтому начну теперь делиться своей разработкой с вами. Только сразу мы соединять наши платы не будем, вынем этот оранжевый провод и доведём до ума сначала клиент, а потом сервер.
Начнём с клиента. Проект возьмём из урока 96 под названием LAN8720_TCP_CLIENT и переименуем нашим хитрым образом его в LAN8720_TCP_CLIENT_DS18B20.
Откроем наш проект в Cube MX и включим на вход ножку порта PE6, к ней мы и подсоединим наш датчик температуры.
Идём в Configuration в ETH и проверяем, чтобы был правильный адрес PHI. У меня например на клиенте модуль от WaveShare, значит должен быть 1
Далее генерируем проект, открываем его в System Workbench и начинаем с ним работать. Как всегда уберём все настройки отладчика и установим уровень оптимизации в 1.
Модуль LAN соединим с ПК, так как надо пока его будет настроить. То есть в роли сервера пока по-прежнему будет ПК, успеем ещё соединить. USART также подключим.
Как всегда, запустим WireShark, терминальную программу CoolTerm и командую строку, в которой попробуем попинговать наш клиент.
Если всё нормально, то вернёмся к проекту.
Из проекта урока 94 по датчику температуры возьмём и скопируем по соответствующим папкам нашего нового проекта файлы ds18b20.h и ds18b20.c. Обновим дерево проекта и в файле ds18b20.h исправим подключенную библиотеку, так как у нас 4 серия контроллеров
#include "stm32f4xx_hal.h"
Перейдём в файл main.h и подключим нашу библиотеку, а также библиотеку по работе со строками
/* USER CODE BEGIN Includes */
#include "ds18b20.h"
#include <string.h>
/* USER CODE END Includes */
А в файле main.c в функции main() вызовем функцию инициализации нашего датчика и порта, а старт таймера перенесём ниже всех инициализаций. Код станет теперь таким
/* USER CODE BEGIN 2 */
net_ini();
port_init();
ds18b20_init(SKIP_ROM);
HAL_TIM_Base_Start_IT(&htim2);
HAL_UART_Receive_IT(&huart6,(uint8_t*)str,1);
/* USER CODE END 2 */
Также в нашем main.c создадим и подключим некоторые глобальные переменные и массивы
extern char str[30];
extern char str1[100];
uint8_t Dev_ID[8][8]={0};
uint8_t Dev_Cnt;
uint8_t dt[8];
uint16_t raw_temper;
float temper;
char c;
volatile uint32_t Tim2Cnt=0;
extern struct tcp_pcb *client_pcb;
/* USER CODE END PV */
В бесконечном цикле мы не будем опрашивать показания датчика, нечего там тормозить процессы, всё это мы проделаем позже в процедуре обработки прерываний от таймера, а в бесконечном цикле мы вызов двух функций объединим в вызов одной функции
/* USER CODE BEGIN 3 */
MX_LWIP_Process();
}
Это было сделано не только для того, чтобы уменьшить текст кода, но и для того, что в том файле, где лежит тело данной функции, можно добавлять свой код, так как есть там подобающие для этого комментарии.
Теперь нам просто необходимо перейти в файл ds18b20.c и внести там соответствующие изменения в низкоуровневой код, который использует библиотеку CMSIS, так как для контроллера STM32F407 такие регистры, которые мы там использовали, неприменимы ибо у него их просто нет. У него несколько другие регистры. Я не буду показывать вам Reference Manual, я думаю вы его уже 100 раз читали. Мы просто те же самые процедуры исправим с применением существующих регистров.
Начнём с функции DelayMicro и исправим там строку, в которой делитель я подсчитал экспериментально
micros *= SystemCoreClock / 4940000;
Далее исправим код в функции port_init, заодно с регистрами исправляя и ножку порта, так как она тоже изменилась
void port_init(void)
{
HAL_GPIO_DeInit(GPIOE, GPIO_PIN_6);
GPIOE->MODER &= ~GPIO_MODER_MODE6_1;
GPIOE->MODER |= GPIO_MODER_MODE6_0;
GPIOE->OSPEEDR |= GPIO_OSPEEDR_OSPEED6_1;
GPIOE->OSPEEDR |= GPIO_OSPEEDR_OSPEED6_0;
GPIOE->OTYPER |= GPIO_OTYPER_OT6;
}
Далее исправляем код в функции ds18b20_Reset
GPIOE->ODR &= ~GPIO_ODR_OD6;//низкий уровень
DelayMicro(485);//задержка как минимум на 480 микросекунд
GPIOE->ODR |= GPIO_ODR_OD6;//высокий уровень
DelayMicro(65);//задержка как минимум на 60 микросекунд
status = GPIOE->IDR & GPIO_IDR_ID6;//проверяем уровень
Затем функция ds18b20_ReadBit
GPIOE->ODR &= ~GPIO_ODR_OD6;//низкий уровень
DelayMicro(2);
GPIOE->ODR |= GPIO_ODR_OD6;//высокий уровень
DelayMicro(13);
bit = (GPIOE->IDR & GPIO_IDR_ID6 ? 1 : 0);//проверяем уровень
И функция ds18b20_WriteBit
GPIOE->ODR &= ~GPIO_ODR_OD6;
DelayMicro(bit ? 3 : 65);
GPIOE->ODR |= GPIO_ODR_OD6;
Теперь у нас проект скорей всего соберётся. Попробуем собрать проект, если всё нормально, то движемся дальше.
Перейдём в файл ds18b20.c и исправим там код функции ds18b20_Convert, убрав оттуда всё лишнее.
Иначе у нас отрицательная температура показывается неправильно. Просто у меня не было возможности
проверить, а сейчас, когда на улице морозы, такая возможность появилась. Я просто подключил датчик,
который на проводе к макетной плате и выкинул его в окно, закрыв окно на проводе, в окне есть резинка,
поэтому с проводом ничего не случится. Код в функции будет теперь вот такой
float ds18b20_Convert(int16_t dt)
{
float t;
t = (float)dt / 16.0f;
return t;
}
В прототипе также поменяем тип входного параметра.
Создадим прототип на функцию отправки строки в сеть в заголовочном файле net.h
void UART6_RxCpltCallback(void);
void sendstring(char* buf_str);
Нам теперь нужно вовремя подать команду в датчик, чтобы он начал читать температуру, затем подать команду вовремя, чтобы датчик нам эти показания отдал. Для этого мы перейдём в файл main.c и добавим обработчик прерывания от таймера
//-----------------------------------------------
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim2)
{
Tim2Cnt++;
if(Tim2Cnt>10)
{
}
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}
}
//----------------------------------------------------------
/* USER CODE END 4 */
Таймер, как мы помним у нас работает с периодом приблизительно в полсекунды, поэтому мы сначала секунд 5 подождём, то бишь дадим некоторое время на то, чтобы наши узлы благополучно соединились, а затем мы уже попадём в тело условия, тело которого мы сейчас и начнём писать
if(Tim2Cnt>10)
{
if(client_pcb->state == ESTABLISHED)
{
if(Tim2Cnt%4==0)
{
ds18b20_MeasureTemperCmd(SKIP_ROM, 0);
}
else if(Tim2Cnt%4==2)
{
ds18b20_ReadStratcpad(SKIP_ROM, dt, 0);
raw_temper = ((int16_t)dt[1]<<8)|dt[0];
temper = ds18b20_Convert(raw_temper);
if(temper>0) sprintf(str1,"+%.2f rn", temper);
else sprintf(str1,"%.2f rn", temper);
HAL_UART_Transmit(&huart6,(uint8_t*)str1,strlen(str1),0x1000);
sendstring(str1);
}
}
}
Оказалось всё очень даже просто. Мы делим на 4 по модулю, и когда результат будет 0, то мы говорим датчику измерять температуру, а когда 2 — то нам её отдавать. А затем мы её выводим в USART в терминальную программу и, самое главное, отправляем нашему серверу.
Также, чтобы температура корректно считалась, то нам необходимо в проекте включить работу с плавающей точкой. Для этого зайдём в свойства проекта в раздел C/C++ Build -> Settings -> MCU GCC Linker -> Miscellaneous -> Linker flags в данную строку добавить ещё « -u _printf_float» (не забывайте про пробел)
Только прежде чем данные показания отправятся на сервер, нам надо ещё с ним соединиться. Поэтому идём в файл net.c и сначала добавим там ещё одну глобальную переменную, по которой мы будем следить за состоянием нашего соединения
__IO uint32_t message_count=0;
__IO uint8_t net_stat=0;
Также добавим в структуру ещё один вид состояния соединения
ES_NOT_CONNECTED = 0,
ES_CONNECTING,
ES_CONNECTED,
Добавим код в функцию net_ini, чтобы клиент сразу устанавливал соединение с сервером
usartprop.is_text=0;
ipaddr_dest[0] = 192;
ipaddr_dest[1] = 168;
ipaddr_dest[2] = 1;
ipaddr_dest[3] = 87;
port_dest = 7;
net_stat = ES_CONNECTING; //Инициализируем попытку соединения
tcp_client_connect();
Теперь давайте соберём код, прошьём наш контроллер и посмотрим, приходит ли наша температура на сервер (пока ПК). Пред этим не забываем запусить netcat и заставить его слушать порт 7
Отлично! Температура у нас начала передаваться на сервер.
Также есть у меня ещё одна затея. Если у нас вдруг что-то случится с сервером и он станет недоступным, и нам не будет смысла тогда держать открытым соединение и посылать куда-то пакеты, то попробуем мы эту оказию отследить и разъединиться с ним. Ну вернее корректного разъединения не произойдёт, раз он недоступен, но мы хотя бы все наши необходимые процедуры проделаем, освободим память под структуру и т.д. и пакеты у нас уже никуда отправляться не будут. Да и температура также перестанет считываться, так как у нас не будет сатуса «Соединено» и мы просто в соответствующие тела условий не попадём.
Только оказалось не так легко отследить данные вещи. По идее оно просто. Нам перестанут какое-то время приходить подтверждения на пакеты и мы можем измерять максимальное время между подтверждениями. Но вот ведь беда, эти подтверждения не доходят до наших обработчиков и теряются где-то в низкоуровневых функциях стека протоколов, а туда нам нет смысла со своими заглушками соваться, так как это не предусмотрено и при следующей перегенерации проекта всё это сотрётся. Но есть одно интересное свойство нашей структуры, где есть номер сегмента последнего подтверждения. И соответственно, если не будет этих подтверждений, то данный номер будет оставаться постоянным, то есть он не будет увеличиваться. Вот это-то мы и отследим.
Сначала давайте в функции разрыва соединения мы погасим все светодиоды и зажжем один зелёный, чтобы нам увидеть это как-то
tcp_close(tpcb);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
}
Подключим глобальную переменную счётчика таймера
extern UART_HandleTypeDef huart6;
extern volatile uint32_t Tim2Cnt;
Перейдём в функцию tcp_client_poll и добавим там статическую переменную
err_t ret_err;
static uint32_t cur_lastack=0;
Теперь перед циклом, в котором пустует его тело, добавим следующий код
if (es != NULL)
{
//Проверяем, что номер сегментов подтверждений от сервера наращивается
if(Tim2Cnt>20)
{
if(Tim2Cnt%40==0)
{
sprintf(str1,"%lu, %lurn", tpcb->lastack, Tim2Cnt);
HAL_UART_Transmit(&huart6,(uint8_t*)str1,strlen(str1),0x1000);
if(cur_lastack==tpcb->lastack)
{
es->state = ES_CLOSING;
tcp_client_connection_close(tpcb, es);
net_stat = ES_CONNECTING;
Tim2Cnt = 0;
}
cur_lastack = tpcb->lastack;
}
}
if (es->p_tx != NULL)
Тут всё тоже просто. Опять же при помощи операции вычисления остатка от деления мы приблизительно раз в 20 секунд сверяем сохранённое значение поля структуры с текущим значением и, если они вдруг равны, то, понятное дело, пакеты не ходили между узлами, и в этом случае мы закрываем соединение, расставляем соответствующие статусы, а также обнуляем счётчик таймера.
Соберём наш код, прошьём контроллер, пусть немного попередаются данные и мы затем отключим кабель сети. Через некоторое время клиент должен отключиться и на плате загорится зелёный светодиод.
Конечно была задумка, чтобы клиент потом начал запрашивать соединение у сервера и при появлении последнего чтобы оно создалось. Но добиться этого мне не удалось. Соединения становились в очередь и при появлении сервера они все применялись, то есть с сервером сразу устанавливалось 5 соединений (такое значение у нас установлено в свойствах LWIP). Тут уже нужно знать работу с виртуальными сокетами в режиме RAW API, но мы пока до этого не дошли. Хотя бы что-то мы отследили. Уже какая-никакая автоматика. То есть при возобновлении работы сервера, нам нужно будет клиент перезагрузить.
Перед тем, как мы начнём работать с проектом сервера, мы должны в проекте по клиенту изменить IP-адрес в функции инициализации на адрес нашего будущего сервера
ipaddr_dest[0] = 192;
ipaddr_dest[1] = 168;
ipaddr_dest[2] = 1;
ipaddr_dest[3] = 191;
В следующей части нашего урока мы поработаем с проектом для сервера, затем соединим наши платы по сети LAN и проверим работу нашего кода.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату можно приобрести здесь STM32F4-DISCOVERY
Модуль LAN можно приобрести здесь: LAN8720
Плату расширения можно приобрести здесь: STM32F4DIS-BB
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Датчик температуры DS18B20 в экране с проводом можно приобрести здесь DS18B20
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий