Bitbang - это программный режим управления выводами микроконтроллера. Программа, записывая в порты микроконтроллера управляющие слова, может установить на разных выводах микросхемы логические единицы или нули, или считать текущее значение на них. Скорость передачи при таком способе обычно не очень высокая, но зато оказывается достаточно просто реализовать любой протокол.
Микросхема FTDI2232HL, конечно, не микроконтроллер, но возможности у нее широкие. В том числе можно программно управлять некоторыми / многими выводами микросхемы.
На основе микросхемы FTDI2232HL построен наш программатор MBFTDI. Вот с ним и проведем эксперименты.
Микросхема имеет два канала A и B. Оба канала микросхемы имеют по 16 бит программно управляемых данных. Канал A на плате программатора используется для JTAG, не все из 16-ти линий идут на разъем JTAG, только некоторые, а канал B выведен на гребеночку - 8 младших бит доступны для опроса или программного управления. На рисунке ниже отмечены сигналы, которые выходят на разъемы программатора MBFTDI:
Будем программировать канал A, тот что выходит на разъем JTAG. Просто так удобнее делать эксперименты.
Подключим / вставим в разъем JTAG программатора два светодиода к сигналам ADBUS0 и ADBUS1 (там есть последовательные резисторы, так что светодиоды не сгорят). Еще в разъем подключим кнопочку к сигналу ADBUS3 и к земле. Входы внутри микросхемы подтянуты к питанию резисторами. Так что "болтающиеся в воздухе" входы будут читаться как логическая единица.При замыкании кнопки должен читаться ноль.
Подключенные светодиоды и кнопка показаны на фото вверху статьи.
Напишем программу на C/C++ (Microsoft Visual Studio) для управления светодиодами из компьютера.
Так же, будем опрашивать состояние кнопки: нажата она или нет.
Для программирования микросхемы нужно читать документацию FTDI.
Нам понадобится всего несколько управляющих команд, чтобы выполнить наш проект. Файл описание команд микросхемы называется Application Note AN_108 Command Processor for MPSSE and MCU Host Bus Emulation Modes. Его можно найти на сайте FTDI.
Кроме описания команд микросхемы FTDI еще потребуется знание некоторых функций библиотеки FTDI ftd2xx.lib, в принципе есть заголовочный файл ftd2xx.h в котором все функции объявлены.
В принципе, здесь все довольно просто.
- Вызываем функцию FT_CreateDeviceInfoList, чтобы узнать сколько FTDI устройств подключено к системе.
- Вызываем FT_Open, чтобы открыть нужный нам канал передачи.
- Вызываем всякие функции типа FT_ResetDevice, FT_SetUSBParameters, FT_SetChars, FT_SetTimeouts, FT_SetBitMode для настройки некоторых параметров микросхемы.
- Дальше основными рабочими функциями будут FT_Write для записи блоков команд или данных и FT_Read для чтения отклика из микросхемы.
Весь проект MS Visual Studio можно выкачать на нашем сайте в разделе загрузки:
А вот собственно основной файл проекта:
// leds.cpp: определяет точку входа для консольного приложения. // #include "stdafx.h" #include <string.h> #include <windows.h> #include "ftd2xx.h" FT_HANDLE ftHandle; // Handle of the FTDI device FT_STATUS ftStatus; // Result of each D2XX call DWORD dwNumDevs; // The number of devices DWORD dwNumBytesToRead = 0; // Number of bytes available to read in the driver's input buffer DWORD dwNumBytesRead; unsigned char byInputBuffer[1024]; // Buffer to hold data read from the FT2232H DWORD dwNumBytesSent; DWORD dwNumBytesToSend; unsigned char byOutputBuffer[1024]; // Buffer to hold MPSSE commands and data to be sent to the FT2232H int ft232H = 0; // High speed device (FTx232H) found. Default - full speed, i.e. FT2232D DWORD dwClockDivisor = 0; DWORD dwCount; int ftdi_init() { FT_DEVICE ftDevice; DWORD deviceID; char SerialNumber[16+1]; char Description[64+1]; // Does an FTDI device exist? printf("Checking for FTDI devices...\n"); ftStatus = FT_CreateDeviceInfoList(&dwNumDevs); // Get the number of FTDI devices if (ftStatus != FT_OK) // Did the command execute OK? { printf("Error in getting the number of devices\n"); return 1; // Exit with error } if (dwNumDevs < 1) // Exit if we don't see any { printf("There are no FTDI devices installed\n"); return 1; // Exist with error } printf("%d FTDI devices found - the count includes individual ports on a single chip\n", dwNumDevs); // Open the port - For this application note, assume the first device is a FT2232H or FT4232H // Further checks can be made against the device descriptions, locations, serial numbers, etc. // before opening the port. printf("Assume first device has the MPSSE and open it...\n"); ftStatus = FT_Open(0, &ftHandle); if (ftStatus != FT_OK) { printf("Open Failed with error %d\n", ftStatus); printf("If runing on Linux then try <rmmod ftdi_sio> first\n"); return 1; // Exit with error } memset(SerialNumber, 0, sizeof(SerialNumber)); memset(Description, 0, sizeof(Description)); // Detecting device type, hi-speed or full-speed ftStatus = FT_GetDeviceInfo( ftHandle, &ftDevice, &deviceID, SerialNumber, Description, NULL ); ft232H = 0; // deviceID contains encoded device ID // SerialNumber, Description contain 0-terminated strings if (ftStatus == FT_OK) { printf("Device: %s\nSerial: %s\n", Description, SerialNumber); if (/* ftDevice == FT_DEVICE_232H || */ ftDevice == FT_DEVICE_4232H || ftDevice == FT_DEVICE_2232H) { printf("Hi-speed device (FT232H, FT2232H or FT4232H) detected\n"); ft232H = 1; } } else { printf("FT_GetDeviceType FAILED!\n"); } // Configure port parameters printf("Configuring port for MPSSE use...\n"); //Reset USB device ftStatus |= FT_ResetDevice(ftHandle); //Purge USB receive buffer first by reading out all old data from FT2232H receive buffer // Get the number of bytes in the FT2232H receive buffer ftStatus |= FT_GetQueueStatus(ftHandle, &dwNumBytesToRead); //Read out the data from FT2232H receive buffer if ((ftStatus == FT_OK) && (dwNumBytesToRead > 0)) FT_Read(ftHandle, &byInputBuffer, dwNumBytesToRead, &dwNumBytesRead); //Set USB request transfer sizes to 64K ftStatus |= FT_SetUSBParameters(ftHandle, 65536, 65535); //Disable event and error characters ftStatus |= FT_SetChars(ftHandle, false, 0, false, 0); //Sets the read and write timeouts in milliseconds ftStatus |= FT_SetTimeouts(ftHandle, 0, 5000); //Set the latency timer (default is 16mS) ftStatus |= FT_SetLatencyTimer(ftHandle, 16); ftStatus |= FT_SetBitMode(ftHandle, 0x0, 0x00); //Reset controller ftStatus |= FT_SetBitMode(ftHandle, 0x0, 0x02); //Enable MPSSE mode if (ftStatus != FT_OK) { printf("Error in initializing the MPSSE %d\n", ftStatus); FT_Close(ftHandle); return 1; // Exit with error } //return with success return 0; } int configure_mpsse() { unsigned char bCommandEchod; // ----------------------------------------------------------- // Synchronize the MPSSE by sending a bogus opcode (0xAA), // The MPSSE will respond with "Bad Command" (0xFA) followed by // the bogus opcode itself. // ----------------------------------------------------------- // Reset output buffer pointer dwNumBytesToSend=0; //Add bogus command 'xAA' to the queue byOutputBuffer[dwNumBytesToSend++] = 0xAA; // Send off the BAD commands ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); do { // Get the number of bytes in the device input buffer ftStatus = FT_GetQueueStatus(ftHandle, &dwNumBytesToRead); } while ((dwNumBytesToRead == 0) && (ftStatus == FT_OK)); //Read out the data from input buffer ftStatus = FT_Read(ftHandle, &byInputBuffer, dwNumBytesToRead, &dwNumBytesRead); //Check if Bad command and echo command received bCommandEchod = false; for (dwCount = 0; dwCount < dwNumBytesRead - 1; dwCount++) { if ((byInputBuffer[dwCount] == 0xFA) && (byInputBuffer[dwCount+1] == 0xAA)) { bCommandEchod = true; break; } } if (bCommandEchod == false) { printf("Error in synchronizing the MPSSE\n"); FT_Close(ftHandle); return 1; // Exit with error } // ----------------------------------------------------------- // Configure the MPSSE settings for JTAG // Multiple commands can be sent to the MPSSE with one FT_Write // ----------------------------------------------------------- // Set up the Hi-Speed specific commands for the FTx232H // Start with a fresh index dwNumBytesToSend = 0; if (ft232H) { // Use 60MHz master clock (disable divide by 5) byOutputBuffer[dwNumBytesToSend++] = 0x8A; // Turn off adaptive clocking (may be needed for ARM) byOutputBuffer[dwNumBytesToSend++] = 0x97; // Disable three-phase clocking byOutputBuffer[dwNumBytesToSend++] = 0x8D; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); } // Send off the HS-specific commands dwNumBytesToSend = 0; // Set initial states of the MPSSE interface - low byte, both pin directions and output values // Pin name Signal Direction Config Initial State Config // ADBUS0 GPIOL0 input 0 0 // ADBUS1 GPIOL1 input 0 0 // ADBUS2 GPIOL2 input 0 0 // ADBUS3 GPIOL3 input 0 0 // ADBUS4 GPIOL4 input 0 0 // ADBUS5 GPIOL5 input 0 0 // ADBUS6 GPIOL6 input 0 0 // ADBUS7 GPIOL7 input 0 0 // Set data bits low-byte of MPSSE port byOutputBuffer[dwNumBytesToSend++] = 0x80; // Initial state config above byOutputBuffer[dwNumBytesToSend++] = 0x00; // Direction config above byOutputBuffer[dwNumBytesToSend++] = 0x00; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); // Send off the low GPIO config commands dwNumBytesToSend = 0; // Set initial states of the MPSSE interface - high byte, both pin directions and output values // Pin name Signal Direction Config Initial State Config // ACBUS0 GPIOH0 input 0 0 // ACBUS1 GPIOH1 input 0 0 // ACBUS2 GPIOH2 input 0 0 // ACBUS3 GPIOH3 input 0 0 // ACBUS4 GPIOH4 input 0 0 // ACBUS5 GPIOH5 input 0 0 // ACBUS6 GPIOH6 input 0 0 // ACBUS7 GPIOH7 input 0 0 // Set data bits low-byte of MPSSE port byOutputBuffer[dwNumBytesToSend++] = 0x82; // Initial state config above byOutputBuffer[dwNumBytesToSend++] = 0x00; // Direction config above byOutputBuffer[dwNumBytesToSend++] = 0x00; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); // Send off the high GPIO config commands dwNumBytesToSend = 0; // Set TCK frequency // TCK = 60MHz /((1 + [(1 +0xValueH*256) OR 0xValueL])*2) //Command to set clock divisor byOutputBuffer[dwNumBytesToSend++] = 0x86; //Set 0xValueL of clock divisor byOutputBuffer[dwNumBytesToSend++] = dwClockDivisor & 0xFF; //Set 0xValueH of clock divisor byOutputBuffer[dwNumBytesToSend++] = (dwClockDivisor >> 8) & 0xFF; // Send off the clock divisor commands ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); dwNumBytesToSend = 0; // Disable internal loop-back // Disable loopback byOutputBuffer[dwNumBytesToSend++] = 0x85; // Send off the loopback command ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); return ftStatus; } void set_bits(unsigned char dir, unsigned char val) { // Send off the low GPIO config commands dwNumBytesToSend = 0; // Set data bits low-byte of MPSSE port byOutputBuffer[dwNumBytesToSend++] = 0x80; // Initial state config above byOutputBuffer[dwNumBytesToSend++] = val; // Direction config above byOutputBuffer[dwNumBytesToSend++] = dir; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); } int read_bits(unsigned char* presult) { dwNumBytesToSend = 0; if(presult) *presult = 0; // force read status byOutputBuffer[dwNumBytesToSend++] = 0x81; //complete wr buffer with fast reply command byOutputBuffer[dwNumBytesToSend++] = 0x87; ftStatus = FT_Write(ftHandle, byOutputBuffer, dwNumBytesToSend, &dwNumBytesSent); if( ftStatus != FT_OK || dwNumBytesSent != dwNumBytesToSend ) return 1; //error //wait for answer do { // Get the number of bytes in the device input buffer ftStatus = FT_GetQueueStatus(ftHandle, &dwNumBytesToRead); } while ((dwNumBytesToRead == 0) && (ftStatus == FT_OK)); if( ftStatus != FT_OK ) return 1; //error //read status bits from programmer ftStatus = FT_Read(ftHandle, presult, 1, &dwNumBytesToRead); if( ftStatus != FT_OK ) return 1; //error return 0; } int _tmain(int argc, _TCHAR* argv[]) { if( ftdi_init() ) { printf("Cannot init FTDI chip\n"); return -1; } if( configure_mpsse() ) { printf("Cannot init MPSSE chip\n"); return -1; } for(int i=0; i<20; i++) { unsigned char input_pins; set_bits(0x03,0x01); read_bits(&input_pins); printf("input %02X%s\n",input_pins, (input_pins&8) ? "" : "*******" ); Sleep(1000); set_bits(0x03,0x02); read_bits(&input_pins); printf("input %02X%s\n",input_pins, (input_pins&8) ? "" : "*******" ); Sleep(1000); } return 0; }
Теперь об управлении собственно выводами микросхемы.
Для установки выводов канала микросхемы в заданное состояние нужно записать (FT_Write) в нее всего три байта:
- Cmd 0x80 команда установки бит данных.
- 0xValue байт, определяющий побитно выходное значение на 8 выводах канала.
- 0xDirection байт, побитно определяющий состояние контактов канала: будут ли они выходами или входами. Логическая единица - выход. Логический ноль - линия будет входом. В нашей программе функция void set_bits(unsigned char dir, unsigned char val) работает наустановку состояния линий канала.
Для чтения текущего состояния линий канала есть команда 0x81. Просто записываем (FT_Write) один байт 0x81 и далее ожидаем, что через некоторое время сможем прочитать (FT_Read) из порта один байт отклика - состояние линий. В нашей программе - это функция int read_bits(unsigned char* presult);
Теперь еще одно важное дополнение.
Очень часто требуется не только управлять отдельными внешними линиями, но и одновременно с их установкой читать другие входные линии. Несмотря на то, что микросхема FTDI 2232HL высокоскоростная и работает на шине USB2 все равно побитное управление отдельными линиями типа установил-прочитал-установил-прочитал-установил-прочитал-установил-прочитал может оказаться очень медленным, если писать и читать короткими блоками. На самом деле все решается довольно просто средствами самой микросхемы и ее внутреннего фифо.
Чтобы выполнить передачу и одновременное чтение нужно всего навсего сформировать длинный блок команд для записи FT_Write:
0x80 0x?? 0x?? 0x81 0x80 0x?? 0x?? 0x81 0x80 0x?? 0x?? 0x81 0x80 0x?? 0x?? 0x81 0x80 0x?? 0x?? 0x81 ... 0x87
Пишем в порт с помощью функции FT_Write большой блок данных, где последовательность команд как раз определяет последовательность операций записи и чтения линий. Потом ожидаем считать длинный блок данных откликов из микросхемы. Сколько было послано команд 0x81 столько и сможем прочитать байт из порта данных с помощью функции FT_Read. Каждый прочитанный байт будет определять состояние входных линий канала в момент исполнения команды 0x81 в микросхеме FTDI.
Еще обратите внимание - в конце блока команд я поставил особую команду 0x87 - это Send Immediate. Эта команда заставляет микросхему отправлять байты накопленные в ее выходном буфере в компьютер как можно быстрее. FT_Read - это функция блокирующая вызвавший ее поток. Она либо будет ждать пока не прочитает все заявленные байты либо вернется с ошибкой таймаута, если не дождется всех обещанных байт. Величина таймаута зависит от ранее вызванной во время инициализации порта FT_SetTimeouts. В любом случае завершающая команда 0x87 ускоряет отклик.
Еще один важный момент. С какой частотой микросхема будет исполнять свой буфер команд и передавать биты / байты в порт?
На самом деле частоту передачи можно установить программно.
Есть специальная команда микросхемы (три байта, которые нужно записать в порт с помощью функции FT_Write):
- 0x86,
- 0xValueL,
- 0xValueH
Здесь два байта 0xValueL и 0xValueH определяют частоту передачи:
TCK = 60MHz /( (1 + [(1 + 0xValueH * 256) OR 0xValueL])*2 )
Исходя из этого можно выбрать и установить желаемую частоту работы приемопередатчика.
Ну и теперь собственно видео, демонстрирующее работу программы: мигаем светодиодами и опрашиваем кнопочку:
Подробнее...