├── media ├── simple_ui.webp ├── bsd.svg ├── raw.svg ├── socket.svg ├── frame.svg ├── stack.svg └── mongoose.svg ├── LICENSE └── README.md /media/simple_ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpq/embedded-network-programming-guide/HEAD/media/simple_ui.webp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sergey Lyubka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /media/bsd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/raw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/socket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/frame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/stack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/mongoose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embedded network programming guide 2 | 3 | [](https://opensource.org/licenses/MIT) 4 | 5 | This guide is written for embedded developers who work on connected products. 6 | That includes microcontroller based systems that operate in bare metal or 7 | RTOS mode, as well as microprocessor based systems which run embedded Linux. 8 | 9 | ## Prerequisites 10 | 11 | All fundamental concepts are explained in details, so the guide should be 12 | understood by a reader with no prior networking knowledge. 13 | 14 | A reader is expected to be familiar with microcontroller C programming - for 15 | that matter, I recommend reading my [bare metal programming 16 | guide](https://github.com/cpq/bare-metal-programming-guide). I will be using 17 | Ethernet-enabled Nucleo-H743ZI board throughout this guide. Examples for other 18 | architectures are summarized in a table below - this list will expand with 19 | time. Regardless, for the best experience I recommend Nucleo-H743ZI to get the 20 | most from this guide: [buy it on 21 | Mouser](https://www.mouser.ie/ProductDetail/STMicroelectronics/NUCLEO-H743ZI2?qs=lYGu3FyN48cfUB5JhJTnlw%3D%3D). 22 | 23 | ## Network stack explained 24 | 25 | ### Network frame structure 26 | 27 | When any two devices communicate, they exchange discrete pieces of data called 28 | frames. Frames can be sent over the wire (like Ethernet) or over the air (like 29 | WiFi or Cellular). Frames differ in size, and typically range from couple of 30 | dozen bytes to a 1.5Kb. Each frame consists of a sequence of protocol headers 31 | followed by user data: 32 | 33 |  34 | 35 | The purpose of the headers is as follows: 36 | 37 | **MAC (Media Access Control) header** is only 3 fields: destination MAC 38 | address, source MAC addresses, and an upper level protocol. MAC addresses are 39 | 6-byte unique addresses of the network cards, e.g. `42:ef:15:c8:29:a1`. 40 | Protocol is usually 0x800, which means that the next header is IP. MAC header 41 | handles addressing in the local network (LAN). 42 | 43 | 44 | **IP (Internet Protocol) header** has many fields, but the most important are: 45 | destination IP address, source IP address, and upper level protocol. IP 46 | addresses are 4-bytes, e.g. `209.85.202.102`, and they identify a machine 47 | on the Internet, so their purpose is similar to phone numbers. The upper level 48 | protocol is usually 6 (TCP) or 17 (UDP). IP header handles global addressing. 49 | 50 | **TCP or UDP header** has many fields, but the most important are destination 51 | port and source ports. On one device, there can be many network applications, 52 | for example, many open tabs in a browser. Port number identifies 53 | an application. 54 | 55 | **Application protocol** depends on the target application. For example, there 56 | are servers on the Internet that can tell an accurate current time. If you want 57 | to send data to those servers, the application protocol must SNTP (Simple 58 | Network Time Protocol). If you want to talk to a web server, the protocol must 59 | be HTTP. There are other protocols, like DNS, MQTT, etc, each having their own 60 | headers, followed by the application data. 61 | 62 | Install [Wireshark](https://www.wireshark.org/) tool to observe network 63 | frames. It helps to identify issues quickly, and looks like this: 64 | 65 |  66 | 67 | The structure of a frame described above, makes it possible to accurately 68 | deliver a frame to the correct device and application over the Internet. When a 69 | frame arrives to a device, a software that handles that frame (a network 70 | stack), is organised in four layers. 71 | 72 | ### Network stack architecture 73 | 74 |  75 | 76 | Layer 1: **Driver layer**, only reads and writes frames from/to network hardware 77 | Layer 2: **TCP/IP stack**, parses protocol headers and handles IP and TCP/UDP 78 | Layer 3: **Network Library**, parses application protocols like DNS, MQTT, HTTP 79 | Layer 4: **Application** - Web dashboard, smart sensor, etc 80 | 81 | 82 | ### DNS request example 83 | 84 | Let's provide an example. In order to show your this guide on the Github, your 85 | browser first needs to find out the IP address of the Github's machine. For 86 | that, it should make a DNS (Domain Name System) request to one of the DNS 87 | servers. Here's how your browser and the network stack work for that case: 88 | 89 |  90 | 91 | **1.** Your browser (an application) asks from the lower layer (library), "what 92 | IP address `github.com` has?". The lower layer (layer 3, a library layer - in 93 | this case, it is a C library) provides an API function `gethostbyname()` 94 | that returns an IP address for a given host name. So everything said below, 95 | essentially describes how `gethostbyname()` works. 96 | 97 | **2.** The library layer gets the name `github.com` and creates a properly 98 | formatted, binary DNS request: `struct dns_request`. Then it calls an API 99 | function `sendto()` provided by the TCP/IP stack layer (layer 2), to send that 100 | request over UDP to the DNS server. The IP of the DNS server is known to the library 101 | from the workstation settings. The UDP port is also known - port 53, a standard 102 | port for DNS. 103 | 104 | **3.** The TCP/IP stack's `sendto()` function receives a chunk of data to send. 105 | it contains DNS request, but `sendto()` does not know that and does not care 106 | about that. All it knows is that this is the piece of user data that needs to 107 | be delievered over UDP to a certain IP address (IP address of a DNS server) on 108 | port 53. Hence TCP/IP stack prepends UDP, IP, and MAC headers 109 | to the user data to form a frame. Then it calls API function `send_frame()` 110 | provided by the driver layer, layer 1. 111 | 112 | **4.** A driver's `send_frame()` function transmits a frame over the wire or 113 | over the air, the frame travels to the destination DNS server. A chain of 114 | Internet routers pass that frame from one to another, until a frame finally hits 115 | DNS server's network card. 116 | 117 | **5.** A network card on the DNS server gets a frame and generates a hardware 118 | interrupt, invoking interrupt handler. It is part of a driver - layer 1. It 119 | calls a function `recv_frame()` that reads a frame from the card, and passes 120 | it up by calling `ethernet_input()` function provided by the TCP/IP stack 121 | 122 | **6.** TCP/IP stack parses the frame, and finds out that it is for the UDP port 123 | 53, which is a DNS port number. TCP/IP stack finds an application that listens 124 | on UDP port 53, which is a DNS server application, and wakes up its `recv()` 125 | call. So, DNS server application that is blocked on a `recv()` call, receives 126 | a chunk of data - which is a DNS request. A library routine parses that request 127 | by extracting a host name, and passes that parsed DNS request to the application. 128 | 129 | 130 | **7.** A DNS server application receives DNS request: "someone wants an 131 | IP address for `github.com`". Then the application layer looks at its 132 | configuration, figures out "Oh, it's me who is responsible for the github.com 133 | domain, and this is the IP address I should respond with". The application 134 | extracts an IP address from the configuration, and calls a library function 135 | "get this IP, wrap into a DNS response, and send back". And the response 136 | travels all the way back in the reverse order. 137 | 138 | ### BSD socket API 139 | 140 | The communication between layers are done via a function calls. So, each 141 | layer has its own API, which upper and lower levels can call. They are not 142 | standardized, so each implementation provides their own set of functions. 143 | However, on OSes like Windows/Mac/Linux/UNIX, a driver and TCP/IP layers are 144 | implemented in kernel, and TCP/IP layer provides a standard API to the 145 | userland which is called a "BSD socket API": 146 | 147 |  148 | 149 | This is done becase kernel code does not implement application level protocols 150 | like MQTT, HTTP, etc, - so it let's user application to implement them in 151 | userland. So, a library layer and an application layer reside in userland. 152 | Some library level routines are provided in C standard library, like 153 | DNS resolution function `gethostbyname()`, but that DNS library functions 154 | are probably the only ones that are provided by OS. For other protocols, 155 | many libraries exist that provide HTTP, MQTT, Websocket, SSH, API. Some 156 | applications don't use any external libraries: they use BSD socket API 157 | directly and implement library layer manually. Usually that is done when 158 | application decides to use some custom protocol. 159 | 160 | Embedded systems very often use TCP/IP stacks that provide the same BSD API as 161 | mainstream OSes do. For example, lwIP (LightWeight IP) TCP/IP stack, Keil's MDK 162 | TCP/IP stack, Zephyr RTOS TCP/IP stack - all provide BSD socket API. Thus let's 163 | review the most important BSD API stack functions: 164 | 165 | - `socket(protocol)` - creates a connection descriptor and assigns an integer ID for it, a "socket" 166 | - `bind(sock, addr)` - assigns a local IP:PORT for a listening socket 167 | - `accept(sock, addr)` - creates a new socket, assigns local IP:PORT 168 | and remote IP:PORT (incoming) 169 | - `connect(sock, addr)` - assigns local IP:PORT and remote IP:PORT for 170 | a socket (outgoing) 171 | - `send(sock, buf, len)` - sends data 172 | - `recv(sock, buf, len)` - receives data 173 | - `close(sock)` - closes a socket 174 | 175 | Some implementations do not implement BSD socket API, and there are perfectly 176 | good reasons for that. Examples for such implementation is lwIP raw API, 177 | and Mongoose Library. 178 | 179 | ### TCP echo server implemented with socket API 180 | 181 | Let me demonstrate the two approaches (using socket and non-socket API) on a simple 182 | TCP echo server example. TCP echo server is a simple application that 183 | listens on a TCP port, receives data from clients that connect to that port, 184 | and writes (echoes) that data back to the client. That means, this application 185 | does not use any application protocol on top of TCP, thus it does not need 186 | a library layer. Let's see how this application would look like written 187 | with a BSD socket API. First, a TCP listener should bind to a port, and 188 | for every connected client, spawn a new thread that would handle it. A thread 189 | function that sends/receives data, looks something like this: 190 | 191 | ```c 192 | void handle_new_connection(int sock) { 193 | char buf[20]; 194 | for (;;) { 195 | ssize_t len = recv(sock, buf, sizeof(buf), 0); // Receive data from remote 196 | if (len <= 0) break; // Error! Exit the loop 197 | send(sock, buf, len); // Success. Echo data back 198 | } 199 | close(sock); // Close socket, stop thread 200 | } 201 | ``` 202 | 203 | Note that `recv()` function blocks until it receives some data from the client. 204 | Then, `send()` also blocks until is sends requested data back to the client. 205 | That means that this code cannot run in a bare metal implementation, because 206 | `recv()` would block the whole firmware. For this to work, an RTOS is required. 207 | A TCP/IP stack should run in a separate RTOS task, and both `send()` and 208 | `recv()` functions are implemented using an RTOS queue API, providing a 209 | blocking way to pass data from one task to another. Overall, this is how an 210 | embedded receive path looks like with socket API: 211 | 212 |  213 | 214 | The `send()` part would work in the reverse direction. Note that this approach 215 | requires TCP/IP stack implement data buffering for each socket, because 216 | an application consumes received data not immediately, but after some time, 217 | when RTOS queue delivers data. Note that using non-blocking sockets and 218 | `select()/poll()` changes things that instead of many application tasks, 219 | there is only one application task, but the mechanism stays the same. 220 | 221 | Therefore this approach with socket API has 222 | the following major characteristics: 223 | 224 | 1. It uses queues for exchanging data between TCP/IP stack and 225 | application tasks, which consumes both RAM and time 226 | 2. TCP/IP stack buffers received and sent data for each socket. Note that 227 | the app/library layer may also buffer data - for example, buffering a full 228 | HTTP request before it can be processed. So the same data goes through 229 | two buffering "zones" - TCP/IP stack, and library/app 230 | 231 | That means, socket API implementation takes extra time for data to be processed, 232 | and takes extra RAM for double-buffering in the TCP/IP stack. 233 | 234 | ### TCP echo server with non-socket (callback) API 235 | 236 | Now let's see how the same approach works without BSD socket API. Several 237 | implementations, including lwIP and Mongoose Library, provide callback API to 238 | the TCP/IP stack. Here is how TCP echo server would look like written using 239 | Mongoose API: 240 | 241 | ```c 242 | // This callback function is called for various network events, MG_EV_* 243 | void event_handler(struct mg_connection *c, int ev, void *ev_data, void *fn_data) { 244 | if (ev == MG_EV_READ) { 245 | // MG_EV_READ means that new data got buffered in the c->recv buffer 246 | mg_send(c, c->recv.buf, c->recv.len); // Send back the data we received 247 | c->recv.len = 0; // Discard received data 248 | } 249 | } 250 | ``` 251 | 252 | In this case, all functions are non-blocking, that means that data exchange 253 | between TCP/IP stack and an app can be implemented via direct function calls. 254 | This is how receive path looks like: 255 | 256 |  257 | 258 | As you can see, in this case TCP/IP stack provides a callback API which 259 | a library or application layer can use to receive data directly. No need 260 | to send it over a queue. A library/app layer can buffer data, and that's 261 | the only place where buffering takes place. This approach wins for 262 | memory usage and performance. A firmware developer should use 263 | a proprietary callback API instead of BSD socket API. 264 | 265 | lwIP TCP/IP stack, for example, provides both socket and non-socket (raw) API, 266 | and raw API is more efficient in terms of RAM and performance. However 267 | developers rarely use raw API, because it is not trivial to understand and use 268 | compared to the socket API. The API of the Mongoose Library shown above 269 | is designed to be simple and easy to understand. API design can make things 270 | very easy or very difficult, so it is important to have a good API. 271 | 272 | ## Implementing layers 1,2,3 - making ping work 273 | 274 | ### Development environment and tools 275 | 276 | Now let's make our hands dirty and implement a working network stack on 277 | a microcontroller board. I will be using 278 | [Mongoose Library](https://github.com/cesanta/mongoose) for all examples 279 | further on, for the following reasons: 280 | 281 | - Mongoose is very easy to integrate: just by copying two files, 282 | [mongoose.c]() and [mongoose.h]() 283 | - Mongoose has a built-in drivers, TCP/IP stack, HTTP/MQTT/Websocket library, 284 | and TLS 1.3 all in one, so it does not need any other software to create 285 | a network-enabled application 286 | - Mongoose provides a simple, polished callback API designed specifically 287 | for embedded developers 288 | 289 | The diagram below shows Mongoose architecture. As you can see, Mongoose can 290 | use external TCP/IP stack and TLS libraries, as well as built-in ones. In the 291 | following example, we are going to use only a built-in functionality, so we 292 | won't need any other software. 293 | 294 |  295 | 296 | All source code in this guide is MIT licensed, however Mongoose 297 | is licensed under a dual GPLv2/commercial license. 298 | I will be using a Nucleo board from ST Microelectronics, and there are several choices for the 299 | development environment: 300 | - Use Cube IDE provided by ST: [install Cube](https://www.st.com/en/development-tools/stm32cubeide.html) 301 | - Use Keil from ARM: [install Keil](https://www.keil.com/) 302 | - Use make + GCC compiler, no IDE: follow [this guide](https://mongoose.ws/documentation/tutorials/tools/) 303 | 304 | Here, I am going to use Cube IDE. In the templates, however, both Keil and 305 | make examples are provided, too. So, in order to proceed, install Cube IDE 306 | on your workstation, and plug in Nucleo board to your workstation. 307 | 308 | ### Skeleton firmware 309 | 310 | Note: this and the following sections has a Youtube helper video recorded: 311 | https://www.youtube.com/watch?v=lKYM4b8TZts 312 | 313 | The first step would be to create a minimal, skeleton firmware that does 314 | nothing but logs messages to the serial console. Once we've done that, we'll 315 | add networking functionality on top of it. The table below summarises 316 | peripherals for various boards: 317 | 318 | 319 | | Board | UART, TX, RX | Ethernet | LED | 320 | | ---------------- | --------------- | ------------------------------------- | -------------- | 321 | | STM32H747I-DISCO | USART1, A9, A10 | A1, A2, A7, C1, C4, C5, G12, G11, G13 | I12, I13, I14 | 322 | | STM32H573I-DK | USART1, A9, A10 | A1, A2, A7, C1, C4, C5, G12, G11, G13 | I8, I9, F1 | 323 | | Nucleo-H743ZI | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B13, G11, G13 | B0, E1, B14 | 324 | | Nucleo-H723ZG | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B13, G11, G13 | B0, E1, B14 | 325 | | Nucleo-H563ZI | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B15, G11, G13 | B0, F4, G4 | 326 | | Nucleo-F746ZG | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B13, G11, G13 | B0, B7, B14 | 327 | | Nucleo-F756ZG | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B13, G11, G13 | B0, B7, B14 | 328 | | Nucleo-F429ZI | USART3, D8, D9 | A1, A2, A7, C1, C4, C5, B13, G11, G13 | B0, B7, B14 | 329 | 330 | **Step 1.** Start Cube IDE. Choose File / New / STM32 project 331 | **Step 2.** In the "part number" field, type the microcontroller name, 332 | for example "H743ZI". That should narrow down 333 | the MCU/MPU list selection in the bottom right corner to a single row. 334 | Click on the row at the bottom right, then click on the Next button 335 | **Step 3.** In the project name field, type any name, click Finish. 336 | Answer "yes" if a pop-up dialog appears 337 | **Step 4.** A configuration window appears. Click on Clock configuration tab. 338 | Find a field with a system clock value. Type the maximum value, hit enter, 339 | answer "yes" on auto-configuration question, wait until configured 340 | **Step 5.** Switch to the Pinout tab, Connectivity, then enable the UART controller 341 | and pins (see table above), choose "Asynchronous mode" 342 | **Step 6.** Click on Connectivity / ETH, Choose Mode / RMII, verify that the 343 | configured pins are like in the table above - if not, change pins 344 | **Step 7.** Lookup the LED GPIO from the peripherals table, and configure it 345 | for output. Click on the corresponding pin, select "GPIO output" 346 | **Step 8.** Click Ctrl+S to save the configuration. This generates the code 347 | and opens main.c file 348 | **Step 9.** Navigate to the `main()` function and add some logging to the 349 | `while` loop. Make sure to insert your code between the "USER CODE" comments, 350 | because CubeIDE will preserve it during code regeneration: 351 | ```c 352 | /* USER CODE BEGIN WHILE */ 353 | while (1) 354 | { 355 | printf("Tick: %lu\r\n", HAL_GetTick()); 356 | HAL_Delay(500); 357 | ``` 358 | **Step 10.** Redirect `printf()` to the UART. Note the UART global variable 359 | generated by Cube at the beginning of `main.c` - typically it is 360 | `UART_HandleTypeDef huart3;`. Copy it, open `syscalls.c`, find function 361 | `_write()` and modify it the following way : 362 | ```c 363 | #include "main.h" 364 | 365 | __attribute__((weak)) int _write(int file, char *ptr, int len) { 366 | if (file == 1 || file == 2) { 367 | extern UART_HandleTypeDef huart3; 368 | HAL_UART_Transmit(&huart3, (unsigned char *) ptr, len, 999); 369 | } 370 | return len; 371 | } 372 | ``` 373 | **Step 11.** Click on "Run" button to flash this firmware to the board. 374 | **Step 12.** Attach a serial monitor tool (e.g. putty on Windows, or 375 | `cu -l COMPORT -s 115200` on Mac/Linux) and observe UART logs: 376 | ``` 377 | Tick: 90358 378 | Tick: 90860 379 | ... 380 | ``` 381 | Our skeleton firmware is ready! 382 | 383 | ### Integrate Mongoose 384 | 385 | Now it's time to implement a functional TCP/IP stack. We'll use Mongoose 386 | Library for that. To integrate it, we need to copy two files into our source tree. 387 | 388 | **Step 1**. Open https://github.com/cesanta/mongoose in your browser, click on "mongoose.h". Click on "Raw" button, and copy file contents into clipboard. 389 | In the CubeIDE, right click on Core/Inc, choose New/File in the menu, type 390 | "mongoose.h", paste the file content and save. 391 | **Step 2**. Repeat for "mongoose.c". On Github, copy `mongoose.c` contents 392 | to the clipboard. In the CubeIDE, right click on Core/Src, choose New/File 393 | in the menu, type "mongoose.c", paste the file content and save. 394 | **Step 3**. Right click on Core/Inc, choose New/File in the menu, type 395 | "mongoose_custom.h", and paste the following contents: 396 | ```c 397 | #pragma once 398 | 399 | // See https://mongoose.ws/documentation/#build-options 400 | #define MG_ARCH MG_ARCH_NEWLIB 401 | 402 | #define MG_ENABLE_TCPIP 1 // Enables built-in TCP/IP stack 403 | #define MG_ENABLE_CUSTOM_MILLIS 1 // We must implement mg_millis() 404 | #define MG_ENABLE_DRIVER_STM32H 1 // On STM32Fxx series, use MG_ENABLE_DRIVER_STM32F 405 | ``` 406 | **Step 4**. Implement Layer 1 (driver), 2 (TCP/IP stack) and 3 (library) in 407 | our code. Open `main.c`. Add `#include "mongoose.h"` at the top: 408 | ```c 409 | /* USER CODE BEGIN Includes */ 410 | #include "mongoose.h" 411 | /* USER CODE END Includes */ 412 | ``` 413 | **Step 5**. Before `main()`, define function `mg_millis()` that returns 414 | an uptime in milliseconds. It will be used by Mongoose Library for the time 415 | keeping: 416 | ```c 417 | /* USER CODE BEGIN 0 */ 418 | uint64_t mg_millis(void) { 419 | return HAL_GetTick(); 420 | } 421 | /* USER CODE END 0 */ 422 | ``` 423 | **Step 6**. Navigate to `main()` function and change the code around `while` 424 | loop this way: 425 | ```c 426 | /* USER CODE BEGIN WHILE */ 427 | struct mg_mgr mgr; 428 | mg_mgr_init(&mgr); 429 | mg_log_set(MG_LL_DEBUG); 430 | 431 | // On STM32Fxx, use _stm32f suffix instead of _stm32h 432 | struct mg_tcpip_driver_stm32h_data driver_data = {.mdc_cr = 4}; 433 | struct mg_tcpip_if mif = {.mac = {2, 3, 4, 5, 6, 7}, 434 | // Uncomment below for static configuration: 435 | // .ip = mg_htonl(MG_U32(192, 168, 0, 223)), 436 | // .mask = mg_htonl(MG_U32(255, 255, 255, 0)), 437 | // .gw = mg_htonl(MG_U32(192, 168, 0, 1)), 438 | .driver = &mg_tcpip_driver_stm32h, 439 | .driver_data = &driver_data}; 440 | NVIC_EnableIRQ(ETH_IRQn); 441 | mg_tcpip_init(&mgr, &mif); 442 | 443 | while (1) { 444 | mg_mgr_poll(&mgr, 0); 445 | /* USER CODE END WHILE */ 446 | ``` 447 | 448 | **Step 7**. Connect your board to the Ethernet. Flash firmware. In the serial 449 | log, you should see something like this: 450 | ``` 451 | bb8 3 mongoose.c:14914:mg_tcpip_driv Link is 100M full-duplex 452 | bbd 1 mongoose.c:4676:onstatechange Link up 453 | bc2 3 mongoose.c:4776:tx_dhcp_discov DHCP discover sent. Our MAC: 02:03:04:05:06:07 454 | c0e 3 mongoose.c:4755:tx_dhcp_reques DHCP req sent 455 | c13 2 mongoose.c:4882:rx_dhcp_client Lease: 86400 sec (86403) 456 | c19 2 mongoose.c:4671:onstatechange READY, IP: 192.168.2.76 457 | c1e 2 mongoose.c:4672:onstatechange GW: 192.168.2.1 458 | c24 2 mongoose.c:4673:onstatechange MAC: 02:03:04:05:06:07 459 | ``` 460 | If you don't, and see DHCP requests message like this: 461 | ``` 462 | 130b0 3 mongoose.c:4776:tx_dhcp_discov DHCP discover sent. Our MAC: 02:03:04:05:06:07 463 | 13498 3 mongoose.c:4776:tx_dhcp_discov DHCP discover sent. Our MAC: 02:03:04:05:06:07 464 | ... 465 | ``` 466 | The most common cause for this is you have your Ethernet pins wrong. Click 467 | on the `.ioc` file, go to the Ethernet configuration, and double-check the 468 | Ethernet pins against the table above. 469 | 470 | **Step 8**. Open terminal/command prompt, and run a `ping` command against 471 | the IP address of your board: 472 | ```sh 473 | $ ping 192.168.2.76 474 | PING 192.168.2.76 (192.168.2.76): 56 data bytes 475 | 64 bytes from 192.168.2.76: icmp_seq=0 ttl=64 time=9.515 ms 476 | 64 bytes from 192.168.2.76: icmp_seq=1 ttl=64 time=1.012 ms 477 | ``` 478 | 479 | Now, we have a functional network stack running on our board. Layers 1,2,3 480 | are implemented. It's time to create an application - a simple web server, 481 | hence implement layer 4. 482 | 483 | ## Implementing layer 4 - a simple web server 484 | 485 | Let's add a very simple web server that responds "ok" to any HTTP request. 486 | 487 | **Step 1**. After the `mg_tcpip_init()` call, add this line that creates HTTP listener 488 | with `fn` event handler function: 489 | ```c 490 | mg_http_listen(&mgr, "http://0.0.0.0:80", fn, NULL); 491 | ``` 492 | **Step 2**. Before the `mg_millis()` function, add the `fn` event handler function: 493 | ```c 494 | static void fn(struct mg_connection *c, int ev, void *ev_data) { 495 | if (ev == MG_EV_HTTP_MSG) { 496 | struct mg_http_message *hm = ev_data; // Parsed HTTP request 497 | mg_http_reply(c, 200, "", "ok\r\n"); 498 | } 499 | } 500 | ``` 501 | That's it! Flash the firmware. Open your browser, type board's IP address and 502 | see the "ok" message. 503 | 504 | Note that the [mg_http_reply()](https://mongoose.ws/documentation/#mg_http_reply) 505 | function is very versatile: it cat create formatted output, like printf 506 | on steroids. See [mg_snprintf()](https://mongoose.ws/documentation/#mg_snprintf-mg_vsnprintf) 507 | for the supported format specifiers: most of them are standard printf, but 508 | there are two non-standard: `%m` and `%M` that accept custom formatting 509 | function - and this way, Mongoose's printf can print virtually anything. 510 | For example, JSON strings. That said, with the aid of `mg_http_reply()`, 511 | we can generate HTTP responses of arbitrary complexity. 512 | 513 | So, how the whole flow works? Here is how. When a browser connects, 514 | an Ethernet IRQ handler (layer 1) kicks in. It is defined by Mongoose, and activated by 515 | the `#define MG_ENABLE_DRIVER_STM32H 1` line in the `mongoose_custom.h`: [ETH_IRQHandler](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/drivers/stm32h.c#L252). Other environments, like CubeIDE, implement `ETH_IRQHandler` 516 | and activate it when you select "Enable Ethernet interrupt" in the Ethernet 517 | configuration. To avoid clash with Cube, we did not activate Ethernet interrupt. 518 | 519 | IRQ handler reads frame from the DMA, copies that frame to the Mongoose's 520 | [receive queue](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/net_builtin.h#L30), and exits. 521 | That receive queue is special, it is a thread-safe 522 | single-producer-single-consumer non-blocking queue, so an IRQ handler, being 523 | executed in any context, can safely write to it. 524 | 525 | The `mg_poll()` function in the infinite `while()` loop constantly 526 | verifies, whether we receive any data in the receive queue. When it detects 527 | a frame in the receive queue, it extracts that frame, passes it on to the 528 | [mg_tcp_rx()](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/net_builtin.c#L800) function - which is an etry point to the layer 2 TCP/IP stack. 529 | 530 | 531 | That `mg_tcp_rx()` function parses headers, starting from Ethernet header, 532 | and when it detects that a received frame belongs to one of the Mongoose 533 | TCP or UDP connections, it copies frame payload to the connection's `c->recv` 534 | buffer and [calls `MG_EV_READ` event](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/net_builtin.c#L687). 535 | 536 | 537 | At this point, processing leaves layer 2 and enters layer 3 - a library layer. 538 | Mongoose's HTTP event handlers catches `MG_EV_READ`, parses received data, 539 | and when it detects that the full HTTP message is buffered, it [sends the 540 | `MG_EV_HTTP_MSG` with parsed HTTP message](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/http.c#L1033) to the application - layer 4. 541 | 542 | And this is where our event handler function `fn()` gets called. Our code is 543 | simple - we catch `MG_EV_HTTP_MSG` event, and use Mongoose's API function 544 | `mg_http_reply()` to craft a simple HTTP response: 545 | ``` 546 | HTTP/1.1 200 OK 547 | Content-Length: 4 548 | 549 | ok 550 | ``` 551 | 552 | This response goes to Mongoose's `c->send` output buffer, and `mg_mgr_poll()` 553 | drains that data to the browser, [splitting the response by frames](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/net_builtin.c#L587-L588) 554 | in layer 2, then passing to the layer 1. An Ethernet driver's output function [mg_tcpip_driver_stm32h_tx()](https://github.com/cesanta/mongoose/blob/68e2cd9b296733c9aea8b3401ab946dd25de9c0e/src/drivers/stm32h.c#L208) sends those frames back to the browser. 555 | 556 | This is how Mongoose Library works. 557 | 558 | Other implementations, like Zephyr, Amazon FreeRTOS-TCP, Azure, lwIP, work in 559 | a similar way. They implement BSD socket layer so it is a bit more complicated 560 | cause it includes an extra socket layer, but the principle is the same. 561 | 562 | ## Implementing Web UI 563 | 564 | Using `mg_http_reply()` function is nice, but it's very good for creating 565 | custom responses. It is not suitable for serving files. And the standard way 566 | to build a web UI is to split it into two parts: 567 | - a static part, which consists of directory with `index.html`, CSS, 568 | JavaScript and image files, 569 | - a dynamic part, which serves REST API 570 | 571 | So instead of using `mg_http_reply()` and responding with "ok" to any request, 572 | let's create a directory with `index.html` file and serve that directory. 573 | Mongoose has API function `mg_http_serve_dir()` for that. Let's change the 574 | event handler code to use that function: 575 | 576 | ```c 577 | static void fn(struct mg_connection *c, int ev, void *ev_data) { 578 | if (ev == MG_EV_HTTP_MSG) { 579 | struct mg_http_message *hm = ev_data; // Parsed HTTP request 580 | struct mg_http_serve_opts opts = {.root_dir = "/web_root"}; 581 | mg_http_serve_dir(c, hm, &opts); 582 | } 583 | } 584 | ``` 585 | 586 | Build it and get build error "undefined reference to 'mkdir'". This is because 587 | `mg_http_serve_dir()` function tries to use a default POSIX filesystem to 588 | read files from directory `/web_root`, and our firmware does not have support 589 | for the POSIX filesystem. 590 | 591 | What are the possibilities here? First, we can implement POSIX filesystem, 592 | by using an internal or external flash memory. Then we can copy our `web_root` 593 | directory there, and our code will start to work. This is the hard way. 594 | 595 | The easy way is to use a so-called embedded filesystem, by 596 | transforming all files in the web directory into C arrays, and compiling them 597 | into the firmware binary. This way, all UI files are simply hardcoded into the 598 | firmware binary, and there is no need to implement a "real" filesystem: 599 | 600 | **Step 1**. Tell `mg_http_serve_dir()` to use packed filesystem: 601 | ```c 602 | struct mg_http_serve_opts opts = {.root_dir = "/web_root", .fs = &mg_fs_packed}; 603 | ``` 604 | **Step 2**. Enable packed filesystem, and disable POSIX filesystem in `mongoose_custom.h`: 605 | ```c 606 | #define MG_ENABLE_PACKED_FS 1 607 | #define MG_ENABLE_POSIX_FS 0 608 | ``` 609 | **Step 3**. Create a new file `Core/Src/packed_fs.c`. Go to https://mongoose.ws/ui-pack/, 610 | review UI files. Copy/paste the contents of generated `packed_fs.c`, save. 611 | 612 | Build the firmware - and now it should build with no errors. 613 | 614 | Let's review what that UI packer does. As you can see, it has 3 files, which 615 | implement a very simple Web UI with LED control. The `index.html` file 616 | loads `main.js` file, which defines a button click handler. When a button 617 | gets clicked, it makes a request to the `api/led/toggle` URL, and when 618 | than request completes, it makes another request to `api/led/get` URL, 619 | and sets the status span element to the result of the request. 620 | 621 | The tool has a preview window, and if any of the files are changed, 622 | it automatically refreshes preview and regenerates packed_fs.c. The packed_fs.c 623 | is a simple C file, which contains three C arrays, representing three files 624 | we have, and two helper functions `mg_unlist()` and `mg_unpack()`, used by 625 | Mongoose: 626 | - the `mg_unlist()` function allows to scan the whole "filesystem" and get names of every file, 627 | - the `mg_unpack()` function returns file contents, size, and modification time for a given file. 628 | 629 | Mongoose provides a command line utility [pack.c](https://github.com/cesanta/mongoose/blob/master/test/pack.c) 630 | to generate `packed_fs.c` automatically during the build. The example of that 631 | is a [Makefile](https://github.com/cesanta/mongoose/blob/f883504d2d44d24cae1ca6c9f88ce780ab36f59b/examples/device-dashboard/Makefile#L38-L43) 632 | for device dashboard example in Mongoose repository, which not only packs, 633 | but also compresses files to minimise their size. But here, we'll use the 634 | web tool because it is visual and makes it easy to understand the flow. 635 | 636 | The static HTML is extremely simple. There 3 files: `index.html`, `style.css` 637 | and `main.js`. The `index.html` references, or loads, the other two: 638 | 639 | **index.html:** 640 | ```html 641 | 642 | 643 |
644 | 645 | 646 | 647 |