📜 ⬆️ ⬇️

Minicomputer from a router with OpenWRT: we are developing a USB video card


Good afternoon, dear habrovchane. In this series of articles, we will go through a rather long but very interesting way of turning an ordinary router into a mini-computer with an LCD display. To do this, we will first develop a USB video card based on the STM32F103 microcontroller, then a test driver, which will allow us to display graphics on it, and finally - a full-fledged framebuffer driver, through which you can run real graphical applications, such as x-server. At the same time, we will learn to include our code in the OpenWRT source tree, finish its kernel and do other useful things.
Well, at the very end we will get the result, which I hope will cause a nostalgic tear in many readers. I will try to present the material in such a way that at the end of each stage we will get a tangible result that does not let the enthusiasm die away. So, let's begin.

Iron


Traditionally, let's see what we need of iron. Those who do not like to solder, I can immediately reassure - all that we will do is purely furvarnoe and software, so we don’t have to solder. But on the other hand, you will need debugging on the STM32F103VE controller and QVGA display, which I already mentioned in my article .
We also need, in fact, the router, which I also mentioned in another article , but we will return to it later. Now let's focus on the development of the USB video card itself based on our debug board.

The STM32F103 has two blocks, which will be very useful to us. The first of them is, of course, a hardware USB controller that is perfect for organizing the connection between our video card and the host. The second - FSMC - Flexible Static Memory Controller. It is a static memory controller that can be configured for use with SRAM chips, NAND / NOR FLASH and similar devices, and the bus width and timings in it are configurable. In this case, the chips are mapped to the address space and, when addressed to the appropriate address, the FSMC itself generates the necessary signals on the control lines and address and data buses, so that for the programmer this process is completely transparent.

In the case of the display, this will help us a lot, since displays of this kind are equipped with an interface that almost completely coincides with the interface of a NOR flash drive and SRAM: a 16-bit data bus, CS, ~ WR, ~ RD gates. With these signals, everything is simple - CS activates the display, it begins the cycle of data exchange. ~ WR or ~ RD are activated when they are read or written, respectively.
Unfortunately, the display does not provide us with its own bus address and data for access directly to the video memory, instead, we have a single 16-bit bus and an additional RS signal, Register Select, available to us. When the RS signal is active, the value set for this bus is taken as the register address (RAM cell) of the display controller to be addressed, and the subsequent reading or writing with an inactive RS is the display RAM operation.
')
It should be noted that the RAM in this case is not the video memory, it is the memory of the display controller, which is accessible from the outside through the mechanism described above. Video memory also appears in the documentation as GRAM, Graphics RAM, and its contents are accessible through the “window” in the form of one of the registers. In this case, the logic of the display controller itself increments / decrements the internal address of the video memory when sequentially reading from the same register (CTR_WRITE_DATA, CTR_READ_DATA)
FSMC does not have a specialized RS signal, so one trick is used for this: one of the available FSMC address bus signals is connected to the RS signal.
Suppose we connect the signal A0 to the RS. Then, when accessing the memory address 0x00000000 to write (relative to the base address to which the FSMC bank is mapped), the RS signal will be inactive and the display will take this as setting the register address.
When accessing 0x00000001, the address line A0 will be active, and reading or writing will be performed for the RAM cell, that is, for the register whose address was specified when accessing the zero address.
You can read more about this in the apnote from the STM on this issue.
The description of the registers of the display controller is available to its datasheet .

By the way, you should be careful with the datasheets and carefully look at its version, because the Chinese comrades adore first copying the microchip itself (not completely, but how it will turn out), and then copying the datasheet from this microchip. Therefore, in the process of reading a datasheet, you can surprisingly find registers and functions that this controller has never supported and which are already wiped off in the next version of the datasheet.
For example, early versions of the datasheet on this controller report that the display can do hardware bitwise operations, including a hardware mask to implement transparency, however, if you dig deeper, it turns out that this line got into the datasheet on ILI9325 from the datasheet of another Japanese controller display, which the Chinese safely copied and called them compatible.

Since the display is already connected to the Mini-STM32, all we need is to find out which of the chip select signals it is connected to and which address line is used as the RS signal.
According to the scheme, FSMC_NE1 is used as the CS signal, and FSMC_A16 is used as the RS.
The display also has a Reset signal output to PE1 and a backlight control signal connected to the PD13.
At the same time, let's see which of the signals is used to connect a USB pull-up, which we'll talk about later - in this scheme it is PC13.

So go to the code.

Soft


LCD operation

Let's start with the development of a small library to work with the display. We will work in CooCox IDE. We put in the header file all the addresses of the registers from the datasheet:

LCD Register Announcement
#define CTR_OSC_START 0x0000 #define CTR_DRV_OUTPUT1 0x0001 #define CTR_DRV_WAVE 0x0002 #define CTR_ENTRY_MODE 0x0003 #define CTR_RESIZE 0x0004 #define CTR_DISPLAY1 0x0007 #define CTR_DISPLAY2 0x0008 #define CTR_DISPLAY3 0x0009 #define CTR_DISPLAY4 0x000A #define CTR_RGB_INTERFACE1 0x000C #define CTR_FRM_MARKER 0x000D #define CTR_RGB_INTERFACE2 0x000F #define CTR_POWER1 0x0010 #define CTR_POWER2 0x0011 #define CTR_POWER3 0x0012 #define CTR_POWER4 0x0013 #define CTR_HORZ_ADDRESS 0x0020 #define CTR_VERT_ADDRESS 0x0021 #define CTR_WRITE_DATA 0x0022 #define CTR_READ_DATA 0x0022 #define CTR_POWER7 0x0029 #define CTR_FRM_COLOR 0x002B #define CTR_GAMMA1 0x0030 #define CTR_GAMMA2 0x0031 #define CTR_GAMMA3 0x0032 #define CTR_GAMMA4 0x0035 #define CTR_GAMMA5 0x0036 #define CTR_GAMMA6 0x0037 #define CTR_GAMMA7 0x0038 #define CTR_GAMMA8 0x0039 #define CTR_GAMMA9 0x003C #define CTR_GAMMA10 0x003D #define CTR_HORZ_START 0x0050 #define CTR_HORZ_END 0x0051 #define CTR_VERT_START 0x0052 #define CTR_VERT_END 0x0053 #define CTR_DRV_OUTPUT2 0x0060 #define CTR_BASE_IMAGE 0x0061 #define CTR_VERT_SCROLL 0x006A #define CTR_PIMG1_POS 0x0080 #define CTR_PIMG1_START 0x0081 #define CTR_PIMG1_END 0x0082 #define CTR_PIMG2_POS 0x0083 #define CTR_PIMG2_START 0x0084 #define CTR_PIMG2_END 0x0085 #define CTR_PANEL_INTERFACE1 0x0090 #define CTR_PANEL_INTERFACE2 0x0092 #define CTR_PANEL_INTERFACE4 0x0095 #define CTR_OTP_VCMPROGRAM 0x00A1 #define CTR_OTP_VCMSTATUS 0x00A2 #define CTR_OTP_IDKEY 0x00A5 


We remember that from the point of view of the code, accessing the FSMC will be a simple write / read from memory, so we need to determine by which addresses to access. We look in the reference manual on STM32, section FSMC, and we see that for NOR / SRAM, addresses starting with 0x60000000 are highlighted.
By banks in a broad sense, the manual refers to large regions allocated for devices of different types, for example, bank # 1 is NOR / SRAM, banks # 2 and # 3 are NAND, bank # 4 is a PC Card.
In turn, bank # 1 can be used to access as many as 4 memory chips, each of which can independently of the others be NOR or SRAM. Since the display is connected as NE1, we are interested in the bank announced as FSMC_Bank1_NORSRAM1. Based on the base address, you can immediately write the definition

 #define LCDRegister (*((volatile uint16_t*) 0x60000000)) 

The address that activates the RS will be the address where the A16 line is active, that is, for example, 0x60000000 + (2 << 16), that is, 0x60020000, so we write

 #define LCDMemory (*((volatile uint16_t*) 0x60020000)) 

And immediately define the corresponding macros for writing values ​​to the display registers and to its memory:

 #define LCD_WRITE_REGISTER(REG, DATA) LCDRegister=REG;LCDMemory=DATA; #define LCD_BEGIN_RAM_WRITE LCDRegister=CTR_WRITE_DATA; #define LCD_WRITE_RAM(DATA) LCDMemory=DATA 

At the same time, we will define the names of the pins and their ports, responsible for resetting the display and turning on the backlight:

 #define BacklightPin GPIO_Pin_13 #define BacklightPort GPIOD #define ResetPin GPIO_Pin_1 #define ResetPort GPIOE 

Now we will write the initialization code for the FSMC and associated peripherals:

FSMC Initialization Code
 void LCDInitHardware() { SysTick_Config(SystemCoreClock/1000); GPIO_InitTypeDef GPIO_InitStructure; FSMC_NORSRAMInitTypeDef FSMC_InitStructure; FSMC_NORSRAMTimingInitTypeDef FSMC_Timing; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10| GPIO_Pin_11| GPIO_Pin_14| GPIO_Pin_15; //Interface GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11| GPIO_Pin_12| GPIO_Pin_13| GPIO_Pin_14 | GPIO_Pin_15; //Interface GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = BacklightPin; //Backlight GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(BacklightPort, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = ResetPin; //Reset GPIO_Init(ResetPort, &GPIO_InitStructure); GPIO_SetBits(ResetPort,ResetPin); FSMC_Timing.FSMC_AddressSetupTime = 1; FSMC_Timing.FSMC_AddressHoldTime = 0; FSMC_Timing.FSMC_DataSetupTime = 5; FSMC_Timing.FSMC_BusTurnAroundDuration = 0; FSMC_Timing.FSMC_CLKDivision = 0; FSMC_Timing.FSMC_DataLatency = 0; FSMC_Timing.FSMC_AccessMode = FSMC_AccessMode_B; FSMC_InitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM1; FSMC_InitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; FSMC_InitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM; FSMC_InitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; FSMC_InitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; FSMC_InitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; FSMC_InitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_InitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; FSMC_InitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; FSMC_InitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_InitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; FSMC_InitStructure.FSMC_AsynchronousWait = FSMC_AsynchronousWait_Disable; FSMC_InitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_InitStructure.FSMC_ReadWriteTimingStruct = &FSMC_Timing; FSMC_InitStructure.FSMC_WriteTimingStruct = &FSMC_Timing; FSMC_NORSRAMInit(&FSMC_InitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE); } 


Everything is quite simple here - we set the system timer to one millisecond interval (which will be necessary to organize delays in the initialization process of the display controller), then we set up all the signals that the FSMC owns as controlled by “alternative functions”, set the Reset and Backlight pins as Output Push-pull, then go to the settings FSMC.
Timing settings you can try to calculate yourself, I took the recommended from the apnute from the STM and they came up perfectly. We set the memory type as SRAM, 16-bit bus width, disabling all additional features, the description of which takes more than one page of datasheet.
Let's declare an auxiliary function that allows us to make a fairly accurate delay:

Delay function on SysTick-timer
 volatile uint32_t Tick = 0x00000000; void SysTick_Handler() { if(Tick>0) Tick--; } void SysTickDelay(uint32_t msDelay) { Tick=msDelay; while(Tick); } 


Next, write the initialization function directly display:

LCD Initialization
 void LCDInit() { LCDHardwareReset(); LCD_WRITE_REGISTER(CTR_OSC_START, 0x0001); LCD_WRITE_REGISTER(CTR_DRV_OUTPUT1, 0x0100); LCD_WRITE_REGISTER(CTR_DRV_WAVE, 0x0700); LCD_WRITE_REGISTER(CTR_ENTRY_MODE, 0x1038); LCD_WRITE_REGISTER(CTR_RESIZE, 0x0000); LCD_WRITE_REGISTER(CTR_DISPLAY2, 0x0202); LCD_WRITE_REGISTER(CTR_DISPLAY3, 0x0000); LCD_WRITE_REGISTER(CTR_DISPLAY4, 0x0000); LCD_WRITE_REGISTER(CTR_RGB_INTERFACE1, 0x0001); LCD_WRITE_REGISTER(CTR_FRM_MARKER, 0x0000); LCD_WRITE_REGISTER(CTR_RGB_INTERFACE2, 0x0000); LCD_WRITE_REGISTER(CTR_POWER1, 0x0000); LCD_WRITE_REGISTER(CTR_POWER2, 0x0007); LCD_WRITE_REGISTER(CTR_POWER3, 0x0000); LCD_WRITE_REGISTER(CTR_POWER4, 0x0000); SysTickDelay(200); LCD_WRITE_REGISTER(CTR_POWER1, 0x1590); LCD_WRITE_REGISTER(CTR_POWER2, 0x0227); SysTickDelay(50); LCD_WRITE_REGISTER(CTR_POWER3, 0x009C); SysTickDelay(50); LCD_WRITE_REGISTER(CTR_POWER4, 0x1900); LCD_WRITE_REGISTER(CTR_POWER7, 0x1900); LCD_WRITE_REGISTER(CTR_FRM_COLOR, 0x000E); SysTickDelay(50); LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS, 0x0000); LCD_WRITE_REGISTER(CTR_VERT_ADDRESS, 0x0000); LCD_WRITE_REGISTER(CTR_HORZ_START, 0x0000); LCD_WRITE_REGISTER(CTR_HORZ_END, 239); LCD_WRITE_REGISTER(CTR_VERT_START, 0x0000); LCD_WRITE_REGISTER(CTR_VERT_END, 319); LCD_WRITE_REGISTER(CTR_DRV_OUTPUT2, 0x2700); LCD_WRITE_REGISTER(CTR_BASE_IMAGE, 0x0001); LCD_WRITE_REGISTER(CTR_VERT_SCROLL, 0x0000); GPIO_SetBits(BacklightPort,BacklightPin); } 


The sequence is taken from the datasheet to the display controller, all that needs to be done is simply to initialize certain registers in the right order, turning on its oscillator and power, as well as withstand the recommended delays, allowing the display schematics to settle. Also in this code, the display modes are set, such as the behavior of the address counter when writing to GRAM — you can force the display to increase or decrease the counter, move the indicators down or up — that is, set the direction in which pixels will be painted when they are output. .
In this code, the LCDHardwareReset function is a small block of code that exposes the active level on the Reset line, waiting for some time and resetting it to the inactive state:

 void LCDHardwareReset() { GPIO_ResetBits(ResetPort,ResetPin); SysTickDelay(50); GPIO_SetBits(ResetPort,ResetPin); SysTickDelay(10); } 

We introduce a couple of control functions responsible for turning the display on and off at the level of its controller:

 void LCDOn() { LCD_WRITE_REGISTER(CTR_DISPLAY1, 0x0133); } void LCDOff() { LCD_WRITE_REGISTER(CTR_DISPLAY1, 0x0131); } 

It remains quite a bit - we declare a very important function for us, which sets a rectangle within which the graph will be displayed. At the same time, work with the output address lies on the shoulders of the display controller, all we need is to set the boundaries of this area:

  void LCDSetBounds(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { LCD_WRITE_REGISTER(CTR_VERT_START, left); LCD_WRITE_REGISTER(CTR_VERT_END, right); LCD_WRITE_REGISTER(CTR_HORZ_START, top); LCD_WRITE_REGISTER(CTR_HORZ_END, bottom); LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS, top); LCD_WRITE_REGISTER(CTR_VERT_ADDRESS, left); } 

From the name of the registers it is immediately clear that the function sets the left, right, upper and lower bounds, and then sets the pointer to the position corresponding to the upper left pixel.
Since these pointers refer to the vertical position of the display, and we will use it horizontally, we should swap the left and top places, recording the left value in the registers related to VERTICAL, and top to the HORIZONTAL.
And finally, the function with which we can immediately verify the correct operation of the display is the cleaning function. It is a simple sequential recording of the same color value in the GRAM display:

 void LCDClear(uint16_t color) { LCDSetBounds(0,0,320-1,240-1); LCD_BEGIN_RAM_WRITE; uint32_t i; for(i=0;i<320*240;i++) LCD_WRITE_RAM(color); } 

All we need to do to turn on our display is to run

 LCDInitHardware(); LCDInit(); LCDOn(); LCDClear(0x0000); 

If done correctly, the display will turn on and paint over black. When replacing the function argument LCDClear and restarting the program, the display should turn to the selected color.
We now turn to the more difficult part - USB.

Work with USB

Working with USB is a very extensive and multifaceted topic, for which one article is clearly not enough.
Therefore, I recommend that you start by reading a very useful document called USB In A Nutshell ( English version , Russian version ) before continuing.
To sum up briefly, one of the basic concepts in the USB interface is Endpoint, which with some stretch can be called an analogue of a socket.
Just as a socket can be opened for UDP or for TCP connections, endpoints are also divided into types.

  1. Control Endpoints are used for asynchronous transfer of small control messages, up to 64 bytes in the case of Full Speed ​​USB. Zero endpoint must be in any USB device and it must be Control. Through it, the host requests initial information and initializes the device. Package delivery through endpoints of this type is guaranteed, in case of an error, the host automatically tries to resend the data. Control Endpoint is the only bidirectional type of endpoint, all others work either to receive or only to transfer data.
  2. Interrupt Endpoints - endpoints for which polling by a host is important at a given frequency (we remember that USB devices cannot initiate a transfer themselves, they are waiting for a request from the host). Usually this type of points is used in HID-devices such as keyboard and mouse. The packet size can also be up to 64 bytes in FS USB. Delivery is guaranteed.
  3. Isochronous Endpoint - endpoint, used mainly for transferring audio and video information, where packet loss is not terrible - in this type of endpoint only data integrity is checked, no re-sending in case of an error. Package size up to 1024 bytes in FS USB.
  4. Bulk Enpoints - the main type of endpoints used in storage devices.
    It involves sending data packets with guaranteed delivery and a packet size of up to 64 bytes for FS USB (for High Speed, the packet size can be up to 1023 bytes). In the USB controller of our STM32 it is possible to organize hardware double buffering for packets of this type, increasing throughput. All available bandwidth remaining after Interrupt, Control and Isochronous is divided between Bulk endpoints - so you should pay attention to which other transmissions are running in parallel with Bulk.

The type of endpoint that is most suitable for our case is Bulk, we will send blocks of graphic information that will be displayed on the display, and in consequence it will be possible to arrange double buffering. At the same time, we do not need any periodicity in the premises and the "streaming" essence of Isochronous endpoints, since graphics data will go in an uncompressed format to arbitrary display positions and lose packets to us absolutely to anything.
In order for the host to understand what device we have, what endpoint it has and what features it has, the host requests several descriptors via Control Endpoint # 0.
The structure described by them is perfectly shown in the figure from the article mentioned above:
image
Let's return to them a bit later, and now let's move on to considering what needs to be added to our firmware in order to make the device work with USB. We will work consistently and point by point.
  1. To begin, download the library from STM to work with USB from the official site and place the folder STM32_USB-FS-Device_Driver from the depths of the downloaded archive (located in the Libraries folder) in the folder with our project. Add it to the project by selecting File - Add Linked Folder . We add a new folder to our project, calling it somehow like usb_user, in which we create the files hw_config.h and usb_conf.h - these files are required by the library from STM.
    Immediately write to hw_config.h

     #include "stm32f10x.h" 

    otherwise there will be a lot of errors from unresolved types (uint8_t, uint16_t, uint32_t, ...)
    Do not forget to specify the folder with the library and our usb_user in the project properties as additional paths for searching for inclusions.
  2. Add a new header file to usb_user, which will contain the declarations necessary for the descriptors, let's call it, say, usb_desc.h, putting the following code there:

     #include "usb_lib.h" #define SIZ_DEVICE_DESC 0x12 #define SIZ_CONFIG_DESC 0x19 #define SIZ_STRING_LANGID 0x04 #define SIZ_STRING_VENDOR 0x10 #define SIZ_STRING_PRODUCT 0x10 #define SIZ_STRING_SERIAL 0x12 extern const uint8_t USB_ConfigDescriptor[SIZ_CONFIG_DESC]; extern ONE_DESCRIPTOR Device_Descriptor; extern ONE_DESCRIPTOR Config_Descriptor; extern ONE_DESCRIPTOR String_Descriptor[4]; 

    Here defajny, beginning on " SIZ_ " contain the sizes of future descriptors. In the design process, these dimensions are determined after the descriptors have been written, but since I have already designed the device, you can simply copy it.
    extern const uint8_t USB_ConfigDescriptor we bring to the header just because we need access to this structure from the main module. The rest of the descriptors are not required, for the reason that we will not give the descriptors in the form of arrays uint_8 to the library, but special structures called ONE_DESCRIPTOR , which, for this occasion, are declared below. There is nothing wrong with them, they are simply two-member structures, the first of which is a pointer to the same descriptor in the form of uint8_t * , the second is the sixteen-bit length of this descriptor.
    Now let's move on to the descriptors, by adding a new usb_desc.c file and connecting our header there.
    We start with the descriptor of the device itself. All the necessary information about the fields is in the USB In A Nutshell article, I will only note that all descriptors begin with the byte length of the descriptor (therefore we took them to the define), followed by the byte - the type of descriptor.
    This is how the device handle looks:

    Device handle
     const uint8_t USB_DeviceDescriptor[SIZ_DEVICE_DESC] = { 0x12, /* bLength */ 0x01, /* bDescriptorType */ 0x00, 0x02, /* bcdUSB = 2.00 */ 0xFF, /* bDeviceClass: Vendor Specific */ 0x00, /* bDeviceSubClass */ 0x00, /* bDeviceProtocol */ 0x40, /* bMaxPacketSize0 */ 0xAD, 0xDE, /* idVendor*/ 0x0D, 0xF0, /* idProduct*/ 0x00, 0x01, /* bcdDevice = 2.00 */ 1, /* Index of string descriptor describing manufacturer */ 2, /* Index of string descriptor describing product */ 3, /* Index of string descriptor describing the device's serial number */ 0x01 /* bNumConfigurations */ }; 



    We create a “vendor-specific” device (not belonging to any specific predefined class like HID), with VID = 0xDEAD and PID = 0xF00D, with a single configuration and a maximum packet size of 64 bytes.
    Next, we declare a configuration descriptor that includes interface descriptors and endpoints:

    Configuration descriptor
     const uint8_t USB_ConfigDescriptor[SIZ_CONFIG_DESC] = { /*Configuration Descriptor*/ 0x09, /* bLength: Configuration Descriptor size */ 0x02, /* bDescriptorType: Configuration */ SIZ_CONFIG_DESC, /* wTotalLength:no of returned bytes */ 0x00, 0x01, /* bNumInterfaces: 1 interface */ 0x01, /* bConfigurationValue: Configuration value */ 0x00, /* iConfiguration: Index of string descriptor describing the configuration */ 0xE0, /* bmAttributes: bus powered */ 0x32, /* MaxPower 100 mA */ /*Interface Descriptor*/ 0x09, /* bLength: Interface Descriptor size */ 0x04, /* bDescriptorType: Interface */ 0x00, /* bInterfaceNumber: Number of Interface */ 0x00, /* bAlternateSetting: Alternate setting */ 0x01, /* bNumEndpoints: One endpoints used */ 0xFF, /* bInterfaceClass: Vendor Specific*/ 0x00, /* bInterfaceSubClass*/ 0x00, /* bInterfaceProtocol*/ 0x00, /* iInterface: */ /*Endpoint 1 Descriptor*/ 0x07, /* bLength: Endpoint Descriptor size */ 0x05, /* bDescriptorType: Endpoint */ 0x01, /* bEndpointAddress: (OUT1) */ 0x02, /* bmAttributes: Bulk */ 0x40, /* wMaxPacketSize: */ 0x00, 0x00 /* bInterval: */ }; 


    Here you have to be careful - the first byte is the size only, in fact, the config-descriptor, which is always 0x09 bytes. Next comes the type of this descriptor, and then comes the two-byte length of the entire array, including the config descriptor, interface descriptors and endpoints. In this case, it fits into one byte, so the second is left zero.
    Then we write that we have one interface, one configuration (located at index 0), that the device is bus-powered and consumes no more than 100 mA.
    Further, in the same array, there is an interface descriptor, the same 0x09 bytes in size, two indices, both zeros, on which the host accesses this particular interface, the number of endpoints not counting zero - we will have one, the device class, again “Vendor Specific” , no subclasses, no protocols, no string interface descriptors (zeroes in all corresponding bytes).
    Finally, the last is the descriptor of our only endpoint. Endpoint 0 does not need one, so we immediately describe Bulk Endpoin 1. The address byte sets not only the endpoint number but also the direction of transmission with its high-order bit. Set the type - Bulk, the maximum packet size (attention, two bytes are allocated for it!) And leave the last byte equal to zero, since It does not play a role for Bulk-endpoints.
    Further we declare string descriptors:

    String Descriptors
     /* USB String Descriptors */ const uint8_t USB_StringLangID[SIZ_STRING_LANGID] = { SIZ_STRING_LANGID, /* bLength */ 0x03, /* String descriptor */ 0x09, 0x04 /* LangID = 0x0409: US English */ }; const uint8_t USB_StringVendor[SIZ_STRING_VENDOR] = { SIZ_STRING_VENDOR, /* Size of Vendor string */ 0x03, /* bDescriptorType*/ /* Manufacturer: "Amon-Ra" */ 'A', 0, 'm', 0, 'o', 0, 'n', 0, '-', 0, 'R', 0, 'a', 0 }; const uint8_t USB_StringProduct[SIZ_STRING_PRODUCT] = { SIZ_STRING_PRODUCT, /* bLength */ 0x03, /* bDescriptorType */ /* Product name: "USB LCD" */ 'U', 0, 'S', 0, 'B', 0, ' ', 0, 'L', 0, 'C', 0, 'D', 0 }; uint8_t USB_StringSerial[SIZ_STRING_SERIAL] = { SIZ_STRING_SERIAL, /* bLength */ 0x03, /* bDescriptorType */ 'U', 0, 'S', 0, 'B', 0, 'L', 0, 'C', 0, 'D', 0, '0', 0, '1', 0 }; 


    Unicode strings are written and can be changed as desired.
    Finally, fill in the structures that are required by the library:

    Library Structures
     ONE_DESCRIPTOR Device_Descriptor = { (uint8_t*)USB_DeviceDescriptor, SIZ_DEVICE_DESC }; ONE_DESCRIPTOR Config_Descriptor = { (uint8_t*)USB_ConfigDescriptor, SIZ_CONFIG_DESC }; ONE_DESCRIPTOR String_Descriptor[4] = { {(uint8_t*)USB_StringLangID, SIZ_STRING_LANGID}, {(uint8_t*)USB_StringVendor, SIZ_STRING_VENDOR}, {(uint8_t*)USB_StringProduct, SIZ_STRING_PRODUCT}, {(uint8_t*)USB_StringSerial, SIZ_STRING_SERIAL} }; 


  3. Fill in some descriptive code in usb_conf.h:

    Usb_conf.h code
     #define EP_NUM 0x02 #define BTABLE_ADDRESS (0x00) /* EP0 */ /* rx/tx buffer base address */ #define ENDP0_RXADDR (0x40) #define ENDP0_TXADDR (0x80) /* EP1 */ /* tx buffer base address */ #define ENDP1_RXADDR (0xC0) /* IMR_MSK */ /* mask defining which events has to be handled */ /* by the device application software */ #define IMR_MSK (CNTR_CTRM | CNTR_RESETM) 


    Almost all of the above is not required by the library and is only needed to improve the readability of the code in our main module. The exception is IMR_MSK , a mask indicating which USB interrupts are used. We set it to the required minimum - interrupt Correct Transfer and Reset.
    Endpoint addresses are set in the address space of the so-called PMA, Packet Memory Area, taking into account the length of the packets. Since for both endpoints the maximum packet size is set to 64 bytes, we place them with the appropriate step, not forgetting the table of these same endpoint addresses, which is stored in the same place and also takes up space.
  4. Now we need to determine the callbacks that the library requires. These callbacks are combined into the DEVICE_PROP and USER_STANDARD_REQUESTS structures , while the library will look for their instances under the names Device_Property and User_Standard_Requests .
    We start with the callback called at the very beginning, when the USB controller is initialized, whose function is to reset the USB-clock, call the library initialization functions and activate the USB pull-up, which causes the host to see us on the bus.

    void Device_init ()
     void Device_init() { DEVICE_INFO *pInfo = &Device_Info; pInfo->Current_Configuration = 0; _SetCNTR(CNTR_FRES); //Reset USB block _SetCNTR(0); //Deassert reset signal _SetISTR(0); //Clear pending interrupts USB_SIL_Init(); GPIO_ResetBits(GPIOC, GPIO_Pin_13); //Enable pull-up } 


    The following callback will be called when the host requests a reset of our device:

    void Device_Reset ()
     void Device_Reset() { //Set device as not configured pInformation->Current_Configuration = 0; pInformation->Current_Interface = 0; //the default Interface /* Current Feature initialization */ pInformation->Current_Feature = USB_ConfigDescriptor[7]; SetBTABLE(BTABLE_ADDRESS); /* Initialize Endpoint 0 */ SetEPType(ENDP0, EP_CONTROL); SetEPTxStatus(ENDP0, EP_TX_STALL); SetEPRxAddr(ENDP0, ENDP0_RXADDR); SetEPTxAddr(ENDP0, ENDP0_TXADDR); Clear_Status_Out(ENDP0); SetEPRxCount(ENDP0, Device_Property.MaxPacketSize); SetEPRxValid(ENDP0); SetEPType(ENDP1, EP_BULK); SetEPRxAddr(ENDP1, ENDP1_RXADDR); SetEPRxCount(ENDP1, 0x40); SetEPRxStatus(ENDP1, EP_RX_VALID); SetEPTxStatus(ENDP1, EP_TX_DIS); /* Set this device to response on default address */ SetDeviceAddress(0); } 


    , 0, .
    ,

     #define Device_Status_In NOP_Process #define Device_Status_Out NOP_Process 

    , , , , , .
    , - Control Endpoint 0, :

     RESULT Device_Data_Setup(uint8_t RequestNo) { return USB_UNSUPPORT; } RESULT Device_NoData_Setup(uint8_t RequestNo) { return USB_UNSUPPORT; } 

    , , , , . - , .
    , , . , USB_UNSUPPORT .

     RESULT Device_Get_Interface_Setting(uint8_t Interface, uint8_t AlternateSetting) { if (AlternateSetting > 0) { return USB_UNSUPPORT; } else if (Interface > 0) { return USB_UNSUPPORT; } return USB_SUCCESS; } 

    STM , :

     uint8_t *Device_GetDeviceDescriptor(uint16_t Length) { return Standard_GetDescriptorData(Length, &Device_Descriptor); } uint8_t *Device_GetConfigDescriptor(uint16_t Length) { return Standard_GetDescriptorData(Length, &Config_Descriptor); } uint8_t *Device_GetStringDescriptor(uint16_t Length) { uint8_t wValue0 = pInformation->USBwValue0; if (wValue0 > 4) { return NULL; } else { return Standard_GetDescriptorData(Length, &String_Descriptor[wValue0]); } } 


    , :

     DEVICE_PROP Device_Property = { Device_init, Device_Reset, Device_Status_In, Device_Status_Out, Device_Data_Setup, Device_NoData_Setup, Device_Get_Interface_Setting, Device_GetDeviceDescriptor, Device_GetConfigDescriptor, Device_GetStringDescriptor, 0, 0x40 /*MAX PACKET SIZE*/ }; 

  5. , User_Standard_Requests .
    — .. HID - , , NOP_Process . , User_SetDeviceAddress – , .

    User_Standard_Requests
     #define Device_GetConfiguration NOP_Process #define Device_SetConfiguration NOP_Process #define Device_GetInterface NOP_Process #define Device_SetInterface NOP_Process #define Device_GetStatus NOP_Process #define Device_ClearFeature NOP_Process #define Device_SetEndPointFeature NOP_Process #define Device_SetDeviceFeature NOP_Process #define Device_SetDeviceAddress NOP_Process USER_STANDARD_REQUESTS User_Standard_Requests = { Device_GetConfiguration, Device_SetConfiguration, Device_GetInterface, Device_SetInterface, Device_GetStatus, Device_ClearFeature, Device_SetEndPointFeature, Device_SetDeviceFeature, Device_SetDeviceAddress }; 


  6. , :
     __IO uint16_t wIstr; DEVICE Device_Table = { EP_NUM, 1 }; 

    , ( ) .
  7. – , :

    void USB_Interrupts_Config(void) void Set_USBClock()
     void USB_Interrupts_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = USB_HP_CAN1_TX_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } void Set_USBClock() { /* Select USBCLK source */ RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); /* Enable the USB clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE); } 


    .
    Low-Priority ( , ), .
    Isochronous Bulk , ( ). , .
    USB 1.5 (48 ) USB.
  8. , , .

     void USB_HP_CAN1_TX_IRQHandler(void) { CTR_HP(); } void USB_LP_CAN1_RX0_IRQHandler(void) { wIstr = _GetISTR(); #if (IMR_MSK & ISTR_CTR) if (wIstr & ISTR_CTR & wInterrupt_Mask) { CTR_LP(); } #endif #if (IMR_MSK & ISTR_RESET) if (wIstr & ISTR_RESET & wInterrupt_Mask) { _SetISTR((uint16_t)CLR_RESET); Device_Property.Reset(); } #endif } 


    , – USB , USB LCD, . , Bulk Endpoint 1 , .
  9. – , NOT_ADDRESSED TRANSFER_IN_PROGRESS . , 8 , X Y ( 16- ) , , . ( ) , , . , , , - , . , . , .
    , – , PMAToUserBufferCopy , . , - ?
    PMAToUserBufferCopy , PMAToLCDBufferCopy , :

     void PMAToLCDBufferCopy(uint16_t wPMABufAddr, uint16_t offset ,uint16_t wNBytes) { uint32_t n = (wNBytes + 1) >> 1; uint32_t i; uint32_t *pdwVal; pdwVal = (uint32_t *)(wPMABufAddr * 2 + PMAAddr+offset); for (i = n; i != 0; i--) LCD_WRITE_RAM(*pdwVal++); } 

    :

     void EP1_OUT_Callback(void) { uint16_t dataLen = GetEPRxCount(EP1_OUT & 0x7F); uint16_t offset=0; if(GraphicsState==NOT_ADDRESSED) { if(dataLen<=8) { SetEPRxStatus(ENDP1, EP_RX_VALID); return; } PMAToUserBufferCopy(buffer, GetEPRxAddr(EP1_OUT & 0x7F), 8); uint16_t horz = *((uint16_t*)(buffer)); uint16_t vert = *(uint16_t*)(buffer+2); dataTotal = *(uint32_t*)(buffer+4); LCD_WRITE_REGISTER(CTR_HORZ_ADDRESS,vert); //  LCD_WRITE_REGISTER(CTR_VERT_ADDRESS,horz); offset=16; dataTransfered=0x00; GraphicsState=TRANSFER_IN_PROGRESS; dataLen-=8; } LCD_BEGIN_RAM_WRITE; PMAToLCDBufferCopy(GetEPRxAddr(EP1_OUT & 0x7F), offset, dataLen); dataTransfered+=(dataLen)>>1; if(dataTransfered>=dataTotal) GraphicsState=NOT_ADDRESSED; SetEPRxStatus(ENDP1, EP_RX_VALID); } 


    , , , ( ) NOP_Process :

     void (*pEpInt_IN[7])(void) = { NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, }; void (*pEpInt_OUT[7])(void) = { EP1_OUT_Callback, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, NOP_Process, }; 



Check

– .
- windows ( - ), LibUSB.
C#, LibUSB.Net,
– , , , , .
, . , InfWizard. inf-, libusb.
Run the bundled Test_bulk, select our device, click Open, and enter more than 8 characters in the line. After we press "Write", they should come to our callback and be interpreted as a header and graphic data, and then appear on the display in the form of several colored dots.
Of course, this is not very impressive, so we open the source of this Test_Bulk itself, go to the Write button handler and, instead of sending data from the string, do the download from the binary file.

 var bytesToWrite = File.ReadAllBytes("D:\\myfile.raw"); 

, . 320240, :

, , BMP , , . 16- RGB565.
040 () - -, – , . USB- – (0000, 0000) (320240) — 002C0100
, , Test_bulk, , Write

. , , . , , – , - OpenWRT.
!

Source: https://habr.com/ru/post/163689/


All Articles