Proteus и два с половиной hello world для UART и USB на микроконтроллере

в 13:49, , рубрики: diy или сделай сам, ethernet, pic, RS232, usb, Программинг микроконтроллеров, метки: , , , ,

Недавно разгребал папки на Dropbox и наткнулся на свои первые поделки. Когда делал первые шаги с микроконтроллерами, почти сразу меня стали посещать мысли и идеи об управлении моими поделками с ПК, либо каким-либо образом с этим самым ПК общаться. Это казалось увлекательным и «серьезным». Сейчас я стараюсь все сразу тестировать в железе, но в начале наступать на грабли и стрелять себе в ногу хотелось безболезненно и быстро. В этом мне неизменно помогал Proteus. Десятки раз пересобирая проекты было важно, чтобы можно было экспериментировать с комфортом, если так можно сказать. Это потом я уже купил фирменную отладку и что в Proteus, что в железе — все стало одинаково быстро.
В первую очередь мне хотелось попробовать UART и USB, а потом уже и Ethernet. Каждому желанию я придумывал свой «проект». Многие идеи так и остались в виде проекта для Proteus — идея надоедала сразу после реализации программной части.
Надеюсь, данный пост поможет всем, кто хотел попробовать сделать свое USB устройство или просто увидеть, что это все не так сложно; более того, мне хотелось, чтобы можно было попробовать сразу в симуляторе. Рассказать как новичок для новичка — чтобы захотелось открыть гугл и начать читать, чтобы сделать больше, узнать лучше.
Я не буду рассказывать о регистрах, режимах. Я сомневаюсь, что это поможет зажечь огонь в глазах. Возможно, кто-то захочет сделать что-то полезное для себя и у простых примеров больше шансов затянуть в это крайне увлекательное творчество (а для меня уже практически наркотик). А прежде чем бежать на ebay или начинать считать дни с момента отправки отладочной платы из Китая, можно попробовать свои силы в виртуальном микроконтроллере.
Я хотел бы попробовать сделать два своеобразных hello world проекта, которые, тем ни менее, не сильно сложнее стандартного LED blinking. Под катом много изображений.

Описание и примеры кода в тексте будут для микроконтроллера PIC18F4550, уж прошу простить меня, с atmel я не работал. А STM8/32 отсутствуют даже в 8-й версии.
Код для микроконтроллеров написан для компилятора mikroC от Mikroelektronika и писался для удобства понимания, надеюсь, у меня это вышло хотя бы частично.

Протеус и обвязка MCU

Протеус, как мне кажется, создан для макетирования и тестирования работы софта в первую очередь. Возможно именно поэтому он допускает множественные упрощения в создании схем. Ниже приведен абсолютно достаточный набор для тестирования общения с ПК по UART/USB. Хочу снова обратить внимание, что такая схема работать будет только в Proteus'е.
Все проекты приложены к статье, так что установив программу, можно сразу все попробовать.

Proteus и два с половиной hello world для UART и USB на микроконтроллере

U(S)ART

Тема избита, но все же позволю себе описать ее еще раз.
Чтобы подключить к нашему виртуальному микроконтроллеру putty или любую другю произвольную программу, нам нужно сделать несколько действий.
Модуль COMPIM использует виртуальный порт, чтобы к нему подключиться, нужно создать ещё один и соединить их как нуль модем.
Хорошим помощником тут будет бесплатная утилита com0com.
Там создаем два виртуальных порта, в моем случае это COM 3 и 4. Один подключаем к Proteus'овскому COMPIM, второй уже используем «снаружи»

Окно программы
Proteus и два с половиной hello world для UART и USB на микроконтроллере

А вот так это будет выглядеть в devmgmt.msc (Диспетчере устройств)
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Теперь все готово к тесту.

Создадим программу для теста

char uart_rd;
void main()
{
    UART1_Init(9600);
    UART1_Write_Text("Hello Habrahabr");
    UART1_Write(10);
    UART1_Write(13);
    for(;;)
    {
        if(UART1_Data_Ready())
        {
            uart_rd = UART1_Read( );
            switch(uart_rd)
            {
                case 0xD:
                UART1_Write(10);
                UART1_Write(13);
                break;
                default:
                UART1_Write(uart_rd);
                break;
            }
        }
    }
}

Настроим COMPIM устройство (правый клик — Edit properties).
Напомню, что у меня com0com эмулирует порты COM3/COM4
Proteus и два с половиной hello world для UART и USB на микроконтроллере

На всякий случай, приложу картинку с настройками микроконтроллера
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Включаем Putty, переводим его в режим Serial и подключаем к COM4. Теперь… нажми на кнопку — получишь результат
Печатаем текст в окне терминала и микроконтроллер делает echo нам назад. По нажатию enter переводим каретку + новая строка.
Backspace так же работает. Сразу вспомнилось как чатился с друзьями по Hyper Terminal, часами занимая телефонную линию…
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Теперь можно изменяя код поиграться с микроконтроллером.

Например, можно добавить примитивный командный обработчик

#define CRLF UART1_Write(10);UART1_Write(13)
char uart_rd = 0;
char cmd[32] = {0};
int char_counter = 0;
void cmd_exec(void)
{
    if (strstr(cmd, "hello"))
    {
        UART1_Write_Text("EHLO");
        CRLF;
    }
    else if (strstr(cmd, "test"))
    {
        UART1_Write_Text("TSET");
        CRLF;
    }
    else
    {
        UART1_Write_Text("Unknown command");
        CRLF;
    }
    char_counter=0;
    memset(cmd, 0, sizeof cmd);
}
void main(void)
{
    UART1_Init(9600);
    UART1_Write_Text("MCU started");
    UART1_Write(10);
    UART1_Write(13);
    for(;;)
    {
        if(UART1_Data_Ready())
        {
            uart_rd = UART1_Read( );
            switch(uart_rd)
            {
                case 0xD:
                CRLF;
                cmd_exec();
                break;
                default:
                UART1_Write(uart_rd);
                break;
            }
            cmd[char_counter++] = uart_rd;
            if(char_counter == (sizeof cmd - 1))
            {
                CRLF;
                cmd_exec();
            }
        }
    }
}

USB HID

Сделать свое USB устройство оказалось делом непростым для меня тогда. Тестировать мы будем HID устройства, то есть, в большинстве случаев — устройства ввода.
Очень хотелось мне сделать автоматический ввод пароля, а так же блокировку компьютера, когда я отошел, и разблокировку, когда я подошел. Ну и кучу чего еще, что можно было реализовать посредством виртуальной клавиатуры.
Кучу полезной информации по HID можно найти тут. Читать — не перечитать.
Вкратце: каждое USB HID устройство имеет специальное описание, дескриптор. Которое описывает что это за устройство, как им можно управлять, сколько оно потребляет от шины или же имеет самостоятельное питание и кучу другой информации. Поэтому нам нужно сделать правильное описание, чтобы ОС могла понять, что это клавиатура и могла с ней работать.

Но сначала, чтобы Proteus мог пробросить свой виртуальный USB хост в наш реальный ПК, необходимо поставить виртуальный драйвер, он идет в комплекте
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Чтобы устройство работало как полноценное FullSpeed USB 2.0, необходимо включить PLL и настроить его соответствующим образом.
В Proteus'е так же надо выставить частоту процессора как 96МГц.

Под спойлером объяснение, как работает задающий генератор (в случае с PIC)

Все подробно описано тут

image

Дескриптор

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

В нашем случае от «стандартного» декриптора изменений немного:
Мы изменили VID/PID, указали, что у нас буфер ввода/вывода на 8 байт каждый и, собственно, указали, что у нас устройство класса «клавиатура» и использовать его нужно именно так.
Все названия переменных говорят сами за себя.

Сам код дескриптора, много кода

const unsigned int USB_VENDOR_ID = 0xdead;
const unsigned int USB_PRODUCT_ID = 0xbeaf;
const char USB_SELF_POWER = 0x80;            // Self powered 0xC0,  0x80 bus powered
const char USB_MAX_POWER = 50;               // Bus power required in units of 2 mA
const char HID_INPUT_REPORT_BYTES = 8;
const char HID_OUTPUT_REPORT_BYTES = 8;
const char USB_TRANSFER_TYPE = 0x03;         //0x03 Interrupt
const char EP_IN_INTERVAL = 1;
const char EP_OUT_INTERVAL = 1;
const char USB_INTERRUPT = 1;
const char USB_HID_EP = 1;
const char USB_HID_RPT_SIZE = 63;
/* Device Descriptor */
const struct {
    char bLength;               // bLength         - Descriptor size in bytes (12h)
    char bDescriptorType;       // bDescriptorType - The constant DEVICE (01h)
    unsigned int bcdUSB;        // bcdUSB          - USB specification release number (BCD)
    char bDeviceClass;          // bDeviceClass    - Class Code
    char bDeviceSubClass;       // bDeviceSubClass - Subclass code
    char bDeviceProtocol;       // bDeviceProtocol - Protocol code
    char bMaxPacketSize0;       // bMaxPacketSize0 - Maximum packet size for endpoint 0
    unsigned int idVendor;      // idVendor        - Vendor ID
    unsigned int idProduct;     // idProduct       - Product ID
    unsigned int bcdDevice;     // bcdDevice       - Device release number (BCD)
    char iManufacturer;         // iManufacturer   - Index of string descriptor for the manufacturer
    char iProduct;              // iProduct        - Index of string descriptor for the product.
    char iSerialNumber;         // iSerialNumber   - Index of string descriptor for the serial number.
    char bNumConfigurations;    // bNumConfigurations - Number of possible configurations
}
device_dsc = {
    0x12,                   // bLength
    0x01,                   // bDescriptorType
    0x0200,                 // bcdUSB
    0x00,                   // bDeviceClass
    0x00,                   // bDeviceSubClass
    0x00,                   // bDeviceProtocol
    8,                      // bMaxPacketSize0
    USB_VENDOR_ID,          // idVendor
    USB_PRODUCT_ID,         // idProduct
    0x0001,                 // bcdDevice
    0x01,                   // iManufacturer
    0x02,                   // iProduct
    0x00,                   // iSerialNumber
    0x01                    // bNumConfigurations
}
;
/* Configuration 1 Descriptor */
const char configDescriptor1[]= {
    // Configuration Descriptor
    0x09,                   // bLength             - Descriptor size in bytes
    0x02,                   // bDescriptorType     - The constant CONFIGURATION (02h)
    0x29,0x00,              // wTotalLength        - The number of bytes in the configuration descriptor and all of its subordinate descriptors
    1,                      // bNumInterfaces      - Number of interfaces in the configuration
    1,                      // bConfigurationValue - Identifier for Set Configuration and Get Configuration requests
    0,                      // iConfiguration      - Index of string descriptor for the configuration
    USB_SELF_POWER,         // bmAttributes        - Self/bus power and remote wakeup settings
    USB_MAX_POWER,          // bMaxPower           - Bus power required in units of 2 mA
    // Interface Descriptor
    0x09,                   // bLength - Descriptor size in bytes (09h)
    0x04,                   // bDescriptorType - The constant Interface (04h)
    0,                      // bInterfaceNumber - Number identifying this interface
    0,                      // bAlternateSetting - A number that identifies a descriptor with alternate settings for this bInterfaceNumber.
    2,                      // bNumEndpoint - Number of endpoints supported not counting endpoint zero
    0x03,                   // bInterfaceClass - Class code
    0,                      // bInterfaceSubclass - Subclass code
    0,                      // bInterfaceProtocol - Protocol code
    0,                      // iInterface - Interface string index
    // HID Class-Specific Descriptor
    0x09,                   // bLength - Descriptor size in bytes.
    0x21,                   // bDescriptorType - This descriptor's type: 21h to indicate the HID class.
    0x01,0x01,              // bcdHID - HID specification release number (BCD).
    0x00,                   // bCountryCode - Numeric expression identifying the country for localized hardware (BCD) or 00h.
    1,                      // bNumDescriptors - Number of subordinate report and physical descriptors.
    0x22,                   // bDescriptorType - The type of a class-specific descriptor that follows
    USB_HID_RPT_SIZE,0x00,  // wDescriptorLength - Total length of the descriptor identified above.
    // Endpoint Descriptor
    0x07,                   // bLength - Descriptor size in bytes (07h)
    0x05,                   // bDescriptorType - The constant Endpoint (05h)
    USB_HID_EP | 0x80,      // bEndpointAddress - Endpoint number and direction
    USB_TRANSFER_TYPE,      // bmAttributes - Transfer type and supplementary information
    0x40,0x00,              // wMaxPacketSize - Maximum packet size supported
    EP_IN_INTERVAL,         // bInterval - Service interval or NAK rate
    // Endpoint Descriptor
    0x07,                   // bLength - Descriptor size in bytes (07h)
    0x05,                   // bDescriptorType - The constant Endpoint (05h)
    USB_HID_EP,             // bEndpointAddress - Endpoint number and direction
    USB_TRANSFER_TYPE,      // bmAttributes - Transfer type and supplementary information
    0x40,0x00,              // wMaxPacketSize - Maximum packet size supported
    EP_OUT_INTERVAL         // bInterval - Service interval or NAK rate
}
;
const struct {
    char report[];
}
hid_rpt_desc =
{
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x06,                    // USAGE (Keyboard)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
    0x19, 0xe0,                    //   USAGE_MINIMUM 224(Keyboard LeftControl)
    0x29, 0xe7,                    //   USAGE_MAXIMUM 231(Keyboard Right GUI)    (left and right: alt, shift, ctrl and win)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x08,                    //   REPORT_COUNT (8)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
    0x95, 0x05,                    //   REPORT_COUNT (5)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x05, 0x08,                    //   USAGE_PAGE (LEDs)
    0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
    0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x03,                    //   REPORT_SIZE (3)
    0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)
    0x95, 0x06,                    //   REPORT_COUNT (6)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
    0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
    0xc0                           // END_COLLECTION
}
;
//Language code string descriptor
const struct {
    char bLength;
    char bDscType;
    unsigned int string[1];
}
strd1 = {
    4,
    0x03,
    {
        0x0409
    }
}
;
//Manufacturer string descriptor
const struct{
    char bLength;
    char bDscType;
    unsigned int string[10];
}
strd2={
    22,           //sizeof this descriptor string
    0x03,
    {
        'H','a','b','r','a','h','a','b','r'
    }
}
;
//Product string descriptor
const struct{
    char bLength;
    char bDscType;
    unsigned int string[15];
}
strd3={
    32,          //sizeof this descriptor string
    0x03,
    {
        'H','a','b','r','a','K','e','y','b','o','a','r','d'
    }
}
;
//Array of configuration descriptors
const char* USB_config_dsc_ptr[1];
//Array of string descriptors
const char* USB_string_dsc_ptr[3];
void USB_Init_Desc(){
    USB_config_dsc_ptr[0] = &configDescriptor1;
    USB_string_dsc_ptr[0] = (const char*)&strd1;
    USB_string_dsc_ptr[1] = (const char*)&strd2;
    USB_string_dsc_ptr[2] = (const char*)&strd3;
}

Этот дескриптор добавляется в проект, и будет автоматически использован при сборке.

Простейшая программа, которая будет набирать текст

unsigned short USBResponse[8] = { 0} absolute 0x500;
unsigned short USBCommand[8] = {0} absolute 0x508;

char *text="habrahabr";
int i = 0;
void interrupt(void)
{
    USB_Interrupt_Proc( );
}
void clrUSB(void)
{    
    memset(USBCommand, 0, sizeof USBCommand);
   while ( !HID_Write(&USBCommand, sizeof USBCommand));
}
void
main(void)
{
    ADCON1 |= 0x0F;
    CMCON |= 7;
    HID_Enable(&USBResponse, &USBCommand);
    delay_ms(1000);
    for (i=0; i < strlen(text); i++)
    {
        USBCommand[2] = text[i] - 93;
        while ( !HID_Write(&USBCommand, sizeof USBCommand));
        clrUSB();
        delay_ms(200);
    }
}

По сути вся работа заключается в заполнении соответствующего буфера и в отравке его в ПК. Не сложнее, чем с UART. Вся работа выполняется в подпрограмме прерывания. В комплекте с IDE уже идут готовые библиотеки работы с HID.
Тут следует пояснить, что scan коды клавиатуры отличаются от ASCII, но чтобы не перегружать код (у нас же hello world), я обошел сие неудобство примитивным образом. Работать будет только для букв в нижнем регистре. Желающие могут сами сделать преобразование. Подобный девайс я использую для KVM На работе, беспроводной удлинитель клавиатуры — наши D-Linkовские KVM не хотят понимать USB донглы беспроводные.

Теперь открываем Notepad, запускаем Proteus (предварительно кликаем по виртуальному штекеру — USB разъем «вставится»), сразу переводим фокус мышкой на Notepad и наблюдаем, как наше творение печатает слово habrahabr.
Proteus и два с половиной hello world для UART и USB на микроконтроллере
А в диспетчере появилось наше устройство
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Теперь можно на основе этого добавлять что-то свое.

Пару слов про то, как это работает.
Для ввода-вывода с клавиатуры зарезервировано 8 байт:

0 Модификатор
1 Не используется
2 Клавиша 1
3 Клавиша 2
4 Клавиша 3
5 Клавиша 4
6 Клавиша 5
7 Клавиша 6

Модификаторы, это спец клавиши Ctrl, Shift, Alt. Их можно объединять. Например, для комбинации Ctrl Alt Del:

Modifier: 0b00000101 Ctrl, Shift, Alt
Key code: 0x4c (клавиша Delete)

Следует помнить, что сразу после передачи данных, необходимо стирать буфер USB, иначе получится эффект залипшей клавиши. То есть отправлять на ПК восемь нуль байт. В примере это делает подпрограмма clearUSB
Подробнее про сканкоды описано в appnote от microchip
Точно так же можно создать обычное HID устройство и передавать/принимать байты с ПК и по логике работы это почти ничем не отличается от того же UART. Но это уже требует отдельной работы, например, с libusb со стороны ПК.

Ethernet

Не отражено в заголовке, но про это тоже стоит сказать.
К сожалению, данный пример не будет завершенным, поскольку это тема отдельного разговора.
Но, по крайней мере, я опишу как настроить сам симулятор, и кое-что попробовать все же получится.
Тут как раз пример того, что работает в железе, но не всегда работает в симуляторе. В Proteus реализованы VSM модели для микросхем ENC28J60 и RTL8019. Да-да, тот самый чип, который всем нам был знаком по бюджетным сетевым картам. Использование ENC описано достаточно широко и проблем тут быть не должно. Например, уважаемый DIHALT все давно и преподробнейше описал. Поэтому, чтобы не было скучно, возьмем 8019, тем более я пишу под неё софт для использования совместно с Z80.

Как и в случае с USB, нам надо установить драйверы, но теперь WinPCAP. Они лежат в папке Virtual Network, рядом с драйверами USB. Или скачать с сайта самую свежую версию
После этого у нас появится новая виртуальный сетевой интерфейс с адресом 192.168.95.1, который, разумеется, можно поменять.

Cделаем на UART отладочный интерфейс по уже известной нам схеме.
HINT: Если вас раздражают надписи TEXT — в Description компонента поставьте пробел
Proteus и два с половиной hello world для UART и USB на микроконтроллере

В свойствах микросхемы пропишем номер или IP нашей виртуальной сетевухи и можно поменять кое-какие настройки.
В моем случае это 192.168.50.1 (я изменил)

Proteus и два с половиной hello world для UART и USB на микроконтроллере

Далее дело за софтом. Полноценный драйвер для 8019 у меня пока не готов, да и это тема отдельной большой статьи, хотя вряд ли такой анахронизм кому-то интересен. Но даже без настройки протоколов (IP адрес у нас сейчас 0.0.0.0), так как я разрешил ICMP/Broadcast в регистрах, мы можем попинговать какой-нибудь левый адрес в нашей подсети, и сетевая карта микросхема радостно помигает нам светодиодом при получении пакета. После каждой попытки, используйте новый адрес, а то ARP таблица же кэшируется.
Proteus и два с половиной hello world для UART и USB на микроконтроллере

Вместе с проектами приложены готовые HEX, так что компилятор можно даже не качать, если нужно просто проверить, что все работает.
Чтобы симуляция работала — не забывайте указать микроконтроллеру где лежит ваш HEX файл.

Скачать все файлы можно по этой ссылке

Надеюсь, у кого-то загорится желание попробовать и реализовать в железе.

Автор: Pugnator

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js