Termbank
  1. A
    1. Abstraction
    2. Alias
    3. Argument
    4. Array
  2. B
    1. Binary code file
    2. Binary number
    3. Bit
    4. Bitwise negation
    5. Bitwise operation
    6. Byte
  3. C
    1. C library
    2. C-function
    3. C-variable
    4. Character
    5. Code block
    6. Comment
    7. Compiler
    8. Complement
    9. Conditional statement
    10. Conditional structure
    11. Control structure
  4. D
    1. Data structure
    2. Duck typing
  5. E
    1. Error message
    2. Exception
  6. F
    1. Flag
    2. Float
  7. H
    1. Header file
    2. Headers
    3. Hexadecimal
  8. I
    1. Immutable
    2. Initialization
    3. Instruction
    4. Integer
    5. Interpreter
    6. Introduction
    7. Iteroitava
  9. K
    1. Keyword
  10. L
    1. Library
    2. Logical operation
  11. M
    1. Machine language
    2. Macro
    3. Main function
    4. Memory
    5. Method
  12. O
    1. Object
    2. Optimization
  13. P
    1. Parameter
    2. Placeholder
    3. Pointer
    4. Precompiler
    5. Precompiler directive
    6. Prototype
    7. Python console
    8. Python format
    9. Python function
    10. Python import
    11. Python list
    12. Python main program
    13. Python variable
    14. Python-for
    15. Pääfunktio
    16. printf
  14. R
    1. Resource
    2. Return value
  15. S
    1. Statement
    2. Static typing
    3. String
    4. Syntax
  16. T
    1. Terminal
    2. Type
    3. Typecast
  17. U
    1. Unsigned
  18. V
    1. Value
  19. W
    1. Warning
    2. while
Completed: / exercises

Serial communication

Learning goals: Two ways to implement serial communication in an embedded device.
Serial communication means techniques used in the data transmission between devices and components, where data moves in serial format, or in other words, sequentially bit-by-bit through one line. Data can move through the line one way, which means that there needs to be separate lines for receiving and transmitting, or the same line can be used for receiving and transmitting, taking turns in a synchronized way. In the latter case, the communication is often synchronized by a clock line (synchronous communication). In the former case, when there are only two communication lines, there is usually no clock signal and the communication is asynchronous.
Below in the picture is the simple idea of serial communication. Two devices have defined from their I/O-pins transmit- (Tx-pin) and receive-lines (Rx-pin). The devices are connected to each other so that the transmit line of one device is the receive line of another. By doing this, the sequence of bits (the states of Tx-pin as a function of time) from the sender shows up in the states of the receiver's Rx-pin.
"Sarjaliikenne yksinkertaistettuna"
Data is transmitted through data lines from one device to another as a bit sequence. This means that both ends of the transmission must agree on the format of the message (the meaning of each bit), that is, the data transmission protocol. In addition, both ends must use the same transmission speed.
Inside of the data frame of the data transmit protocol, we transfer the actual information, but in addition to that, there might be for example information related to error correction included in the protocol.
Serial communication protocols are mostly standardized, which helps devices (from different vendors) to communicate with each other. Transmit speed on the other hand tells us what is the duration of each bit in the line. If this is not known, it is hard to know in the receiving end, how to interpret the bits from the sequence. In other words, the receiving end would not know, at what point of the signal does the bit start and end, and in which part is the bit supposed to be read. The transmission speed can also be set by the earlier-mentioned clock.
The option introduced earlier for data transmission between components (and systems) is parallel data transfer, examples of which are the address, control and data lines, where bits move at the same time through their own wires. This way, parallel data transfer is faster than serial, but its price is the need for additional parallel wires and I/O-pins. From this point of view, serial communication is more economically efficient, transmission speeds have also increased a lot nowadays.
In this course, we are not going to dive any deeper into the secrets of serial communication. Different, more or less standardized implementations exist in large numbers, but we are going to introduce (and use) the universal serial communication technique UART and I2C-protocol, which we are going to use with the sensors integrated to Pico's extension board. I2C is very common and fairly fast (clock frequency 100-400kHz) serial communication protocol for embedded systems.

Universal Asynchronous Receiver/Transmitter

Universal Asynchronous Receiver/Transmitter (UART), is a serial communication circuit, that converts parallel-type information into serial information, to make communication with peripheral devices easier. With this circuit, we can implement for example data transfer according to the well-known RS-232-standard. UART is general-purpose and a bit old technique, but widely used in embedded systems for ASCII/text-based, bidirectional data transfer. UART can also be used for transmitting binary format ("encoded into numbers") data. One example of this kind of data transfer is writing things to the program memory of an embedded device, when we are uploading our program to it.
A good example of a peripheral device using UART in embedded systems is a GPS-receiver, that sends coordinate information in human-readable NMEA-text format. Another example could be a smallish command line interface based on UART serial communication between a workstation PC and an embedded device. And of course information in CSV-format can be transferred by using UART.
Without going too deep into the protocol implementation, we can conclude that UART-based serial communication needs a couple of data transfer parameters:
  1. Transmission speed (in bauds). Common speeds include 9600, 19200, 38400, 57600 and 115200 bits/second.
  2. Number of data bits: in this course always 8
  3. Parity bit: not used in this course
  4. Number of Stop-bits: in this course always 1.
As a result, the parameters of serial communication are denoted as (for example) 9600 8n1. It is important of course to configure the transmitter and receiver with the same parameters, so that they can understand each other. The upside of this is that when we know the parameters, we don't need a clock line.

Pico UART

Next we will practice with an example, how to use the serial communication library uart, that is also included in the Pico SDK.
In the task serialTask below:
#include <hardware/uart>
...
// Task function
void serialTask(void *pvParams) {

    char input;
    char echo_msg[30];

    // Initializing serial communications
    // Parameters 8n1 9600
    uart_init(uart0, 9600);
    // Setting up UART with default pins
    gpio_set_function(0, GPIO_FUNC_UART);
    gpio_set_function(1, GPIO_FUNC_UART);

    // Eternal loop
    while(1) {
        // Receiving 1 character at a time to the input-variable
        input = uart_getc(uart0);
        // Sending back a string
        sprintf(echo_msg, "Received: %c\n", input);
        uart_puts(uart0, echo_msg);
        // Politely sleeping for a second
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

int main(void) {
    ...
    stdio_init_all();
    ...
    return 0;
}
Serial communication through UART in Pico is in practice just the usage of simple read and write functions, as long as we have an open connection. When opening the connection, it is important to set the desired pins to be used with UART by using the gpio_set_function function call.
Lastly, the serial connection needs to be closed with the function uart_deinit. In the example we haven't used it, since we are operating inside of an infinite loop.
IMPORTANT: It is necessary to check which of the two available UART (UART0 or UART1) can be connected to each one of the pins. This can be found, for instance, in Pico pinout.

Serial communication interrupt

In previous example, we saw how we can read data from serial connection with uart_getc-function so that the task was blocked while waiting for data to be read. However, this is not the best way to get data from UART, because, well.. the task gets blocked until data is received. Now, from the perspective of the MCU, serial communication through UART is a slow operation, so this is not a good practice.
A better way to do this is to set up the serial communication to work in a non-blocking way, so that only when new data is available, an interruption occurs. This can be done by modifying the parameters of our UART connection and of course by using a handler.
// Handler function
void uartFxn() {
    // Now new data is available, it is given to some other function for handling (just for example)
    do_something_fast(uart_getc(uart0));
}

void uartTask(void *pvParams) {
    
    uart_init(uart0, 9600);

    gpio_set_function(0, GPIO_FUNC_UART);
    gpio_set_function(1, GPIO_FUNC_UART);

    // Assigning an interruption handler uartFxn for the interruptions of UART0
    irq_set_exclusive_handler(UART0_IRQ, uartFxn);
    // Start receiving interruptions
    irq_set_enabled(UART0_IRQ, true);

    // We tell the UART that we want to receive interruptions,
    // when new data is available
    uart_set_irq_enables(uart0, true, false);

    while(1) {
        //Infinite loop.
        tight_loop_contents();
        //Here for instance, we could check if there is new data and operate with it. 
    }
}

int main() {
    ...
    stdio_init_all();
    ...
    return 0;
}
Now in the code, the uart library of Pico SDK is initialized inside of a task instead of the main-function. The idea here is still that we isolate the serial communication in its own task.
The difference compared to the previous initialization of serial communication is that here we also set up an interrupt handler using the function irq_set_exclusive_handler, and then enable interrupt processing with the functions irq_set_enabled and uart_set_irq_enables. With these functions, we first inform the processor’s that we want to handle UART interrupts (constant UART_IRQ). After that, we configure the UART peripheral itself to generate interrupts by calling uart_set_irq_enables.
The second argument of this function is set to true, which enables interrupts when data is received. It is also possible to enable interrupts for transmission (third argument), but in this example it is not necessary.

UART and USB

The traditional way to communicate a embedded device with a computer has been through a UART interface. Because modern computers no longer include physical serial ports, we need an extra adapter to convert UART signals (TTL levels) into USB. This is done using an FTDI adapter (also called TTL-to-Serial), which can be a simple cable or an integrated chip on a development board. For example, many Arduino boards and the TI SensorTag include such a chip without us even noticing it, so that the USB cable we connect is actually converted into a UART channel internally. When communicating with a workstation, usually speed and other parameters need to be set to its serial port (e.g /dev/ttyACM0, in Unix, COMx in Windows, etc.). Pico's drivers create one logical serial port (named Raspberry Pi Pico) from its USB-connection with the workstation. This UART over USB to communicate with the device by using a terminal program, screen or Putty for example.
In contrast, the Raspberry Pi Pico can act directly as a USB device without requiring any external adapter. It supports the USB class CDC-ACM, which means it can present itself to the host computer as a virtual COM port. This way, when we call functions like printf in our code, the text can be sent through USB and appear as if it were transmitted over a UART. So, with Pico we can send serial data over USB directly, while still having the option to use the physical UART pins if we want a real hardware serial connection to another device. Since it is a serial communication we can also use any of the previous programs to communicate with the pico, namely screen or Putty for example.
More on USB communication below.

I2C-bus

I2C-serial communication bus is a well-known binary-format serial communication protocol for embedded devices.
The functionality of I2C-bus is based on the master/slave-architecture, that has been used since the dawn of computer technology. Connected to a bus, there is always one master device that controls it, and n slave devices. Here master initiates the connection and sets the transmission speed, that the slave devices follow. I2C needs two I/O-pins, clock (Serial Clock Line, SCL) and data line (Serial DAta Line, SDA).
Now, in I2C bus the devices and components are identified by addresses. The address has been pre-defined by the manufacturer of the device and it can be found from the datasheet of the component. Sometimes manufacturers provide several I2C-addresses for their components and in this case, the user can choose an address. This is extremely useful especially when there are many similar components in one bus, for example sensors. One I2C-bus can have 1008 devices connected to it simultaneously! The address of an I2C device is 7 bits, followed by one bit that indicates whether the operation is a read (1) or a write (0).
In general, I2C-messages consist of two main elements: the receiver address and the message body. The receiver address is usually one byte long (7 address bits plus the r/w bit), and it specifies which device on the bus should respond. The message body can contain a command and, optionally, some data. Any transaction, initiates with the START condition. For example, with an I2C LCD module the master can write a command to set the cursor position, then write data bytes to display characters, and if the module exposes readable status/registers—read back information such as the busy/status flag or keypad state before sending the next command. One transaction, which may include multiple bytes, is terminated by a STOP condition.
In embedded systems, especially when working with digital sensors, this idea is typically refined to the concept of registers. Instead of generic commands, the master specifies the register address inside the sensor that it wants to access. Then the master either writes data to that register (for configuration) or reads data from it (to obtain a measurement).
The write transaction (modifying the value of a register) is simple: the first byte in the transaction (after the START condition) is the number which identifies the register and the following bytes (until the STOP condition) specify the data to be written into the register.
The read transaction is a bit more complicated. First, the master writes the address of the register it wants to read. Then it issues a repeated START (within the same transaction) and begins a read operation, which includes sending the slave address again with the read bit set.
Below is an example of the frame structure of an i2c-message. For general information only.
""

I2C in Pico

Next we will look at the usage of i2c-library provided by the Pico SDK with the help of a code example. In the example we will read a temperature value from the temperature sensor HDC2021, which is integrated to Pico's extension board. We can see that the datasheet of the sensor is full of many types of complex information, which we don't need to familiarize ourselves with during this course. However, we are interested in the usage of the registers of the sensor (chapter 7.6 in the datasheet) through i2c. But now we are doing it based on predefined constants.
void sensorTask(void *pvParameters) {
    float temperature;
    // Initializing I2C-bus
    i2c_init(i2c_default, 400*1000);
    // Setting the default pins for I2C to be used by our program
    gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
    gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
    gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);

    // Read and write buffers for I2C messages
   
   uint8_t txBuffer[1]; // Now we send one byte
   uint8_t rxBuffer[2]; // Now we receive two bytes
   
   txBuffer [0] = HDC2021_TEMP_LOW; 

    while(1) {
        if(i2c_write_blocking(i2c_default, HDC2021_I2C_ADDRESS , txBuffer, 1, true) != PICO_ERROR_GENERIC) {
            if(i2c_read_blocking(i2c_default, HDC2021_I2C_ADDRESS, rxBuffer, 2, false) != PICO_ERROR_GENERIC) {                
                // Changing the 2-byte data in rxBuffer
                // into a temperature value (formula in exercise material)
                //PART OF THE LAB SESSION.
                temperature = ... ;

                // Temperature value to console window
                printf("%f", temperature);
            }
        }
        else {
            printf("I2C Bus fault\n");
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    // Closing the I2C connection, though the infinite loop never gets here
    i2c_deinit(i2c_default);
}

int main() {
    ...
    stdio_init_all();
    //Code to create the sensorTask. Seen in previous slides

    //Starting the schedule.
    vTaskStartScheduler();
    return 0;
}
Let's look at this example step by step.

Initializations

Similarly to other peripheral components, Pico SDK offers us a bunch of functions and the constants needed to initialize and use them. It should be noted that in the example, everything related to i2c is done inside of the sensorTask. The idea here is that this task handles all the i2c-traffic between the device and sensors.
Now, i2c is enabled by using the initialization function provided by Pico's i2c library. We need to tell this function, which one of the two i2c buses of our Pico we want to use. In this case we choose the default bus i2c_default. The initialization function also wants to know the data transfer speed, now we set this to 400 kHz.
    i2c_init(i2c_default, 400*1000);
Just like in the case of UART, we need to reserve some pins to be used with i2c. Here we are going to use the default pins for i2c in Pico, and set the function of those pins to be I2C. Additionally, we also want to set the pins to a known state, which in this case is high. This is done with the function gpio_pull_up. Actually, the bus should have pull up resistors, but we will use the pull-up of the pins just in case.
    gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
    gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
    gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);

Communication using the i2c-bus

In the example we request for measurement values from the temperature sensor HDC2021. To do this, we need to define storage areas for the resulting data, which are also called buffers. Here we need to define two buffers:
by using the constants provided by a library.
    uint8_t txBuffer[1];
    uint8_t rxBuffer[2];

    txBuffer[0] = HDC2021_TEMP_LOW;
Note! It is also possible to send i2c messages that do not contain any data, like some commands. In this case, we obviously don't need a buffer for data reading.
When the buffers are defined and the register to read is assigned to the transmit buffer, we can send the message to the peripheral device. Notice that the transaction is not ended here, because the parameter 'noStop' is set to true in i2c_write_blocking.
If an error occurs, PICO_ERROR_GENERIC is returned from the function call i2c_write_blocking. If this happens, we know that an error has occured and we should not try to read value from the sensor.
       if(i2c_write_blocking(i2c_default, HDC2021_I2C_ADDRESS, txBuffer, 1, true) != PICO_ERROR_GENERIC) {
            ...
        }
        else {
            printf("I2C Bus fault\n");
        }
If however, there is no error, we can read the value from the sensor:
...
    if(i2c_read_blocking(i2c_default, HDC2021_I2C_ADDRESS, rxBuffer, 2, false) != PICO_ERROR_GENERIC) {
    temperature = ... ;
    printf("%f", temperature);
    }
    else {
    printf("I2C Bus fault\n");
    }
...
Here we also need to consider the possible error and inform about it by using the console window.
If reading the messages succeeds, the 16-bit value from the data register of the sensor is now stored to the two bytes in rxBuffer. The task of the programmer is now to convert the value of rxBuffer into a temperature value. The formula for this is presented in the exercise tasks and also in the later course material.

Closing the i2c-connection

There might be a need to close the i2c-connection in a program. It can be done in a following way:
   ...
   i2c_deinit(i2c_default);
   ...

USB communication

USB in RP2040

The RP2040 has an integrated USB controller, so we can use USB directly without any external chips. This controller can work in two modes: as a USB host (controlling other USB devices such as a keyboard or flash drive) or as a USB device (appearing to a computer as a peripheral). In our case, we are interested in the device mode, where the Pico supports the CDC-ACM profile (Communications Device Class – Abstract Control Model).
This profile allows us to use the Pico as a virtual serial port, so that serial communication (normally UART TTL signals) can be carried over USB. This is extremely practical because it lets us communicate directly with a computer or workstation using tools like minicom, screen, or any other serial terminal.
Under the hood, the Pico SDK relies on the TinyUSB library to provide the USB functionality.
In the following examples we will see how to send data from the Raspberry Pi Pico to the computer via USB.
To use TinyUSB in your own project, two files are essential:
You do not need to worry about those files, in the example configuration we have included those files for you.

stdin and stdout over USB

Another option, often the simplest in practice, is to redirect the C standard streams stdin, stdout and stderr to the Pico’s USB CDC-ACM interface. When configured this way, the board also appears to the host as a serial port; you can open it with a terminal such as minicom] or PuTTY], and then send and receive text or binary data through the standard I/O functions.
To make USB work as your stdio backend, you need to initialize it in code and enable it in the build. In code you can call stdio_init_all() to bring up all enabled backends, or stdio_usb_init() if you want to initialize only the USB backend yourself. In the build system you must link the USB stdio support and enable it for your target. With the Pico SDK this is typically expressed in CMake as:
pico_enable_stdio_usb(your_target 1)
pico_enable_stdio_uart(your_target 0)  // optional
This pulls in the required pico_stdio_usb support and routes the C streams to USB for that executable. Disabling UART is optional, but it avoids confusion about where your prints go.
Once USB stdio is active, you can use the usual C I/O along with a couple of Pico-specific helpers. For single characters to stdout, putchar() writes one character and, on the USB backend, a newline \n is often translated to \r\n which is convenient for many terminals. If you need to send bytes exactly as they are (e.g. for sending binary data), use the Pico helper putchar_raw(), which performs no line-ending translation. For printing whole lines of text, puts() writes a C string and appends a newline automatically. Remember that the same newline translation applies on USB when not using the raw path.
For higher throughput, prefer block writes. The standard call fwrite( char *ptr, int size, int count, stdout) sends a block of bytes from memory to the stream and is ideal when you have buffered data such as sensor frames or audio samples. After a large fwrite(), you may want to flush the buffer so it leaves the device promptly; you can use fflush(stdout) from the C library or the Pico helper stdio_flush() to nudge pending data out over USB.
Reading is symmetrical. The standard getchar() performs a blocking read of one character from stdin. In this case, your code will wait until something arrives over the USB serial connection. If you prefer not to block, the Pico SDK provides getchar_timeout_us(timeout_us). With a timeout of zero it becomes a non-blocking poll: if a byte is available, you get it as a non-negative return value; if not, you get PICO_ERROR_TIMEOUT. This is a good fit for main loops that must keep running other tasks while occasionally checking for inbound data. If you need to read many bytes at once, standard buffered I/O such as fread() on stdin works well and pairs naturally with fwrite() for echoing or forwarding.
Here is a compact baseline that brings the pieces together and follows the approach above:
#include "stdio.h"
#include "pico/stdlib.h"
#include "pico/stdio.h"  

int main(void) {
    stdio_init_all();                 // or stdio_usb_init();

    while (!stdio_usb_connected()) {  // optional: wait for a terminal
        sleep_ms(10);
    }

    puts("USB ready.");

    while(1) {
        int ch = getchar_timeout_us(1000);   // non-blocking poll
        if (ch >= 0) {
            putchar_raw((char)ch);        // echo back exactly what we received
        }
        // Example block write elsewhere:
        // fwrite(buf, 1, len, stdout);
        // stdio_flush();
        tight_loop_contents();
    }
}
With this setup you have clear choices: putchar() and puts() are convenient for human-readable logs, putchar_raw() and fwrite() give you exact-bytes control and efficient block transfers, and getchar()/getchar_timeout_us() cover blocking and non-blocking input respectively.

Separate debugging and serial port using USB

In order to make your life easier, we have created a library that generates two different serial ports. One is for sending serial data using the TinyUSB library, and the other is for sending debug information. You will find this library in the project provided for the exercises.
In this case, sending debug information requires avoiding the standard stdio functions (printf, puts, …) and instead using a customized library that we have prepared for the course. Furthermore, the library integrates with FreeRTOS, so you do not need to worry about integration issues. You also do not need to write the descriptors.c or tusb_config, since they are included in the library. You only need to link the library in your CMake and disable pico_enable_stdio_usb(your_app 0), because we are not using the standard stdio.
On your computer, you need to create a serial terminal (e.g., using Arduino, minicom, screen, PuTTY, …). The lower port (/dev/ttyACM0) in Unix behaves like printf, while the upper port (/dev/ttyACM1) is used to send and receive data through the TinyUSB interface.
Let’s start with an example that sends sensor data from the Pico to your computer. The data is processed in one task and sent to the computer’s terminal:
#include "pico/stdio.h"

#include "FreeRTOS.h"
#include "task.h"
#include "tusb.h"

#define CDC_ITF_TX      1


// ---- Task generating sensor data ----
static void sensorTask (void *arg){
    char buf[BUFFER_SIZE]; 

    while (!tud_mounted() || !tud_cdc_n_connected(1)){
        vTaskDelay(pdMS_TO_TICKS(50));
    }

    while (1) {
        int temp = ....; // Obtained somewhere else in the code
        int lux  = ....; // Obtained somewhere else in the code. 
        
        if (tud_cdc_n_connected(CDC_ITF_TX)){
            // Send them using tud_cdc_n_write
            snprintf(buf, BUFFER_SIZE, "%d, %d\n", temp, lux);
            tud_cdc_n_write(CDC_ITF_TX, buf, strlen(buf));
            tud_cdc_n_write_flush(CDC_ITF_TX);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}


// ---- Task running USB stack ----
static void usbTask(void *arg) {
    (void)arg;
    while (1) {
        tud_task();              // With FreeRTOS, wait for events
                                 // Do not add vTaskDelay.
    }
}


int main (void) {

    // Create tasks
    TaskHandle_t hUsb = NULL;
    xTaskCreate(usbTask, "usb", 1024, NULL, 3, &hUsb);
    xTaskCreate(sensorTask, "app", 1024, NULL, 2, NULL);

    #if (configNUMBER_OF_CORES > 1)
        vTaskCoreAffinitySet(hUsb, 1u << 0);
    #endif
     // Initialize TinyUSB first. This code should be just before to vTaskStartScheduler();
    tusb_init();
    vTaskStartScheduler();
}
Let’s start by checking the libraries:
#include "pico/stdio.h"

#include "FreeRTOS.h"
#include "task.h"
#include "tusb.h"
The novelty here is adding <tusb.h>, which is the library used to create a serial port over USB.
Our main function generates two different tasks. One is the task in charge of handling the whole USB stack (with the intuitive name usbTask). Note that this task has higher priority than the rest (in this case, 3):
xTaskCreate(usbTask, "usb", 1024, NULL, 3, &hUsb);
Its body is very simple: it just calls tud_task, which waits for events coming from the USB stack. Please note that you do not need to include vTaskDelay, since tud_task() blocks until an event happens.
The sensorTask simply sends the collected values of temperature and luminance to the computer. First, it checks if we can send data through the CDC port (identified as 1 in this case) using tud_cdc_n_connected. If so, it writes the data to a buffer using snprintf (we told you this function would be useful). Finally, the data is sent using tud_cdc_n_write(1, buf, strlen(buf)), and we ensure that it has left the Pico with tud_cdc_n_write_flush(1).
Ok, but what about sending data from the computer to the Pico? That’s easy: use your serial terminal to type data. How is the Pico able to read it? It uses a callback function — specifically, void tud_cdc_rx_cb(uint8_t itf);. You can see the full callback API in the sources. Let’s see how it’s implemented.
// callback when data is received on a CDC interface
void tud_cdc_rx_cb(uint8_t itf){   
    // allocate buffer for the data on the stack
    uint8_t buf[CFG_TUD_CDC_RX_BUFSIZE + 1];

    // read the available data 
    // | IMPORTANT: do this for CDC0 as well, otherwise
    // | printing to CDC0 can stall (its RX buffer fills up)
    // | before the next time this function is called
    uint32_t count = tud_cdc_n_read(itf, buf, sizeof(buf));

    // check if the data was received on the second CDC interface
    if (itf == 1) {
        // Process the data here (e.g., store in a buffer or send to another task).
        // ...

        // Be gentle and send an OK back. 
        tud_cdc_n_write(itf, (uint8_t const *) "OK\n", 3);
        tud_cdc_n_write_flush(itf);
    }

    // Optional: if you need a C-string, you can terminate it:
    // if (count < sizeof(buf)) buf[count] = '\0';
}
The new piece here is the read call:
uint32_t count = tud_cdc_n_read(itf, buf, sizeof(buf));
It reads data from USB into buf. You’ll then process that data as needed. In this example we just send a friendly acknowledgment back.
Please note:
- You should perform the read on both interfaces (including the one used for debug). Even if you don’t use that data, reading prevents the RX buffer from filling and blocking prints.
- The function name must be exactly tud_cdc_rx_cb. Do not rename it; otherwise the TinyUSB stack won’t know which function to call when data arrives.
- This callback is not an interrupt handler you write yourself (interrupts are handled inside the stack). Still, keep work inside the callback light: avoid heavy processing and large prints here. Offload substantial work to tasks.
Finally, we need to show how to use Port 0 (CDC0) to send debug data. Let’s slightly modify the initial example.
#include "pico/stdio.h"

#include "FreeRTOS.h"
#include "task.h"
#include "tusb.h"
#include "usbSerialDebug/helper.h"

#define CDC_ITF_TX  1
// #define BUFFER_SIZE  ...  // define this to a suitable size if not defined elsewhere

// ---- Task generating sensor data ----
static void sensorTask(void *arg) {
    char buf[BUFFER_SIZE];

    // Wait until USB is mounted and CDC1 is connected (data port)
    while (!tud_mounted() || !tud_cdc_n_connected(CDC_ITF_TX)) {
        vTaskDelay(pdMS_TO_TICKS(50));
    }

    while (1) {
        int temp = ....; // Obtained somewhere else in the code
        int lux  = ....; // Obtained somewhere else in the code

        // Send sensor data over CDC1 using TinyUSB (computer reads /dev/ttyACM1 on Unix)
        if (tud_cdc_n_connected(CDC_ITF_TX)) {
            snprintf(buf, BUFFER_SIZE, "%d, %d\n", temp, lux);
            tud_cdc_n_write(CDC_ITF_TX, buf, strlen(buf));
            tud_cdc_n_write_flush(CDC_ITF_TX);
        }

        // Send debug text over CDC0 using the helper library (computer reads /dev/ttyACM0 on Unix)
        if (usb_serial_connected()) {
            snprintf(buf, BUFFER_SIZE, "temp:%d, light:%d\n", temp, lux);
            usb_serial_print(buf);
            usb_serial_flush();
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// ---- Task running USB stack ----
static void usbTask(void *arg) {
    (void)arg;
    while (1) {
        tud_task();              // With FreeRTOS, wait for events
                                 // Do not add vTaskDelay.
    }
}

int main(void) {

    // Create tasks
    TaskHandle_t hUsb = NULL;
    xTaskCreate(usbTask, "usb", 1024, NULL, 3, &hUsb);
    xTaskCreate(sensorTask, "app", 1024, NULL, 2, NULL);

    #if (configNUMBER_OF_CORES > 1)
        vTaskCoreAffinitySet(hUsb, 1u << 0);
    #endif
    // This two functions should be just before vTaskStartScheduler
    // Initialize TinyUSB first
    tusb_init();

    // Initialize helper library to write via CDC0
    usb_serial_init();
    vTaskStartScheduler();
}
In this case, we first include our helper library with #include "usbSerialDebug/helper.h" and initialize it in main after TinyUSB with usb_serial_init();.
Then, if you want to write debug messages in any task, use usb_serial_print(const char *s); which takes a C string as input (it must end with \0). If you need formatting, prefer snprintf to build the buffer safely. You can also ensure the debug message leaves the Pico with usb_serial_flush(), and you can check if someone is listening with usb_serial_connected().
Easy, isn’t it?

To conclude

Serial communication in the world of embedded systems is surprisingly complicated, with several protocols and implementations, but with the information provided here, we can get started with reading and writing data from/to the sensors included in the extension board of Pico.
In addition to i2c-protocol, SPI is another well known serial communication protocol in the world of embedded systems. The choice of protocol is of course dependent on the decisions taken by the component manufacturer.
In the examples above, we did not consider adjusting the settings of the sensor or calibration, because we know that the SDK of Pico's extension board will handle that for us. This functionality can be of course modified with own code, as long as one knows what they are doing with registers and bitwise operations.
?
Abstraction is a process through which raw machine language instructions are "hidden" underneath the statements of a higher level programming language. Abstraction level determines how extensive the hiding is - the higher the abstraction level, the more difficult it is to exactly say how a complex statement will be turned into machine language instructions. For instance, the abstraction level of Python is much higher than that of C (in fact, Python has been made with C).
Alias is a directive for the precompiler that substitus a string with another string whenever encountered. In it's basic form it's comparable to the replace operation in a text editor. Aliases are define with the #define directeve, e.g. #define PI 3.1416
Argument is the name for values that are given to functions when they are called. Arguments are stored into parameters when inside the function, although in C both sides are often called just arguments. For example in printf("%c", character); there are two arguments: "%c" format template and the contents of the character variable.
Array is a common structure in programming languages that contains multiple values of (usually) the same type. Arrays in C are static - their size must be defined when they are introduced and it cannot change. C arrays can only contain values of one type (also defined when introduced).
Binary code file is a file that contains machine language instructions in binary format. They are meant to be read only by machines. Typically if you attempt to open a binary file in a text editor, you'll see just a mess of random characters as the editor is attempting to decode the bits into characters. Most editors will also warn that the file is binary.
Binary number is a number made of bits, i.e. digits 0 and 1. This makes it a base 2 number system.
A bit is the smallest unit of information. It can have exactly two values: 0 and 1. Inside the computer everything happens with bits. Typically the memory contains bitstrings that are made of multiple bits.
Bitwise negation is an operation where each bit of a binary number is negated so that zeros become ones and vice versa. The operator is ~.
Bitwise operations are a class of operations with the common feature that they manipulate individual bits. For example bitwise negation reverses each bit. Some operations take place between two binary values so that bits in the same position affect each other. These operations include and (&), or (|) and xor (^). There's also shift operations (<< and >>) where the bits of one binary number are shifted to the left or right N steps.
Byte is the size of one memory slot - typically 8 bits. It is the smallest unit of information that can be addressed from the computer's memory. The sizes of variable types are defined as bytes.
External code in C is placed in libraries from which they can be taken to use with the #include directive. C has its own standard libraries, and other libraries can also be included. However any non-standard libraries must be declared to the compiler. Typically a library is made of its source code file (.c) and header file (.h) which includes function prototypes etc.
Functions in C are more static than their Python counterparts. A function in C can only have ne return value and its type must be predefined. Likewise the types of all parameers must be defined. When a function is called, the values of arguments are copied into memory reserved for the function parameters. Therefore functions always handle values that are separate from the values handled by the coe that called them.
C variables are statically typed, which means their type is defined as the variable is introduced. In addition, C variables are tied to their memory area. The type of a variable cannot be changed.
Character is a single character, referred in C as char. It can be interpreted as an ASCII character but can also be used as an integer as it is the smallest integer that can be stored in memory. It's exactly 1 byte. A character is marked with single quotes, e.g. 'c'.
Code block is a group of code lines that are in the same context. For instance, in a conditional structure each condtion contains its own code block. Likewise the contents of a function are in their own code block. Code blocks can contain other code blocks. Python uses indentation to separate code blocks from each other. C uses curly braces to mark the beginning and end of a code block.
Comments are text in code files that are not part of the program. Each language has its own way of marking comments. Python uses the # character, C the more standard //. In C it's also possible to mark multiple lines as comments by placing them between /* and */.
A compiler is a program that transforms C source code into a binary file containing machine language instructions that can be executed by the computer's processor. The compiler also examines the source code and informs the user about any errors or potential issues in the code (warnings). The compiler's behavior can be altered with numerous flags.
Complement is a way to represent negative numbers, used typically in computers. The sign of a number is changed by flipping all its bits. In two's complement which is used in this course, 1 is added to the result after flipping.
Conditional statement is (usually) a line of code that defined a single condition, followed by a code block delimited by curly braces that is entered if the condition evaluates as true. Conditional statements are if statements that can also be present with the else keyword as else if. A set of conditional statements linked together by else keywords are called conditional structures.
Conditional structure is a control structure consisting of one or more conditional statements. Most contrl structures contain at least two branches: if and else. Between these two there can also be any number of else if statements. It is however also possible to have just a single if statement. Each branch in a conditional structure cotains executable code enclosed within a block. Only one branch of the structure is ever entered - with overlapping conditions the first one that matches is selected.
Control structures are code structures that somehow alter the program's control flow. Conditional structures and loops belong to this category. Exception handling can also be considered as a form of control structure.
Data structure is a comman name for collection that contain multiple values. In Python these include lists, tuples and dictionaries. In C the most common data structures are arrays and structs.
Python's way of treating variable values is called dynamic typing aka duck typing. The latter comes from the saying "if it swims like a duck, walks like a duck and quacks like a duck, it is a duck". In other words, the validity of a value is determined by its properties in a case-by-case fashion rather than its type.
An error message is given by the computer when something goes wrong while running or compiling a program. Typically it contains information about the problem that was encountered and its location in the source code.
An exception is what happens when a program encounters an error. Exceptions have type (e.g. TypeError) that can be used in exception handling within the program, and also as information when debugging. Typically exceptions also include textual description of the problem.
Flags are used when executing programs from the command line interface. Flags are options that define how the program behaves. Usually a flag is a single character prefixed with a single dash (e.g. -o) or a word (or multiple words connected with dashes) prefixed with two dashes (e.g. --system. Some flags are Boolean flags which means they are either on (if present) or off (if not present). Other flags take a parameter which is typically put after the flag separated either by a space or = character (e.g. -o hemulen.exe.
Floating point numbers are an approximation of decimal numbers that are used by computers. Due to their archicture computers aren't able to process real decimal numbers, so they use floats instead. Sometimes the imprecision of floats can cause rounding errors - this is good to keep in mind. In C there are two kinds of floating point numbers: float and double, where the latter has twice the number of bits.
Header files use the .h extension, and they contain the headers (function prototypes, type definitions etc.) for a .c file with the same name.
Headers in C are used to indicate what is in the code file. This includes things like function prototypes. Other typical content for headers are definition of types (structs etc.) and constants. Headers can be at the beginning of the code file, but more often - especially for libraries - they are in placed in a separate header (.h) file.
Hexadecimal numbers are base 16 numbers that are used particularly to represent memory addresses and the binary contents of memory. A hexadecimal number is typically prefixed with 0x. They use the letters A-F to represent digits 10 to 15. Hexadecimals are used because each digit represents exactly 4 bits which makes transformation to binary and back easy.
In Python objects were categorized into mutable and immutable values. An immutable value cannot have its contents changed - any operations that seemingly alter the object actually create an altered copy in a new memory location. For instance strings are immutable in Python. In C this categorization is not needed because the relationship of variables and memory is tighter - the same variable addresses the same area of memory for the duration of its existence.
When a variable is given its initial value in code, the process is called initialization. A typical example is the initialization of a number to zero. Initialization can be done alongside with introduction: int counter = 0; or separately. If a variable has not been initialized, its content is whatever was left there by the previous owner of the memory area.
Instruction set defines what instructions the processor is capable of. These instructions form the machine language of the processor architecture.
Integers themselves are probably familiar at this point. However in C there's many kinds of integers. Integer types are distinguished by their size in bits and whether they are signed or not. As a given number of bits can represent up to (2 ^ n) different integers, the maximum value for a signed integer is (2 * (n - 1))
Python interpreter is a program that transforms Python code into machine language instructions at runtime.
The moment a variable's existence is announed for the first is called introduction. When introduced, a variable's type and name must be defined, e.g. int number;. When a variable is introduced, memory is reserved for it even though nothing is written there yet - whatever was in the memory previously is still there. For this reason it's often a good idea to initialize variables when introducing them.
Iteroitava objekti on sellainen, jonka voi antaa silmukalle läpikäytäväksi (Pythonissa for-silmukalle). Tähän joukkoon kuuluvat yleisimpinä listat, merkkijonot ja generaattorit. C:ssä ei ole silmukkaa, joka vastaisi Pythonin for-silmukan toimintaa, joten taulukoiden yms. läpikäynti tehdään indeksiä kasvattavilla silmukoilla.
Keywords are words in programming languages that have been reserved. Good text editors generally use a different formatting for keywords (e.g. bold). Usually keywords are protected and their names cannot be used for variables. Typical keywords include if and else that are used in control structures. In a way keywords are part of the programming language's grammar.
A library is typically a toolbox of functions around a single purpose. Libraries are taken to use with the include directive. If a library is not part of the C standard library, its use must also be told to the compiler.
Logical operation refers to Boole's algebra, dealing with truth values. Typical logical operations are not, and, or which are often used in conditional statements. C also uses bitwise logical operations that work in the same way but affect each bit separately.
Machine language is made of instructions understood by the processor. Machine language is often called Assembly and it is the lowest level where it's reasonable for humans to give instructions to computers. Machine language is used at the latter part of this course - students taking the introduction part do not need to learn it.
Macro is an alias that defines a certain keyword to be replaced by a piece of code. When used well, macros can create more readable code. However, often the opposite is true. Using macros is not recommended in this course, you should just be able to recognize one when you see it.
In C the main function is the starting point when the program is run. The command line arguments of the program are passed on to the main function (although they do not have to be received), and its return value type is int. At its shortest a main function can defined as int main().
When programs are run, all their data is stored in the computer's memory. The memory consists of memory slots with an address and contents. All slots are of equal size - if an instance of data is larger, a continuous area of multiple memory slots is reserved.
Method is a function that belongs to an object, often used by the object to manipulate itself. When calling a method, the object is put before the method: values.sort().
Object is common terminology in Python. Everything in Python is treated as objects - this means that everything can be referenced by a variable (e.g. you can use a variable to refer to a function). Objects are typically used in object-oriented languages. C is not one.
Optimization means improving the performance of code, typically by reducing the time it takes to run the code or its memory usage. The most important thing to understand about opimization is that it should not be done unless it's needed. Optimization should only be considered once the code is running too slowly or doesn't fit into memory. Optimization should also not be done blindly. It's important to profile the code and only optimize the parts that are most wasteful.
A parameter is a variable defined alongside with a function. Parameters receive the values of the function's arguments when it's called. This differentation between parameters and arguments is not always used, sometimes both ends of the value transfer are called arguments.
Placeholders are used in string formatting to mark a place where a value from e.g. a variable will be placed. In Python we used curly braces to mark formatting placeholders. In C the % character is used which is followed by definitions, where the type of the value is mandatory. For instance "%c" can only receive a char type variable.
Pointers in C are special variables. A pointer contains a memory address of the memory location where the actual data value is located. In a sense they work like Python variables. A variable can be defined as a pointer by postfixing its type with * when it's being introduced, e.g. int* value_ptr; creates a pointer to an integer. The contents of the memory address can be fetched by prefixing the variable name with * (e.g. *value_ptr. On the other hand, the address of a memory adress can be fetched by prefixing a variable name with &, (e.g. &value.
The C precompiler is an apparatus that goes through all the precompiler directives in the code before the program is actually compiled. These directives include statements which add the source code of the included libraries into the program, and define directives that can define constant values (aliases) and macros.
Directives are instructions that are addressed at the precompiler. They are executed and removed from the code before the actual compilation. Directives start with the # character. The most common one is include which takes a library into use. Another common one is define, which is used e.g. to create constant values.
Prototype defines a function's signature - the type of its return value, its name and all the arguments. A prototype is separate from the actual function definition. It's just a promise that the function that matches the prototype will be found in the code file. Prototypes are introduced at the beginning of the file or in a separate header file. In common cases the prototype definition is the same as the line that actually starts the function introduction.
Interactive interpreter or Python console is a program where users can write Python code lines. It's called interactive because each code line is executed after its been fully written, and the interpreter shows the return value (if any).
The format method of string in Python is a powerful way to include variable values into printable text. The string can use placeholders to indicate where the format method's arguments are placed.
Python functions can have optional parameters that have a given default value. In Python the values of arguments in a function call are transferred to function parameters through reference, which means that the values are the same even though they may have different names. Python functions can have multiple return values.
In Python the import statement is used for bringing in modules/libraries - either built-in ones, thrid party modules or other parts of the same application. In Python the names from the imported module's namespace are accessible through the module name (e.g. math.sin). In C libraries are taken to use with include, and unlike Python import it brings the library's namespace into the program's global namespace.
Python lists were discovered to be extremely effective tools in Elementary Programming. A Python list is an ordered collection of values. Its size is dynamic (i.e. can be changed during execution) and it can include any values - even mixed types. Lists can also include other lists etc.
In Python main program is the part of code that is executed when the program is started. Usually the main program is at the end of the code file and most of the time under if __name__ == "__main__": if statement. In C there is no main program as such, code execution starts with the main function instead.
In Python a variable is a reference to a value, a connection between the variable's name in code and the actual data in memory. In Python variables have no type but their values do. The validity of a value is tested case by case when code is executed. In these ways they are different from C variables, and in truth Python variables are closer to C pointers.
Pythonin for-silmukka vastaa toiminnaltaan useimmissa kielissä olevaa foreach-silmukkaa. Se käy läpi sekvenssin -esim. listan - jäsen kerrallaan, ottaen kulloinkin käsittelyssä olevan jäsenen talteen silmukkamuuttujaan. Silmukka loppuu, kun iteroitava sekvenssi päättyy.
Pääfunktio on C:ssä ohjelman aloituspiste ja se korvaa Pythonista tutun pääohjelman. Oletuksena pääfunktion nimi on main ja se määritellään yksinkertaisimmillaan int main().
Resource referes to the processing power, memory, peripheral devices etc. that are availlable in the device. It includes all the limitations within which programs can be executed and therefore defines what is possible with program code. On a desktop PC resources are - for a programmer student - almost limitless, but on embedded devices resources are much more scarce.
Return value is what a function returns when its execution ends. In C functions can only have one return value, while in Python there can be multiple. When reading code, return value can be understood as something that replaces the function call after the function has been executed.
A statement is a generic name for a single executable set of instructions - usually one line of code.
C uses static typing This means that the type of variables is defined as they are created, and values of different types cannot be assigned to them. The validity of a value is determined by its type (usually done by the compiler). Python on the other hand uses dynamic typing aka.duck typing.
In Python all text is handled as strings and it has no type for single characters. However in C there are no strings at all - there's only character arrays. A character array can be defined like a string however, e.g. char animal[7] = "donkey"; where the number is the size of the array + 1. The +1 is neede because the string must have space for the null terminator '\0' which is automatically added to the end of the "string".
Syntax is the grammar of a programming language. If a text file does not follow the syntax of code, it cannot be executed as code, or in the case of C, it cannot be compiled.
Terminal, command line interface, command line prompt etc. are different names to the text-based interface of the operating system. In Windows you can start the command line prompt by typing md to the Run... window (Win+R). Command line is used to give text-based commands to the operating system.
The data in a computer's memory is just bits, but variables have type. Type defines how the bits in memory should be interpreted. It also defines how many bits are required to store a value of the type. Types are for instance int, float and char.
Typecast is an operation where a variable is transformed to another type. In the elementary course this was primarily done with int and float functions. In C typecast is marked a bit differently: floating = (float) integer}. It's also noteworthy that the result must be stored in a variable that is the proper type. it is not possible to change the type of an existing variable.
Unsigned integer is a an integer type where all values are interpreted as positive. Since sign bit is not needed, unsigned integers can represent twice as large numbers as signed integers of the same size. An integer can be introduced as unsigned by using the unsigend keyword, e.g. unsigned int counter;.
In the elementary programming course we used the term value to refer to all kinds of values handled by programs be it variables, statement results or anything. In short, a value is data in the computer's memory that can be referenced by variables. In C the relationship between a variable and its value is tighter as variables are strictly tied to the memory area where its value is stored.
A warning is a notification that while executing or - in this course particularly - compiling it, something suspicious was encountered. The program may still work, but parts of it may exhibit incorrect behavior. In general all warnings should be fixed to make the program stable.
One way to print stuff in C is the printf function, which closely resembles Python's print function. It is given a printable string along with values that will be formatted into the string if placeholders are used. Unlike Python, C's printf doesn't automatically add a newline at the end. Therefore adding \n at the end is usually needed.
Out of loops, while is based on repetition through checking a condition - the code block inside the loop is repeated until the loop's condition is false. The condition is defined similarly to conditional statements, e.g. while (sum < 21).