Top.Mail.Ru

Манчестерский код. Часть 1. Кодирование данных.

Из названия статьи и обложки вы уже знаете, о чем сегодня пойдет речь, так что начинаем без предисловий сразу с главного. Итак, будем осуществлять генерацию манчестерского кода при помощи контроллера STM32. Разумеется, вначале разберемся, что из себя представляет данный тип кодирования в целом. Сразу могу анонсировать следующую статью, которая будет посвящена уже декодированию того сигнала, который мы сгенерируем сегодня, так что следите за обновлениями, добавляйте в закладки и подписывайтесь на всяческие наши группы )

Манчестерский код.

Манчестерское кодирование (код Манчестер-II) представляет из себя способ кодирования исходного двоичного кода двоичным же цифровым сигналом. Или в переводе - на входе единицы и нули, на выходе - единицы и нули, но порядок их следования, естественно, различается.

Существует два основных типа кодирования:

  • кодирование по стандарту IEEE 802.3. В данном случае логический "0" на входе кодируется перепадом напряжения с высокого уровня ни низкий, а логическая "1" - с низкого на высокий. Это мы наглядно рассмотрим чуть ниже, так что все станет окончательно понятно.
  • кодирование по Г. Е. Томасу. Здесь все наоборот: бит "0" - перепад с низкого уровня на высокий, бит "1" - с высокого на низкий.

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

К особенностям манчестерского кода я бы отнес, в первую очередь:

  • какой бы длинной не была исходная последовательность битов, в закодированной последовательности будет одинаковое количество единиц и нулей, что следует из способа кодирования. И это тоже станет более прозрачно при рассмотрении примеров чуть ниже. Из этого факта следует важнейшее свойство, а именно, отсутствие постоянной составляющей сигнала. Что, в свою очередь, делает возможным использование гальванически развязанных элементов для передачи 👍
  • вторым важным свойством является возможность самостоятельной синхронизации приемника. То есть приемнику не требуется ни дополнительная линия с тактовым сигналом, ни даже информация о периоде следования битов в исходной последовательности. Это мы как раз подробно рассмотрим в следующей статье, когда реализуем декодирование.

Наступило время для дважды обещанного наглядного примера. Возьмем два произвольных байта, пусть будут {0x15, 0x65} и закодируем их. Напоминаю, используем манчестерский код по IEEE 802.3. И еще один момент - данные в пределах байта будут следовать от младшего бита к старшему. В общем, смотрим:

Манчестерский код

Каждому "0" на входе соответствует переход от высокого уровня к низкому на выходе (от "1" к "0"), для "1" - аналогично, но наоборот, от низкого к высокому.

На практике создать манчестерский код довольно просто. Для этого используем дополнительный виртуальный тактовый генератор. Виртуальным я его назвал по той причине, что программно этот генератор будет представлять из себя обычную переменную, значение которой будет меняться от "0" к "1" и обратно. При этом период генератора (период изменения значения переменной) должен быть в два раза меньше длительности передачи одного бита в исходной последовательности. Длительность передачи одного бита также называют периодом кодирования. Переходим к наглядности:

Принцип кодирования

В итоге:

бит \medspace манчестерского \medspace кода = бит \medspace тактового \medspace генератора \medspace XOR  \medspace бит \medspace исходного \medspace кода.

Для примера, первый бит: на входе - "1", на тактовом генераторе - "1", код Манчестер-II - "1" ^ "1" = "0".

Аналогично, для второго бита: на входе - "1", на тактовом генераторе - "0", код Манчестер-II - "1" ^ "0" = "1". Именно такой механизм будем использовать в программной реализации, к которой и переходим.

Генератор манчестерского кода на STM32.

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

Настройка STM32 для работы с манчестерским кодом

Не забываем включить интерфейс SWD для отладки и внешнее тактирование. Да, кстати, у меня будет STM32F103C8 с внешним резонатором на 8 МГц. Но, как и всегда, код будет универсальным, чтобы использовать его на любом другом контроллере без малейших проблем. Настройки тактирования традиционные:

Настройки тактирования

То есть тактовая частота таймеров составляет 72 МГц. Настроим TIM2 на переполнение каждые 10 мкс. Для этого берем предделитель 72 (в CubeMx ставим 71), и период, равный 10:

При таком делителе получим частоту таймера равной:

f = \frac{72 \medspace МГц}{72} = 1 \medspace МГц

То есть один "тик" таймера - 1 мкс. С периодом 10 получим желаемые 10 мкс. Использование именно такого периода для таймера не критично, эта величина устанавливается в коде и может быть любой другой. Об этом чуть ниже.

Кроме того, включаем прерывание для таймера и генерируем код. Далее CubeMx нам не понадобится. Добавляем в проект файлы для работы с кодом Манчестер-II:

Структура проекта генератора манчестерского кода

Начнем с определения констант в manchester_code.h - задаем выходной порт и как раз-таки период таймера в мкс:

#define MANCH_OUTPUT_PORT                                       GPIOA
#define MANCH_OUTPUT_PIN                                        GPIO_PIN_3

#define MANCH_ENCODE_TIMER_PERIOD_US                            10
#define MANCH_BIT_TIME_US                                       100

Также тут присутствует третье значение - MANCH_BIT_TIME_US - это длительность передачи одного бита (период кодирования), также в микросекундах. Прерывание по таймеру будет срабатывать каждые 10 мкс, длительность бита - 100 мкс, поэтому рассчитываем сразу, сколько раз таймер должен переполниться до достижения этого значения:

#define MANCH_ENCODE_TIMER_MAX                                  MANCH_BIT_TIME_US / MANCH_ENCODE_TIMER_PERIOD_US

Далее о формате данных. Как мы уже выяснили, приемник может сам синхронизироваться с принимаемыми битами, с этим все гладко. Но приемник никак не может знать, где именно "начало" данных:

Пример манчестерского кода

То есть, приемник самостоятельно настроился на анализ нужных фронтов (в следующей статье про декодер это и будет сделано 👍), но в этот момент он может оказаться в произвольном месте байта. А мы хотим принимать данные ровно в том порядке, в котором отправляем. Поэтому к исходным данным мы будет добавлять 16 бит со специальным кодом синхронизации. В качестве этого кода возьмем, например, значение 0xAA55:

#define MANCH_SYNC_FIELD                                        0xAA55

На данный момент ограничимся тем фактом, что эти 2 байта нам нужно добавить в начало передаваемых данных, соответственно также закодировав их.

Для структуризации данных добавляем:

typedef struct MANCH_Data
{
  uint8_t data[MANCH_BYTES_NUM];
  uint16_t bitStream;
  uint16_t byteIdx;
  uint16_t bytesNum;
  uint8_t bitIdx;
  uint8_t active;
} MANCH_Data;

Пробегаем по полям:

  • data[MANCH_BYTES_NUM] - массив для хранения передаваемых данных. После перечня полей структуры разберемся с размером этого массива
  • bitStream - это поле нужно для декодера, пока о нем умалчиваю
  • byteIdx - индекс текущего кодируемого байта из массива data[]
  • bytesNum - суммарное количество байт, которые нужно закодировать и выдать на выход
  • bitIdx - индекс текущего бита в текущем байте data[byteIdx]
  • active - флаг активности, "1" - процесс генерации активен, "0" - процесс остановлен.

По поводу размера массива - он задается все здесь же в manchester_code.h, пока мы вообще за пределы этого файла даже не выходили:

#define MANCH_BYTES_NUM                                         10
#define MANCH_SYNC_BYTES_NUM                                    2
#define MANCH_DATA_BYTES_NUM                                    MANCH_BYTES_NUM - MANCH_SYNC_BYTES_NUM

Здесь имеем:

  • MANCH_BYTES_NUM - максимальное количество передаваемых байт
  • MANCH_SYNC_BYTES_NUM - размер синхрополя
  • MANCH_DATA_BYTES_NUM - рассчитанное максимальное количество информационных байт, равное 8 в данном случае. 8 я выбрал исключительно для примера и из симпатии к цифре, можно увеличивать до больших значений.

Пойдем поэтапно, manchester_code.c - объявление переменных:

/* Declarations and definitions ----------------------------------------------*/
static uint8_t virtTact = 1;
static MANCH_Data encodeData;
static uint16_t encodeTimerCnt = 0;
extern TIM_HandleTypeDef htim2;
  • virtTact - та самая переменная, которая будет играть роль виртуального тактового генератора при генерации манчестерского кода
  • encodeData - экземпляр нашей структуры - сердце и центр всего процесса
  • encodeTimerCnt - счетчик, его значение будем инкрементировать в прерывании по переполнению таймера
  • htim2 - просто перетягиваем из main.c для использования в этом файле

Сейчас пойдет ряд функций для организации кодирования. Для управления выходом добавляем:

/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
static void SetOutput(uint8_t state)
{
  HAL_GPIO_WritePin(MANCH_OUTPUT_PORT, MANCH_OUTPUT_PIN, (GPIO_PinState)state);
}



/*----------------------------------------------------------------------------*/

Далее по списку и по важности - функция, которая запускает процесс манчестерского кодирования:

/*----------------------------------------------------------------------------*/
void MANCH_Encode(uint8_t* data, uint8_t size)
{
  encodeData.bitIdx = 0;
  encodeData.byteIdx = 0;
  
  if (size > MANCH_DATA_BYTES_NUM)
  {
    encodeData.bytesNum = MANCH_DATA_BYTES_NUM + MANCH_SYNC_BYTES_NUM;
  }
  else
  {
    encodeData.bytesNum = size + MANCH_SYNC_BYTES_NUM;
  }
  
  memcpy(&encodeData.data[MANCH_SYNC_BYTES_NUM], data, encodeData.bytesNum - MANCH_SYNC_BYTES_NUM);
  encodeData.data[0] = MANCH_SYNC_FIELD & 0xFF;
  encodeData.data[1] = (MANCH_SYNC_FIELD & 0xFF00) >> 8;
  
  encodeTimerCnt = 0;
  virtTact = 1;
  encodeData.active = 1;
}



/*----------------------------------------------------------------------------*/

В качестве аргументов - указатель на информационные данные и их размер (кол-во байт). Соответственно, передавать в функцию мы будем набор uint8_t значений, которые затем побитно будут кодироваться и выдаваться на PA3. На всякий случай напоминаю, что кодировать данные будем от младшего бита к старшему. То есть если на входе байт 0x22, что в двоичном виде представляет из себя 0b00100010, то на выходе будут последовательно закодированные "0", "1", "0", "0", "0", "1", "0", "0".

Итак, в функции обнуляем счетчики текущего байта и бита, а также проверяем размер данных на превышение максимально возможного количества информационных байт. В случае превышения сохраняем в bytesNum максимально допустимое значение. Далее копируем информационные данные в структуру, но начиная со 2-го байта (индекс = MANCH_SYNC_BYTES_NUM):

memcpy(&encodeData.data[MANCH_SYNC_BYTES_NUM], data, encodeData.bytesNum - MANCH_SYNC_BYTES_NUM);

Делаем так по той причине, что первые два передаваемых байта будут синхрополем, которое и помещаем в массив encodeData.data[].

Следующая функция возвращает текущее значение бита на основе данных структуры MANCH_Data:

/*----------------------------------------------------------------------------*/
static uint8_t GetDataBit(MANCH_Data* manchData)
{
  uint8_t res;
  
  uint8_t curByte = manchData->data[manchData->byteIdx];
  uint8_t curBitIdx = manchData->bitIdx;
  
  res = (curByte >> curBitIdx) & 0x01;
  
  return res;
}



/*----------------------------------------------------------------------------*/

В manchData->data[] у нас данные, в manchData->byteIdx - номер байта, в manchData->bitIdx - номер бита. В результате функция возвращает значение этого бита в этом байте в этих данных.

Все, наконец, переходим к прерыванию таймера, в котором соберем воедино все, что было подробно разобрано:

/*----------------------------------------------------------------------------*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim == &htim2)
  {
    // Encoding process
    if (encodeData.active == 1)
    {
      if ((encodeTimerCnt == (MANCH_ENCODE_TIMER_MAX / 2)) ||
          (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX))
      {
        uint8_t curCodeBit = GetDataBit(&encodeData);
        uint8_t curOutputBit = curCodeBit ^ virtTact;
        SetOutput(curOutputBit);
        virtTact ^= 0x01;
      }
            
      if (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX)
      {        
        encodeData.bitIdx++;
        
        if (encodeData.bitIdx == (MANCH_BITS_IN_BYTE_NUM))
        {
          encodeData.bitIdx = 0;
          
          encodeData.byteIdx++;
          if (encodeData.byteIdx == encodeData.bytesNum)
          {
            encodeData.active = 0;
          }
        }
        
        encodeTimerCnt = 0;
      }
    
      encodeTimerCnt++;
    }
}

Все действия осуществляем при активном флаге:

if (encodeData.active == 1)

Разберемся, что мы имеем в итоге с временными интервалами при текущей конфигурации.

В функцию HAL_TIM_PeriodElapsedCallback() мы попадаем каждые 10 мкс. При этом период кодирования равен 100 мкс. Каждый раз заходя в callback инкрементируем счетчик encodeTimerCnt, что приведет к тому, что через 100 мкс его значение будет равно рассчитанному ранее значению MANCH_ENCODE_TIMER_MAX. Далее отсчитывать не требуется, поэтому обнуляем счетчик в этой части функции:

if (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX)
{        
  encodeData.bitIdx++;
  
  if (encodeData.bitIdx == (MANCH_BITS_IN_BYTE_NUM))
  {
    encodeData.bitIdx = 0;
    
    encodeData.byteIdx++;
    if (encodeData.byteIdx == encodeData.bytesNum)
    {
      encodeData.active = 0;
    }
  }
  
  encodeTimerCnt = 0;
}

Кроме того, поскольку прошло 100 мкс, что есть длительность передачи одного бита, то переходим к следующему биту, инкрементируя encodeData.bitIdx. Оставшееся просто - проверяем, не вышел ли индекс бита за пределы байта, и в случае выхода, переходим на следующий байт. А если счетчик байт равен кол-ву передаваемых байт (encodeData.bytesNum), то все, финиш, процесс завершается.

Осталась маленькая, но самая важная часть, которая осуществляет генерацию манчестерского кода:

if ((encodeTimerCnt == (MANCH_ENCODE_TIMER_MAX / 2)) ||
    (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX))
{
  uint8_t curCodeBit = GetDataBit(&encodeData);
  uint8_t curOutputBit = curCodeBit ^ virtTact;
  SetOutput(curOutputBit);
  virtTact ^= 0x01;
}

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

бит \medspace манчестерского \medspace кода = бит \medspace тактового \medspace генератора \medspace XOR  \medspace бит \medspace исходного \medspace кода.

И в завершение меняем значение virtTact с 0 на 1, либо с 1 на 0.

Дело за малым - запускаем процесс генерации Манчестер-II кода из main() и анализируем результат. Для этого запускаем таймер HAL_TIM_Base_Start_IT(&htim2) и в while(1) будем запускать генерацию каждые 500 мс:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim2);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    MANCH_Encode(txData, 8);
    HAL_Delay(500);
    
    txData[7]++;
  }
  /* USER CODE END 3 */
}

Также здесь фигурирует txData - обычный массив тестовых данных, объявленный здесь же, в main.c:

uint8_t txData[8] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x00};

Последний байт инкрементируем в цикле, чтобы при декодировании убедиться, что не теряем посылки. Компилируем, прошиваем, запускаем, встаем осциллографом на PA3:

Результат работы программы

Переводим мысленно несколько байт наблюдаемого сигнала из манчестерского кода и получаем полное соответствие исходным данным (плюс синхрополе), что явно сигнализирует об успехе операции кодирования.

Засим откланиваюсь, в следующей статье этот же самый сигнал мы декодируем, причем этим же самым контроллером.

/**
  ******************************************************************************
  * @file           : manchester_code.c
  * @brief          : Manchester code driver
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */



/* Includes ------------------------------------------------------------------*/

#include "manchester_code.h"



/* Declarations and definitions ----------------------------------------------*/

static uint8_t virtTact = 1;
static MANCH_Data encodeData;
static uint16_t encodeTimerCnt = 0;

extern TIM_HandleTypeDef htim2;



/* Functions -----------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
static void SetOutput(uint8_t state)
{
  HAL_GPIO_WritePin(MANCH_OUTPUT_PORT, MANCH_OUTPUT_PIN, (GPIO_PinState)state);
}



/*----------------------------------------------------------------------------*/
void MANCH_Encode(uint8_t* data, uint8_t size)
{
  encodeData.bitIdx = 0;
  encodeData.byteIdx = 0;
  
  if (size > MANCH_DATA_BYTES_NUM)
  {
    encodeData.bytesNum = MANCH_DATA_BYTES_NUM + MANCH_SYNC_BYTES_NUM;
  }
  else
  {
    encodeData.bytesNum = size + MANCH_SYNC_BYTES_NUM;
  }
  
  memcpy(&encodeData.data[MANCH_SYNC_BYTES_NUM], data, encodeData.bytesNum - MANCH_SYNC_BYTES_NUM);
  encodeData.data[0] = MANCH_SYNC_FIELD & 0xFF;
  encodeData.data[1] = (MANCH_SYNC_FIELD & 0xFF00) >> 8;
  
  encodeTimerCnt = 0;
  virtTact = 1;
  encodeData.active = 1;
}



/*----------------------------------------------------------------------------*/
static uint8_t GetDataBit(MANCH_Data* manchData)
{
  uint8_t res;
  
  uint8_t curByte = manchData->data[manchData->byteIdx];
  uint8_t curBitIdx = manchData->bitIdx;
  
  res = (curByte >> curBitIdx) & 0x01;
  
  return res;
}



/*----------------------------------------------------------------------------*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim == &htim2)
  {
    // Encoding process
    if (encodeData.active == 1)
    {
      if ((encodeTimerCnt == (MANCH_ENCODE_TIMER_MAX / 2)) ||
          (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX))
      {
        uint8_t curCodeBit = GetDataBit(&encodeData);
        uint8_t curOutputBit = curCodeBit ^ virtTact;
        SetOutput(curOutputBit);
        virtTact ^= 0x01;
      }
            
      if (encodeTimerCnt == MANCH_ENCODE_TIMER_MAX)
      {        
        encodeData.bitIdx++;
        
        if (encodeData.bitIdx == (MANCH_BITS_IN_BYTE_NUM))
        {
          encodeData.bitIdx = 0;
          
          encodeData.byteIdx++;
          if (encodeData.byteIdx == encodeData.bytesNum)
          {
            encodeData.active = 0;
          }
        }
        
        encodeTimerCnt = 0;
      }
    
      encodeTimerCnt++;
    }
  }
}



/*----------------------------------------------------------------------------*/
/**
  ******************************************************************************
  * @file           : manchester_code.h
  * @brief          : Manchester code driver interface
  * @author         : MicroTechnics (microtechnics.ru)
  ******************************************************************************
  */

#ifndef MANCH_H
#define MANCH_H



/* Includes ------------------------------------------------------------------*/

#include "stm32f1xx_hal.h"
#include <string.h>



/* Declarations and definitions ----------------------------------------------*/

#define MANCH_OUTPUT_PORT                                       GPIOA
#define MANCH_OUTPUT_PIN                                        GPIO_PIN_3

#define MANCH_ENCODE_TIMER_PERIOD_US                            10
#define MANCH_BIT_TIME_US                                       100

#define MANCH_BYTES_NUM                                         10
#define MANCH_SYNC_BYTES_NUM                                    2
#define MANCH_DATA_BYTES_NUM                                    MANCH_BYTES_NUM - MANCH_SYNC_BYTES_NUM

#define MANCH_BITS_IN_BYTE_NUM                                  8

#define MANCH_SYNC_FIELD                                        0xAA55

#define MANCH_ENCODE_TIMER_MAX                                  MANCH_BIT_TIME_US / MANCH_ENCODE_TIMER_PERIOD_US



typedef struct MANCH_Data
{
  uint8_t data[MANCH_BYTES_NUM];
  uint16_t bitStream;
  uint16_t byteIdx;
  uint16_t bytesNum;
  uint8_t bitIdx;
  uint8_t active;
} MANCH_Data;



/* Functions -----------------------------------------------------------------*/

extern void MANCH_Encode(uint8_t* data, uint8_t size);



#endif // #ifndef MANCH_H

Ссылка на проект: MT_ManchesterCode_Encode

Подписаться
Уведомить о
guest

10 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
nbveh
nbveh
3 лет назад
Здравствуйте, спасибо за разбор кода! Прошу разъяснить как меняется /*----------------------------------------------------------------------------*/
static uint8_t GetDataBit(MANCH_Data* manchData)
{
  uint8_t res;
  
  uint8_t curByte = manchData->data[manchData->byteIdx];
  uint8_t curBitIdx = manchData->bitIdx;
  
  res = (curByte >> curBitIdx) & 0x01;
  
  return res;
}



/*----------------------------------------------------------------------------*/

а именно manchData->data то есть я хотел бы понять как меняется encodeData

Yuriy
Yuriy
Ответ на комментарий  nbveh
4 месяцев назад

доброго времени суток ! я так понимаю на основе вашего кода можно сделать openterm на stm32g030?

Сергей
Сергей
2 лет назад

Спасибо!
Заметил одну особенность: если последний бит в пачке "1", то уровень держится в "1", хотя как мне кажется между пачками должен быть всегда "0".
Можно ли это исправить?

Сергей
Сергей
Ответ на комментарий  Aveal
2 лет назад

Пока просто добавил в конце нулевой типа "стоповый" байт...

Serg
Serg
Ответ на комментарий  Aveal
11 месяцев назад

Добрый!
Если по линии не передается постоянная сотавляющая сигнала, 0 сам получится ...

10
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x