STM Урок 100. LAN8720. LWIP. TCP. Соединяем два контроллера. Часть 1



Продолжаем работать с микросхемой сетевого физического уровня 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 в экране с проводом

Дисплей LCD 20×4 можно приобрести здесь LCD 20×4

Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

STM LAN8720. LWIP. TCP. Соединяем два контроллера

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*