USB HID Modbus Master на К1986ВЕ92

Привет!
Попробую вкратце описать прошивку, которую нужно было сделать в краткие сроки и без использования RTOS (что в итоге дало свои багованные особенности, но об это по порядку ;) ).
Была задача сделать Modbus Master, чтобы можно было с программки на ПК опрашивать Slave устройства на шине RS-485 по протоколу Modbus, да еще и на российском АРМ-е К1986ВЕ92QI, благо, уже имел дело с этим МК, почему бы и нет, дело-то простое должно быть!


Первоначально сделал на основе примера USB — COM порт, подукрасив его и сделав тестовое ПО на ПК в виде мастера Modbus. МК в этом случае просто выполнял роль преобразователя интерфейсов и в ОС был виден в качестве обычного, настраиваемого COM-порта. Единственное, с чем повозился — это линия переключения для приёма\передачи RTS, её пришлось делать софтварно. В общем, всё даже работало, но были минусы:

  1. Нужен подписанный драйвер для устройства, а его Миландр не предоставляет! Есть только пример исходников, что означает, что на х64 системах имеем проблемы с установкой (можно, конечно, поменять PID\VID, как в официальном драйвере от STM, НО устройство-то будет определяться не как Миландровское, что было тоже важно).
  2. Нужно постоянно опрашивать из софтины на ПК все подряд Slave-устройства, а задача требовала для некоторых Slave как можно более частый опрос. Проще это делать на более низком уровне, а при необходимости забирания актуальных данных их запрашивать (вот тут уже начались отголоски того, что сказали делать без использования RTOS…).
  3. Исходя из первого пункта на некоторых ПК были проблемы с определением устройства или его установкой (хотя даже с USB HID не всё разрешилось, видимо, есть косяки в драйвере USB периферии).

В общем, т.к. уже имел дело на STM32 с USB, то было решено сделать USB HID, совмещенный с Modbus Master, последний, в свою очередь, просто по циклу должен опрашивать Slave устройства и складывать данные в локальный буфер. А по запросу с ПК выдаем необходимые данные в Feature Report и при необходимости записи в Slave устройства посредством Output Report записываем данные в них.

Вкратце о железе, подключение RS-485 максимально простое:

b2-1-485

Контроллер практически голый, только обязательно кварц 8 мГц подключить необходимо (не показан), иначе не то, что USB, даже UART на нужной скорости не заработает! Разброс внутреннего генератора 8 мГц, как я помню, от 6 до 10 мГц, и всё это от погоды зависит сильно!

b2-2-mcu

USB и стабилизатор +3.3В даже показывать смысла нет — USB подключается к линиям USB_P \ USB_N через 22 Ом, ну а стабилизатор питает МК от +5В линии USB (про блокировочные конденсаторы 0,1 мкФ по питанию МК думаю Вы знаете ;) ).

Прошивки для МК пишу в Keil uVision5 (на момент разработки прошивки пользовался версией 5.15 и потом 5.17), попробовал сейчас поставить свой пак для Миландровских АРМ-ов на 5.20 — как итог, он распаковывается, но выдает ошибку компиляции SVD-файлов. Вроде бы это не страшно, проект компилируется. Если же есть проблемы — то попробуйте версии помладше.

Собственно, пак с правками для более удобной работы с USB-периферией — Milandr.MDR1986BExx.1.4.0U.pack

Довольно долго вникал в примеры, что есть на форуме Миландра, насколько я помню, в итоге остановился на выложенном от R_Max проекте. Но и там не всё работало, после переписки с поддержкой Миландра в итоге всё получилось. На форуме также выложил результат ТУТ. Более ранние сообщения и вообще какие-либо примеры, насколько я помню, в основном были в Этой теме форума (если вдруг понадобится). К слову. Этот же пример успешно работает и на К1986ВЕ1QI — проверено на собственной отладочной плате! (да, может нужно написать заметку с открытым проектом в Альтиуме об этом МК? :) Если интересно — пишите в комментариях! Напишем статью тогда).

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

Всё, конечно же, начинается с названия! :) В файле  usbdesc.c  меняем PID, VID, имя производителя и имя устройства на свои:

/* USB Standard Device Descriptor */
const U8 USB_DeviceDescriptor[] = {
 // ............
 WBVAL(0xC251), /* idVendor */
 WBVAL(0x1C01), /* idProduct */
 // ............
};

// ....................................................................... // 

/* USB String Descriptor (optional) */
const U8 USB_StringDescriptor[] = {
// .............
/* Index 0x01: Manufacturer */
 (8*2 + 2), /* bLength (8 Char + Type + lenght) */
 USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType */
 'A',0,
 '_',0,
 'D',0,
 ' ',0,
 'E',0,
 'l',0,
 'e',0,
 'c',0,
 't',0,
 'r',0,
 'o',0,
 'n',0,
 'i',0,
 'c',0,
 's',0,
/* Index 0x02: Product */
 (16*2 + 2), /* bLength (16 Char + Type + lenght) */
 USB_STRING_DESCRIPTOR_TYPE, /* bDescriptorType */
 'U',0,
 'S',0,
 'B',0,
 '-',0,
 'H',0,
 'I',0,
 'D',0,
 ' ',0,
 'M',0,
 'o',0,
 'd',0,
 'b',0,
 'u',0,
 's',0,
 '-',0,
 'M',0,
// ...............
};

Далее более неизведанная часть для меня — Modbus Master для МК. Если на ПК он давно уже реализован в виде библиотеки и работает прекрасно, то на МК примеров как то совсем нет… ну нашел 2 на тот момент времени… вот что я нашел:

  •  FreeModbus_Slave-Master-RTT-STM32 — проект от китайского товарища, сделан добротно, всё бы хорошо, но он под китайскую же RTOS — RTT (насколько я разобрался). Мне этот вариант не очень подходил… я списался с разработчиком, мол, как мне можно переделать его не под RTOS использование, на удивление разработчик ответил быстро и посоветовал второй вариант, подглядеть что и как переделать;
  • Freemodbus-master-F401 — проект уже не такой добротный и заточен под HAL, но в итоге благодаря ему получилось подкрутить первый с минимальными изменениями (насколько я уже помню, проблема была в основном с событиями — файл portevent_m.c ).

В итоге запись и чтение в Slave-устройства заработали, но вылезла проблема с определением ошибки записи или чтения в Slave-устройство, которого нет на линии — мастеру на это всё равно, он просто ожидает таймаут и ничего не говорит — мол, всё ОК прошло :) Для серьезного применения и стабильной работы таки НЕОБХОДИМО всё делать с применением RTOS. Для теста же можно и так использовать.

Вкратце опишу настройки и интересные места в этой части, которые нужно подстраивать под свой проект:

Из общих настроек — что бы была видна активность интерфейсов, определяем, где есть светодиоды в main.h :

#define LED_PORT MDR_PORTA
#define LED_RS_PIN PORT_Pin_0
#define LED_USB_PIN PORT_Pin_1

Также там же выбираем скорость порта RS-485 :

#define UART_BAUD_RATE  128000

Но с этим есть проблема! В Миландровских МК нет прерывания по опустошению буфера, вследствие чего пришлось реализовывать костыль, чтобы вызывать функцию pxMBMasterFrameCBTransmitterEmpty(); Modbus по опустошению буфера. Костыль заключается в следующем (определён в файле portserial_m.c ): вызываем по срабатыванию 1\8 заполненности буфера передачи с таймаутом, тупо, но работает (буду благодарен, если подскажете решение лучше и правильнее):

// 1\8 full of transmitter
volatile uint8_t DMA_TIM = 0x80;
void DMA_IRQHandler(void)
{
if(DMA_TIM) DMA_TIM--;
else
{
DMA_TIM=0x80;
pxMBMasterFrameCBTransmitterEmpty();
}
}

Также стоит обратить внимание на функцию инициализации xMBMasterPortSerialInit в файле portserial_m.c — там есть 2 предупреждения о нереализованных вещах:

BOOL xMBMasterPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
 static UART_InitTypeDef UART_InitStructure;
 static PORT_InitTypeDef PortInitStructure;

 // UART1_RTS Software
 PortInitStructure.PORT_PULL_UP = PORT_PULL_UP_OFF;
 PortInitStructure.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF;
 PortInitStructure.PORT_FUNC = PORT_FUNC_PORT;
 PortInitStructure.PORT_MODE = PORT_MODE_DIGITAL;
 PortInitStructure.PORT_SPEED = PORT_SPEED_MAXFAST;
 PortInitStructure.PORT_MODE = PORT_MODE_DIGITAL;

 PortInitStructure.PORT_OE = PORT_OE_OUT;
 PortInitStructure.PORT_Pin = RTS_PIN;
 PortInitStructure.PORT_FUNC = PORT_FUNC_PORT;
 PORT_Init(MDR_PORTB, &PortInitStructure);

 // UART1_TX
 PortInitStructure.PORT_FUNC = PORT_FUNC_ALTER;
 PortInitStructure.PORT_PD_SHM = PORT_PD_SHM_OFF;
 PortInitStructure.PORT_PD = PORT_PD_DRIVER;
 PortInitStructure.PORT_GFEN = PORT_GFEN_OFF;
 PortInitStructure.PORT_Pin = PORT_Pin_5;
 PORT_Init(MDR_PORTB, &PortInitStructure);

 // UART1_RX
 PortInitStructure.PORT_OE = PORT_OE_IN;
 PortInitStructure.PORT_Pin = PORT_Pin_6;
 PORT_Init(MDR_PORTB, &PortInitStructure); 

 // UART Init
 RST_CLK_PCLKcmd(RST_CLK_PCLK_UART1, ENABLE);
 UART_BRGInit(MDR_UART1, UART_HCLKdiv1);
 NVIC_EnableIRQ(UART1_IRQn);

 // Initialize UART_InitStructure
 UART_InitStructure.UART_BaudRate = ulBaudRate;

 if(ucDataBits == 8)
 {
 UART_InitStructure.UART_WordLength = UART_WordLength8b;
 }
 else
 {
 #warning Donot Support ucDataBits < 8 !
 UART_InitStructure.UART_WordLength = UART_WordLength8b;
 }

 UART_InitStructure.UART_StopBits = UART_StopBits1;

 if(eParity == MB_PAR_NONE)
 {
 UART_InitStructure.UART_Parity = UART_Parity_No;
 }
 else if(eParity == MB_PAR_ODD)
 {
 UART_InitStructure.UART_Parity = UART_Parity_Odd;
 }
 else if(eParity == MB_PAR_EVEN)
 {
 UART_InitStructure.UART_Parity = UART_Parity_Even;
 }

 UART_InitStructure.UART_FIFOMode = UART_FIFO_OFF;
 UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_RXE | UART_HardwareFlowControl_TXE;

 #warning Don't Support ucPORT parametr, always only MDR_UART1
 // Configure UART1 parameters
 UART_Init (MDR_UART1,&UART_InitStructure);

 // Enables UART1 peripheral
 UART_Cmd(MDR_UART1,ENABLE);
 UART_DMACmd(MDR_UART1,UART_DMA_TXE,ENABLE);

 PORT_ResetBits(MDR_PORTB, RTS_PIN); // RTS

 return TRUE;
}

Для добавления поддержки выбора порта также необходимо или установить стандартные выходы для UART захардкоденными или дополнительно еще сделать выбор — какой пак выводов использовать для выбранного UART (стандартный или альтернативный). Если с UARTx_RTS всё просто и можно использовать абсолютно любой пин (не требуется поддержка спец.периферии), то с UARTx_RX \ UARTx_TX придётся заморочиться. У меня проект был под конкретную плату, поэтому поддержки этого я не реализовывал.

Функции, которые обслуживают буфера регистров мастера, лежат в файле user_mb_app_m.c.

Настройки регистров (их размер и начальный адрес) располагаются в файле user_mb_app.h .

И напоследок, в качестве примера взаимодействия USB HID части и Modbus Master’a в файле  main.c оставлен кусок кода, показывающий простой пример. Вначале определяем для конкретных типов Report’ов, приходящих от ПО на ПК, что мы будем делать.

Input Report —  это односторонние пакеты от устройства в ПК. Не используется. Можно сделать автоматическое их посылание без запроса от ПК (как это сделано в примере Keil\ARM\Utilities\HID_Client) через равные промежутки времени, но в этом примере не используется.

void GetInReport(void)
{
 // InReport[0]++;
}

Output Report — это односторонние пакеты от ПК в устройство. Используется для команд и настроек.

void SetOutReport(void)
{
 if(OutReport[0] == 0x81) // в первом байте проверяем условный номер команды
 {
// записываем новые данные в буфер мастера
 usModbusUserData[0] = (uint16_t)(OutReport[2] + (OutReport[1] << 8));
 usModbusUserData[1] = (uint16_t)(OutReport[4] + (OutReport[3] << 8));
 usModbusUserData[2] = OutReport[5];
 usModbusUserData[3] = OutReport[6];
 // выставляем флаг, означающий, что необходима запись в Slave устройство
 xtNeedWrite = TRUE;
 }
 // моргнем светодиодом :)
 Led_Blink(LED_USB);
}

Feature Report — это двусторонние пакеты от ПК в устройство и обратно. Используется для запроса конкретных данных или команд с подтверждением. Обработка этого типа Report’а состоит из двух функций — приёмной и передающей, вызывающимися по очереди.

// Приёмная часть - эта функция вызывается первой и в ней мы можем задать\узнать номер команды
// а так же произвести запись каких либо параметров или настроек
void SetFeatureReport(void)
{
// тут мы получаем условный ID запроса
 RequestDataID = (uint8_t)FeatureReport[0];

 if(RequestDataID == 0x81) // это была условная команда записи настроек?
 {
 if(usbNextWrite == TRUE) // для теста реализована запись через раз
 {
 usModbusUserData[0] = (uint16_t)(FeatureReport[2] + (FeatureReport[1] << 8));
 usModbusUserData[1] = (uint16_t)(FeatureReport[4] + (FeatureReport[3] << 8));
 usModbusUserData[2] = FeatureReport[5];
 usModbusUserData[3] = FeatureReport[6];
 // выставляем флаг, означающий, что необходима запись в Slave устройство
 xtNeedWrite = TRUE;
// выставляем флаг, означающий, что следующая команда не будет воспринята
 usbNextWrite = FALSE;
 }
 else
 {
 usbNextWrite = TRUE;
 }
 }

 Led_Blink(LED_USB);
}
// Передающая часть - эта функция вызывается перед отправкой на ПК и в ней мы наполняем Report
// так же здесь можно видеть по условной команде из первой части, что же у нас запросили...
void GetFeatureReport(void)
{
if(RequestDataID == 1)
{
FeatureReport[0] = RequestDataID;
FeatureReport[1] = (uint8_t)(usMRegInBuf[RequestDataID-1][1] & 0xFF);
FeatureReport[2] = (uint8_t)(usMRegInBuf[RequestDataID-1][2] & 0xFF);
FeatureReport[3] = (uint8_t)(usMRegInBuf[RequestDataID-1][3] & 0xFF);
FeatureReport[4] = (uint8_t)(usMRegInBuf[RequestDataID-1][4] & 0xFF);
FeatureReport[5] = (uint8_t)(usMRegInBuf[RequestDataID-1][5] & 0xFF);
FeatureReport[6] = (uint8_t)(usMRegInBuf[RequestDataID-1][6] & 0xFF);
FeatureReport[7] = (uint8_t)(usMRegInBuf[RequestDataID-1][7] & 0xFF);
}
else if((RequestDataID > 1) && (RequestDataID < 6)) { FeatureReport[0] = RequestDataID; FeatureReport[1] = (uint8_t)(usMRegInBuf[RequestDataID-1][1] & 0xFF); FeatureReport[2] = (uint8_t)(usMRegInBuf[RequestDataID-1][2] & 0xFF); FeatureReport[3] = 0; FeatureReport[4] = 0; FeatureReport[5] = 0; FeatureReport[6] = 0; FeatureReport[7] = 0; } else if((RequestDataID > 5) && (RequestDataID < 16)) { FeatureReport[0] = RequestDataID; FeatureReport[1] = (uint8_t)(usMRegInBuf[RequestDataID-1][1] >> 8);
FeatureReport[2] = (uint8_t)(usMRegInBuf[RequestDataID-1][1] & 0xFF);
FeatureReport[3] = 0;
FeatureReport[4] = 0;
FeatureReport[5] = 0;
FeatureReport[6] = 0;
FeatureReport[7] = 0;
}
}

Вот и всё :) В скором времени приведу пример библиотеки для работы с USB HID устройствами на C# и WPF.

Все ссылки для скачивания:

Если будут предложения по оптимизации или хотите сообщить о найденной ошибке — пишите, буду рад!

UPD1 (2017.01.09):

Проблема с определением устройства после загрузки ОС (если устройство было подключено к ПК заранее) или после перезагрузки ОС решается следующим образом — необходимо изменить функцию USB_ResetIRQ(void) в файле usbhw_MDR32F9x.c следующим образом:

void USB_ResetIRQ(void)
{
// Double Buffering is not yet supported
MDR_USB->SIS=0x1F; //Clear Interrupt Status
MDR_USB->SIM=RESET_IE | TDONE_IE | (USB_SOF_EVENT ? SOF_IE : 0); //Enable interrupts
//Setup Control Endpoint 0
MDR_USB->USB_SEP_FIFO[0].RXFC=1; //Clear RX FIFO EndPoint 0
MDR_USB->USB_SEP_FIFO[0].TXFDC=1; //Clear TX FIFO EndPoint 0
MDR_USB->USB_SEP[0].CTRL=EP_EN | EP_RDY; //Enable Endpoint 0, go to endpoint redy state

USB_DeviceContext.Address = 0;
USB_SetSA(USB_DeviceContext.Address);
}

Добавились последние две строки в функции — это разрешает работу оконечной точки 0.

Реклама

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s