Привет!
Попробую вкратце описать прошивку, которую нужно было сделать в краткие сроки и без использования RTOS (что в итоге дало свои багованные особенности, но об это по порядку ;) ).
Была задача сделать Modbus Master, чтобы можно было с программки на ПК опрашивать Slave устройства на шине RS-485 по протоколу Modbus, да еще и на российском АРМ-е К1986ВЕ92QI, благо, уже имел дело с этим МК, почему бы и нет, дело-то простое должно быть!
Первоначально сделал на основе примера USB — COM порт, подукрасив его и сделав тестовое ПО на ПК в виде мастера Modbus. МК в этом случае просто выполнял роль преобразователя интерфейсов и в ОС был виден в качестве обычного, настраиваемого COM-порта. Единственное, с чем повозился — это линия переключения для приёма\передачи RTS, её пришлось делать софтварно. В общем, всё даже работало, но были минусы:
- Нужен подписанный драйвер для устройства, а его Миландр не предоставляет! Есть только пример исходников, что означает, что на х64 системах имеем проблемы с установкой (можно, конечно, поменять PID\VID, как в официальном драйвере от STM, НО устройство-то будет определяться не как Миландровское, что было тоже важно).
- Нужно постоянно опрашивать из софтины на ПК все подряд Slave-устройства, а задача требовала для некоторых Slave как можно более частый опрос. Проще это делать на более низком уровне, а при необходимости забирания актуальных данных их запрашивать (вот тут уже начались отголоски того, что сказали делать без использования RTOS…).
- Исходя из первого пункта на некоторых ПК были проблемы с определением устройства или его установкой (хотя даже с USB HID не всё разрешилось, видимо, есть косяки в драйвере USB периферии).
В общем, т.к. уже имел дело на STM32 с USB, то было решено сделать USB HID, совмещенный с Modbus Master, последний, в свою очередь, просто по циклу должен опрашивать Slave устройства и складывать данные в локальный буфер. А по запросу с ПК выдаем необходимые данные в Feature Report и при необходимости записи в Slave устройства посредством Output Report записываем данные в них.
Вкратце о железе, подключение RS-485 максимально простое:
Контроллер практически голый, только обязательно кварц 8 мГц подключить необходимо (не показан), иначе не то, что USB, даже UART на нужной скорости не заработает! Разброс внутреннего генератора 8 мГц, как я помню, от 6 до 10 мГц, и всё это от погоды зависит сильно!
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.
Все ссылки для скачивания:
- Пак для Миландровских МК — Milandr.MDR1986BExx.1.4.0U.pack (более новые, но без правок можно найти на сайте Миландра)
- Пример прошивки USB HID + Modbus Master (non RTOS) — Скачать тут.
Если будут предложения по оптимизации или хотите сообщить о найденной ошибке — пишите, буду рад!
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.