How to write a diploma using three open source projects

    It is no secret that in our project students are used to train. More precisely, students on the basis of the project master the practical aspects of system programming: they write diplomas, term papers, engage in research activities, and so on. One diploma successfully defended last summer will be discussed in this article.

    The author is Aleksandra Butrova AleksandraButrova , topic “Development of a graphics subsystem for embedded operating systems”. When writing the diploma, three open source projects were used: Embox , Nuklear and stb. The latter was used only for downloading pictures, but Nuklear was, in fact, the culprit of the celebration. We can say that the work was reduced to the integration of Nuklear and Embox. The first provided a light graphics library, and Embox was responsible for embedded systems.

    Prior to this work, graphical applications for Embox could only be developed on the Qt framework, which is certainly wonderful because it:

    • Cross platform
    • Contains a lot of all useful
    • Open and well debugged

    But at the same time, Qt is not always suitable for embedded systems, because:

    • Very big
    • Resourceful
    • Written in C ++, not C

    In addition, there are nuances with a license. In short, we in the project have long thought about porting something lightweight and looked intently towards the Nuklear project already mentioned by Khrabrov Dmitry DeXPeriX. We liked the use of pure C and a small amount of code (in fact, one header file). Plus a great license: This software is dual-licensed to the public domain and under the following license: you are granted a perpetual, irrevocable license to copy, modify, publish and distribute this file as you see fit. All in all, Nuklear is great for integrating with other projects.




    Of course, since this is a diploma, the task was not just to use the library that the scientist liked. Six libraries were examined and two approaches to the construction of graphic primitives were revealed: retained and immediate . In addition to the libraries themselves, general models for constructing graphic subsystems were also considered, starting, of course, with the legendary X11 . But since the main emphasis in the work was made on limited resources, the unique directFB analogue present in Embox was recognized as the best .

    Returning to Nuklear, which by a strange coincidence neverthelesswas selected as a graphics library, it should be noted that it has several rendering options (sets of functions for rendering primitives) for different platforms, examples of use for X11, sdl and OpenGL are given. In order to run it on another platform, you need to implement your own rendering. For Embox, of course, there was no rendering. The first practical task was to modify the existing example from the Nuklear repository so that it at least somehow assembled and started on Embox. For this, the simplest example was chosen - canvas, which, in fact, demonstrates the output of graphic primitives.

    I will give the main function code for comparison
    from the original example
    intmain(int argc, char *argv[]){
        /* Platform */static GLFWwindow *win;
        int width = 0, height = 0;
        /* GUI */structdevicedevice;structnk_font_atlasatlas;structnk_contextctx;/* GLFW */
        glfwSetErrorCallback(error_callback);
        if (!glfwInit()) {
            fprintf(stdout, "[GFLW] failed to init!\n");
            exit(1);
        }
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        win = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Demo", NULL, NULL);
        glfwMakeContextCurrent(win);
        glfwSetWindowUserPointer(win, &ctx);
        glfwSetCharCallback(win, text_input);
        glfwSetScrollCallback(win, scroll_input);
        glfwGetWindowSize(win, &width, &height);
        /* OpenGL */
        glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
        glewExperimental = 1;
        if (glewInit() != GLEW_OK) {
            fprintf(stderr, "Failed to setup GLEW\n");
            exit(1);
        }
        /* GUI */
        {device_init(&device);
        {constvoid *image; int w, h;
        structnk_font *font;
        nk_font_atlas_init_default(&atlas);
        nk_font_atlas_begin(&atlas);
        font = nk_font_atlas_add_default(&atlas, 13, 0);
        image = nk_font_atlas_bake(&atlas, &w, &h, NK_FONT_ATLAS_RGBA32);
        device_upload_atlas(&device, image, w, h);
        nk_font_atlas_end(&atlas, nk_handle_id((int)device.font_tex), &device.null);
        nk_init_default(&ctx, &font->handle);
        glEnable(GL_TEXTURE_2D);
        while (!glfwWindowShouldClose(win))
        {
            /* input */
            pump_input(&ctx, win);
            /* draw */
            {structnk_canvascanvas;
            canvas_begin(&ctx, &canvas, 0, 0, 0, width, height, nk_rgb(250,250,250));
            {
                nk_fill_rect(canvas.painter, nk_rect(15,15,210,210), 5, nk_rgb(247, 230, 154));
                nk_fill_rect(canvas.painter, nk_rect(20,20,200,200), 5, nk_rgb(188, 174, 118));
                nk_draw_text(canvas.painter, nk_rect(30, 30, 150, 20), "Text to draw", 12, &font->handle, nk_rgb(188,174,118), nk_rgb(0,0,0));
                nk_fill_rect(canvas.painter, nk_rect(250,20,100,100), 0, nk_rgb(0,0,255));
                nk_fill_circle(canvas.painter, nk_rect(20,250,100,100), nk_rgb(255,0,0));
                nk_fill_triangle(canvas.painter, 250, 250, 350, 250, 300, 350, nk_rgb(0,255,0));
                nk_fill_arc(canvas.painter, 300, 180, 50, 0, 3.141592654f * 3.0f / 4.0f, nk_rgb(255,255,0));
                {float points[12];
                points[0] = 200; points[1] = 250;
                points[2] = 250; points[3] = 350;
                points[4] = 225; points[5] = 350;
                points[6] = 200; points[7] = 300;
                points[8] = 175; points[9] = 350;
                points[10] = 150; points[11] = 350;
                nk_fill_polygon(canvas.painter, points, 6, nk_rgb(0,0,0));}
                nk_stroke_line(canvas.painter, 15, 10, 200, 10, 2.0f, nk_rgb(189,45,75));
                nk_stroke_rect(canvas.painter, nk_rect(370, 20, 100, 100), 10, 3, nk_rgb(0,0,255));
                nk_stroke_curve(canvas.painter, 380, 200, 405, 270, 455, 120, 480, 200, 2, nk_rgb(0,150,220));
                nk_stroke_circle(canvas.painter, nk_rect(20, 370, 100, 100), 5, nk_rgb(0,255,120));
                nk_stroke_triangle(canvas.painter, 370, 250, 470, 250, 420, 350, 6, nk_rgb(255,0,143));
            }
            canvas_end(&ctx, &canvas);}
            /* Draw */
            glfwGetWindowSize(win, &width, &height);
            glViewport(0, 0, width, height);
            glClear(GL_COLOR_BUFFER_BIT);
            glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
            device_draw(&device, &ctx, width, height, NK_ANTI_ALIASING_ON);
            glfwSwapBuffers(win);
        }}}
        nk_font_atlas_clear(&atlas);
        nk_free(&ctx);
        device_shutdown(&device);
        glfwTerminate();
        return0;
    }
    


    and modified example
    intmain(int argc, char *argv[]){
        longint screensize = 0;
        uint8_t *fbp = 0;
        structfb_info *fb_info;structnk_colorrgb_white = { .a = 0xff, .r = 0xff, .g = 0xff, .b = 0xff};
        fb_info = fb_lookup(0);
        printf("%dx%d, %dbpp\n", fb_info->var.xres, fb_info->var.yres,
                fb_info->var.bits_per_pixel);
        /* Figure out the size of the screen in bytes */
        screensize = fb_info->var.xres * fb_info->var.yres
                * fb_info->var.bits_per_pixel / 8;
        /* Map the device to memory */
        fbp = (uint8_t *) mmap_device_memory((void *) fb_info->screen_base,
                screensize, PROT_READ | PROT_WRITE, MAP_SHARED,
                (uint64_t) ((uintptr_t) fb_info->screen_base));
        if ((int) fbp == -1) {
            perror("Error: failed to map framebuffer device to memory");
            exit(4);
        }
        printf("The framebuffer device was mapped to memory successfully.\n");
        structfb_fillrectrect;
        rect.dx = 0;
        rect.dy = 0;
        rect.width = fb_info->var.xres;
        rect.height = fb_info->var.yres;
        rect.rop = ROP_COPY;
        rect.color = rgba_to_device_color(fb_info, &rgb_white);
        fb_fillrect(fb_info, &rect);
        /* GUI */staticstructnk_contextctx;staticstructnk_canvascanvas;uint32_t width = 0, height = 0;
        staticstructnk_user_fontfont;
        font.userdata.ptr = (void *) font_vga_8x8.data;
        font.height = font_vga_8x8.height;
        font.width = your_text_width_calculation;
        nk_init_default(&ctx, &font);
        width = fb_info->var.xres;
        height = fb_info->var.yres;
        /* Draw */while (1) {
            /* what to draw */
            canvas_begin(&ctx, &canvas, 0, 0, 0, width, height,
                    nk_rgb(100, 100, 100));
            {
                canvas.painter->use_clipping = NK_CLIPPING_OFF;
                nk_fill_rect(canvas.painter, nk_rect(15, 15, 140, 140), 5,
                        nk_rgb(247, 230, 154));
                nk_fill_rect(canvas.painter, nk_rect(20, 20, 135, 135), 5,
                        nk_rgb(188, 174, 118));
                nk_draw_text(canvas.painter, nk_rect(30, 30, 100, 20),
                        "Text to draw", 12, &font, nk_rgb(188, 174, 118),
                        nk_rgb(0, 0, 0));
                nk_fill_rect(canvas.painter, nk_rect(160, 20, 70, 70), 0,
                        nk_rgb(0, 0, 255));
                nk_fill_circle(canvas.painter, nk_rect(20, 160, 60, 60),
                        nk_rgb(255, 0, 0));
                nk_fill_triangle(canvas.painter, 160, 160, 230, 160, 195, 220,
                        nk_rgb(0, 255, 0));
                nk_fill_arc(canvas.painter, 195, 120, 30, 0,
                        3.141592654f * 3.0f / 4.0f, nk_rgb(255, 255, 0));
                nk_stroke_line(canvas.painter, 15, 10, 100, 10, 2.0f,
                        nk_rgb(189, 45, 75));
                nk_stroke_rect(canvas.painter, nk_rect(235, 20, 70, 70), 10, 3,
                        nk_rgb(0, 0, 255));
                nk_stroke_curve(canvas.painter, 235, 130, 252, 170, 288, 80, 305,
                        130, 1, nk_rgb(0, 150, 220));
                nk_stroke_triangle(canvas.painter, 235, 160, 305, 160, 270, 220, 10,
                        nk_rgb(255, 0, 143));
                nk_stroke_circle(canvas.painter, nk_rect(90, 160, 60, 60), 2,
                        nk_rgb(0, 255, 120));
                {
                    structnk_imageim;int w, h, format;
                    structnk_rectr;
                    im.handle.ptr = stbi_load("SPBGU_logo.png", &w, &h, &format, 0);
                    r = nk_rect(320, 10, w, h);
                    im.w = w;
                    im.h = h;
                    im.region[0] = (unsignedshort) 0;
                    im.region[1] = (unsignedshort) 0;
                    im.region[2] = (unsignedshort) r.w;
                    im.region[3] = (unsignedshort) r.h;
                    printf("load %p, %d, %d, %d\n", im.handle.ptr, w, h, format);
                    nk_draw_image(canvas.painter, r, &im, nk_rgb(100, 0, 0));
                    stbi_image_free(im.handle.ptr);
                }
            }
            canvas_end(&ctx, &canvas);
            /* Draw each element */
            draw(fb_info, &ctx, width, height);
        }
        nk_free(&ctx);
        printf("\nEnd of program.\nIf you see it then something goes wrong.\n");
        return0;
    }
    


    The code for working with the library has not changed much. The changes concerned the loading of their fonts, various openGL functionality and other specific platform parts.
    The most important platform-specific part is, of course, rendering: the device_draw and draw functions, respectively. Actually, this is a challenge to the very rendering. Since Nuklear refers to imediate by the type of rendering, there is a loop in which the scene is constantly rendered by calling this function. The rendering code itself is as follows:

    for openGL
    staticvoiddevice_draw(struct device *dev, struct nk_context *ctx, int width, int height,
        enum nk_anti_aliasing AA){
        GLfloat ortho[4][4] = {
            {2.0f, 0.0f, 0.0f, 0.0f},
            {0.0f,-2.0f, 0.0f, 0.0f},
            {0.0f, 0.0f,-1.0f, 0.0f},
            {-1.0f,1.0f, 0.0f, 1.0f},
        };
        ortho[0][0] /= (GLfloat)width;
        ortho[1][1] /= (GLfloat)height;
        /* setup global state */
        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glDisable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_SCISSOR_TEST);
        glActiveTexture(GL_TEXTURE0);
        /* setup program */
        glUseProgram(dev->prog);
        glUniform1i(dev->uniform_tex, 0);
        glUniformMatrix4fv(dev->uniform_proj, 1, GL_FALSE, &ortho[0][0]);
        {
            /* convert from command queue into draw list and draw to screen */conststructnk_draw_command *cmd;void *vertices, *elements;
            const nk_draw_index *offset = NULL;
            /* allocate vertex and element buffer */
            glBindVertexArray(dev->vao);
            glBindBuffer(GL_ARRAY_BUFFER, dev->vbo);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, dev->ebo);
            glBufferData(GL_ARRAY_BUFFER, MAX_VERTEX_MEMORY, NULL, GL_STREAM_DRAW);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, MAX_ELEMENT_MEMORY, NULL, GL_STREAM_DRAW);
            /* load draw vertices & elements directly into vertex + element buffer */
            vertices = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
            elements = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
            {
                /* fill convert configuration */structnk_convert_configconfig;staticconststructnk_draw_vertex_layout_elementvertex_layout[] = {
                    {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, position)},
                    {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_glfw_vertex, uv)},
                    {NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct nk_glfw_vertex, col)},
                    {NK_VERTEX_LAYOUT_END}
                };
                NK_MEMSET(&config, 0, sizeof(config));
                config.vertex_layout = vertex_layout;
                config.vertex_size = sizeof(struct nk_glfw_vertex);
                config.vertex_alignment = NK_ALIGNOF(struct nk_glfw_vertex);
                config.null = dev->null;
                config.circle_segment_count = 22;
                config.curve_segment_count = 22;
                config.arc_segment_count = 22;
                config.global_alpha = 1.0f;
                config.shape_AA = AA;
                config.line_AA = AA;
                /* setup buffers to load vertices and elements */
                {structnk_buffervbuf, ebuf;
                nk_buffer_init_fixed(&vbuf, vertices, MAX_VERTEX_MEMORY);
                nk_buffer_init_fixed(&ebuf, elements, MAX_ELEMENT_MEMORY);
                nk_convert(ctx, &dev->cmds, &vbuf, &ebuf, &config);}
            }
            glUnmapBuffer(GL_ARRAY_BUFFER);
            glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
          /* iterate over and execute each draw command */
            nk_draw_foreach(cmd, ctx, &dev->cmds)
            {
                if (!cmd->elem_count) continue;
                glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
                glScissor(
                    (GLint)(cmd->clip_rect.x),
                    (GLint)((height - (GLint)(cmd->clip_rect.y + cmd->clip_rect.h))),
                    (GLint)(cmd->clip_rect.w),
                    (GLint)(cmd->clip_rect.h));
                glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
                offset += cmd->elem_count;
            }
            nk_clear(ctx);
        }
        /* default OpenGL state */
        glUseProgram(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
        glDisable(GL_BLEND);
        glDisable(GL_SCISSOR_TEST);
    }
    


    As you can see, there is another cycle

    /* iterate over and execute each draw command */
    nk_draw_foreach(cmd, ctx, &dev->cmds)

    As you might guess, this cycle goes through all the commands for rendering the scene, and draws them using the platform tools.

    Therefore, the similar code for the draw function for Embox also contains this loop.
    staticinlinevoiddraw(struct fb_info *fb, struct nk_context *ctx, int width, int height){
        assert(fb);
        assert(ctx);
        conststructnk_command *cmd;/* iterate over and execute each draw command */
        nk_foreach(cmd, ctx)
        {
            switch (cmd->type) {
            case NK_COMMAND_NOP:
                break;
            case NK_COMMAND_LINE: {
                conststructnk_command_line *c =
                        (conststructnk_command_line*) cmd;
                embox_stroke_line( fb, c->begin.x, c->begin.y, c->end.x, c->end.y,
                        &c->color, c->line_thickness);
            }
                break;
            case NK_COMMAND_CURVE: {
                conststructnk_command_curve *c =
                        (conststructnk_command_curve*) cmd;int x[4];
                int y[4];
                x[0] = c->begin.x;
                x[1] = c->ctrl[0].x;
                x[2] = c->ctrl[1].x;
                x[3] = c->end.x;
                y[0] = c->begin.y;
                y[1] = c->ctrl[0].y;
                y[2] = c->ctrl[1].y;
                y[3] = c->end.y;
                embox_stroke_curve( fb, x, y, &c->color, c->line_thickness);
            }
                break;
            case NK_COMMAND_RECT: {
                conststructnk_command_rect *c =
                        (conststructnk_command_rect*) cmd;
                embox_stroke_rect( fb, c->x, c->y, c->w, c->h, &c->color,
                        (float) c->rounding, c->line_thickness);
            }
                break;
            case NK_COMMAND_RECT_FILLED: {
                conststructnk_command_rect_filled *c =
                        (conststructnk_command_rect_filled*) cmd;
                embox_fill_rect( fb, c->x, c->y, c->w, c->h, &c->color);
            }
                break;
            case NK_COMMAND_CIRCLE: {
                conststructnk_command_circle *c =
                        (conststructnk_command_circle*) cmd;
                embox_stroke_circle( fb, (float) c->x + (float) c->w / 2,
                        (float) c->y + (float) c->h / 2, (float) c->w / 2,
                        &c->color, c->line_thickness);
            }
                break;
            case NK_COMMAND_CIRCLE_FILLED: {
                conststructnk_command_circle_filled *c =
                        (conststructnk_command_circle_filled *) cmd;
                embox_fill_circle( fb, c->x + c->w / 2, c->y + c->h / 2, c->w / 2,
                        &c->color);
            }
                break;
            case NK_COMMAND_ARC: {
                conststructnk_command_arc *c = (conststructnk_command_arc*) cmd;
                embox_stroke_arc( fb, c->cx, c->cy, c->r, c->a[0], c->a[1], &c->color,
                        c->line_thickness);
            }
                break;
            case NK_COMMAND_ARC_FILLED: {
                conststructnk_command_arc_filled *c =
                        (conststructnk_command_arc_filled*) cmd;
                embox_fill_arc( fb, c->cx, c->cy, c->r, c->a[0], c->a[1], &c->color);
            }
                break;
            case NK_COMMAND_TRIANGLE: {
                conststructnk_command_triangle *c =
                        (conststructnk_command_triangle*) cmd;
                embox_stroke_triangle( fb, c->a.x, c->a.y, c->b.x, c->b.y, c->c.x,
                        c->c.y, &c->color, c->line_thickness);
            }
                break;
            case NK_COMMAND_TRIANGLE_FILLED: {
                conststructnk_command_triangle_filled *c =
                        (conststructnk_command_triangle_filled*) cmd;
                embox_fill_triangle( fb, c->a.x, c->a.y, c->b.x, c->b.y, c->c.x,
                        c->c.y, &c->color);
            }
                break;
            case NK_COMMAND_TEXT: {
                conststructnk_command_text *c =
                        (conststructnk_command_text*) cmd;
                embox_add_text( fb, ctx, c->x, c->y, &c->foreground, &c->background, c->string,
                        c->length);
            }
                break;
            case NK_COMMAND_IMAGE: {
                conststructnk_command_image *c =
                        (conststructnk_command_image*) cmd;int color = rgba_to_device_color( fb, &c->col);
                embox_add_image( fb, c->img, c->x, c->y, c->w, c->h, color);
            }
                break;
                /* unrealized primitives *//*
                 case NK_COMMAND_SCISSOR: {
                 const struct nk_command_scissor *s = (const struct nk_command_scissor*)cmd;
                 nk_draw_list_add_clip(&ctx->draw_list, nk_rect(s->x, s->y, s->w, s->h));
                 } break;
                 case NK_COMMAND_POLYGON: {
                 int i;
                 const struct nk_command_polygon*p = (const struct nk_command_polygon*)cmd;
                 for (i = 0; i < p->point_count; ++i) {
                 struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
                 nk_draw_list_path_line_to(&ctx->draw_list, pnt);
                 }
                 nk_draw_list_path_stroke(&ctx->draw_list, p->color, NK_STROKE_CLOSED, p->line_thickness);
                 } break;
                 case NK_COMMAND_POLYGON_FILLED: {
                 int i;
                 const struct nk_command_polygon_filled *p = (const struct nk_command_polygon_filled*)cmd;
                 for (i = 0; i < p->point_count; ++i) {
                 struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
                 nk_draw_list_path_line_to(&ctx->draw_list, pnt);
                 }
                 nk_draw_list_path_fill(&ctx->draw_list, p->color);
                 } break;
                 case NK_COMMAND_POLYLINE: {
                 int i;
                 const struct nk_command_polyline *p = (const struct nk_command_polyline*)cmd;
                 for (i = 0; i < p->point_count; ++i) {
                 struct nk_vec2 pnt = nk_vec2((float)p->points[i].x, (float)p->points[i].y);
                 nk_draw_list_path_line_to(&ctx->draw_list, pnt);
                 }
                 nk_draw_list_path_stroke(&ctx->draw_list, p->color, NK_STROKE_OPEN, p->line_thickness);
                 } break;
                 case NK_COMMAND_RECT_MULTI_COLOR: {
                 const struct nk_command_rect_multi_color *r = (const struct nk_command_rect_multi_color*)cmd;
                 nk_draw_list_fill_rect_multi_color(&ctx->draw_list, nk_rect(r->x, r->y, r->w, r->h),
                 r->left, r->top, r->right, r->bottom);
                 } break; */default:
                break;
            }
        }
        nk_clear(ctx);
    }
    


    The body of the loop looks very different, because you need to implement your own primitives that are already implemented in OpenGL.

    After the implementation of these primitives, the scene began to be drawn correctly.


    The coat of arms of St. Petersburg State University, of course, was added for the diploma and is not present in the original example from nuklear :)

    Of course, not everything was so simple. The first problem that prevented compiling the header file in its original form was that nuklear is oriented to the c89 standard, where there is no inline keyword, but there are no warnings for static functions that are not used. We use c99 by default, or rather gnu-extension, and we had to do PR in the original nuklear to support c99.

    Another problem that was not directly related to the diploma was that the pixel format can be different, and the image format in the memory may not coincide with the hardware format. Before that, we used a simple conversion from regular RGB with 8 bits for each channel to a particular hardware format in the driver of a particular graphic device, we had to add a layer for full conversion for different formats.

    In total, 3 platforms were tested:

    • QEMU / x86 (boch graphics)
    • QEMU / ARM (graphics pl110)
    • STM32F7Discovery (integrated graphics)

    With the latter platform, the maximum amount of trouble arose: both the problem of aligning structures, and the lack of memory for downloading images, and the portrait orientation of the screen (i.e. the image width is less than the height). But as a result, Sasha coped with all these tasks, and wanted to run an already “real” example. They also became a standard example from nuklear skinning.

    Appearance when running on QEMU / ARM



    Well, photos with the STM32F7 boardDiscovery


    canvas


    skinning

    I don’t want to retell the diploma, the full text can be downloaded here. In conclusion, I want to note that the author, while writing a diploma, participated in several live projects, gained practical experience in real work with distributed modern projects. And it is not so important that she personally knows the authors of one of these projects, so it’s still easier to enter the project. After all, as I said, this project is not the only one that was used when writing the diploma. And in my opinion, there should be as many diploma theses based on open existing projects as possible. After all, this is the most effective way to become a full-fledged specialist.

    Also popular now: