This shows you the differences between two versions of the page.
eap:laboratoare:02 [2024/07/17 16:06] jan.vaduva [Interrupts] |
eap:laboratoare:02 [2024/07/18 10:08] (current) jan.vaduva |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== RP2040 internals ===== | + | ===== RP2040 internals: UART, interrupts & timers ===== |
==== UART ==== | ==== UART ==== | ||
- | The RP2040 microcontroller from Raspberry Pi offers two built-in UART peripherals for serial communication. Here's a basic introduction to using UART with Pico-SDK, including configuration, sending, and receiving data: | + | The RP2040 microcontroller from Raspberry Pi offers two built-in UART peripherals for serial communication. UART (Universal Asynchronous Receiver Transmitter) facilitates serial communication between the RP2040 and other devices like computers or sensors. |
+ | |||
+ | {{ :eap:laboratoare:uart-comm.png?nolink&600 | dsa}} | ||
+ | |||
+ | Data is transmitted one bit at a time, without a dedicated clock signal. Each transmitted character (frame) includes timing information to ensure proper reception. Data bits are transmitted sequentially, one after another, over a single wire (typically with a ground wire for reference). | ||
+ | |||
+ | For transmission the data is prepared in your program as a byte (8-bit) array. The data is sent bit by bit through the TX (Transmit) pin of the RP2040. | ||
+ | Additional control bits (Start bit and Stop bits) are added to the data stream for synchronization. The baud rate determines the speed at which bits are transmitted (e.g., 9600 baud signifies 9600 bits per second). | ||
+ | |||
+ | Similarly for reception The RP2040 receives data bit by bit through the RX (Receive) pin. Start and Stop bits are used to identify the beginning and end of a character. The received data bits are assembled back into a byte. The received byte is stored in the UART's receive buffer, and an interrupt can be triggered if enabled. | ||
+ | |||
+ | {{ :eap:laboratoare:uart-frame.png?nolink&800 | dsa}} | ||
+ | |||
+ | Each transmitted/received character (byte) is encapsulated in a frame structure: | ||
+ | * Start bit: A single "0" bit signifying the beginning of a character. | ||
+ | * Data bits (8): The actual data you want to transmit (e.g., ASCII code for a character). | ||
+ | * Parity bit (optional): Used for error checking (can be configured as Even, Odd, or None). | ||
+ | * Stop bits (1 or 2): One or two "1" bits indicating the end of a character. | ||
+ | |||
+ | The time it takes to transmit a character depends on the baud rate. Transmission time (seconds) = Number of bits in frame / Baud rate (bits/second) | ||
+ | Example: | ||
+ | <code> | ||
+ | Baud rate = 9600 baud | ||
+ | Frame size (Start + 8 data bits + 1 stop bit) = 10 bits | ||
+ | Transmission time = 10 bits / 9600 baud ≈ 0.001042 seconds | ||
+ | </code> | ||
+ | |||
+ | Here's a basic introduction to using UART with Pico-SDK, including configuration, sending, and receiving data: | ||
* Include necessary libraries: In your C/C++ code, include the hardware/uart.h header from Pico-SDK: | * Include necessary libraries: In your C/C++ code, include the hardware/uart.h header from Pico-SDK: | ||
<code> | <code> | ||
Line 9: | Line 36: | ||
* Initialize UART: Use the **uart_init** function to configure the desired UART peripheral (e.g., uart0 or uart1) and set parameters like baud rate, parity, and stop bits: | * Initialize UART: Use the **uart_init** function to configure the desired UART peripheral (e.g., uart0 or uart1) and set parameters like baud rate, parity, and stop bits: | ||
<code> | <code> | ||
- | // Example: Configure UART0 at 115200 baud, 8N1 format | + | // Configure UART0 at 115200 baud, 8N1 format |
int baudrate = 115200; | int baudrate = 115200; | ||
- | uart_init(uart0, baudrate, UART_PARITY_NONE, UART_STOP_BITS_1); | + | uart_init(uart0, baudrate); |
+ | //uart_set_format(uart, 8, 1, UART_PARITY_NONE); | ||
+ | |||
+ | // Set the TX and RX pins by using the function select on the GPIO 0 and 1 | ||
+ | gpio_set_function(0, GPIO_FUNC_UART); | ||
+ | gpio_set_function(1, GPIO_FUNC_UART); | ||
</code> | </code> | ||
* Sending data: | * Sending data: | ||
Line 19: | Line 51: | ||
const char* message = "Hello from RP2040!\n"; | const char* message = "Hello from RP2040!\n"; | ||
- | // Example 1: Sending a string with uart_puts | + | // Sending a string with uart_puts |
uart_puts(uart0, message); | uart_puts(uart0, message); | ||
- | // Example 2: Sending raw data with uart_write_blocking | + | // Sending raw data with uart_write_blocking |
uint8_t data[] = {0x41, 0x42, 0x43}; // Example data bytes | uint8_t data[] = {0x41, 0x42, 0x43}; // Example data bytes | ||
int written = uart_write_blocking(uart0, data, sizeof(data)); | int written = uart_write_blocking(uart0, data, sizeof(data)); | ||
Line 32: | Line 64: | ||
int received_char; | int received_char; | ||
- | // Example 1: Reading a single character | + | // Reading a single character |
while (uart_is_readable(uart0)) { | while (uart_is_readable(uart0)) { | ||
received_char = uart_getc(uart0); | received_char = uart_getc(uart0); | ||
Line 38: | Line 70: | ||
} | } | ||
- | // Example 2: Reading data into a buffer with a timeout | + | // Reading data into a buffer with a timeout |
uint8_t buffer[10]; | uint8_t buffer[10]; | ||
int num_read = uart_read_blocking(uart0, buffer, sizeof(buffer), 100); // Wait 100ms for data | int num_read = uart_read_blocking(uart0, buffer, sizeof(buffer), 100); // Wait 100ms for data | ||
Line 45: | Line 77: | ||
} | } | ||
</code> | </code> | ||
+ | |||
+ | The RP2040's UART peripherals can generate interrupts for various events, such as: | ||
+ | * Transmission complete: Interrupt occurs when a character is completely transmitted. | ||
+ | * Character received: Interrupt triggers when a character is received from the connected device. | ||
+ | * Receive buffer full/empty: Interrupts can be set when the receive buffer reaches a certain level (full or empty) to signal data availability or lack thereof. | ||
+ | |||
+ | You can define interrupt handler functions to handle these events. For example, a receive interrupt handler could read the received character and process it in your program. | ||
==== Interrupts ==== | ==== Interrupts ==== | ||
Line 53: | Line 92: | ||
When an interrupt request happens the first thing that the processor does is to memorize its current state. For ARM Cortex-M0 this happens by pushing 8 words or registered data into the main stack to provide the information need to return the processor to what it was doing before before the interrupt request was called. This part is called the stack frame and it includes registers 0 through 3, register 12, the link register, the program counter and the program status register. | When an interrupt request happens the first thing that the processor does is to memorize its current state. For ARM Cortex-M0 this happens by pushing 8 words or registered data into the main stack to provide the information need to return the processor to what it was doing before before the interrupt request was called. This part is called the stack frame and it includes registers 0 through 3, register 12, the link register, the program counter and the program status register. | ||
+ | |||
+ | {{ :eap:laboratoare:stack_frame-0dc91615c2f2f0409a3df507e0086f49.png?nolink&600 | dsa}} | ||
ARM Cortex-M microcontrollers also use a Nested Vectored Interrupt Controller (NVIC). The NVIC is specifically designed to handle these interrupts more efficiently. Interrupt addresses in the NVIC memory region are set according to their priority: the lower the address, the higher the priority. As suggested by the "Nested" in its name, the NVIC supports nested interrupts. This means that if a higher priority interrupt occurs while another interrupt is being processed, the controller can pause the current interrupt service routine (ISR), handle the higher priority interrupt, and then resume the interrupted ISR. This feature is crucial for responsive and real-time processing. | ARM Cortex-M microcontrollers also use a Nested Vectored Interrupt Controller (NVIC). The NVIC is specifically designed to handle these interrupts more efficiently. Interrupt addresses in the NVIC memory region are set according to their priority: the lower the address, the higher the priority. As suggested by the "Nested" in its name, the NVIC supports nested interrupts. This means that if a higher priority interrupt occurs while another interrupt is being processed, the controller can pause the current interrupt service routine (ISR), handle the higher priority interrupt, and then resume the interrupted ISR. This feature is crucial for responsive and real-time processing. | ||
+ | |||
+ | The RP2040 boots from an internal bootloader that sets the initial interrupt IVT. The before starting the actual code written into Flash, the internal bootloader loads a secondary bootloader that is written in Flash (the first 256 bytes) together with the developer's app. The IVT that the Flash application provides start after the secondary bootloader, at address 0x100. | ||
+ | |||
+ | {{ :eap:laboratoare:flash-address.png?nolink&400 | dsa}} | ||
+ | |||
+ | The RP2040 chip has two cores (processors), and each core has its own NVIC. Each core's NVIC is connected to the same set of hardware interrupt lines with one exception: IO Interrupts. In the RP2040, IO interrupts are organized by banks, and each core has its own set of IO interrupts for each bank. The IO interrupts for each core are completely independent. For instance, Processor 0 (Core 0) can be interrupted by an event on GPIO pin 0 in bank 0, while Processor 1 (Core 1) can be interrupted by a separate event on GPIO pin 1 in the same bank. Each processor responds only to its own interrupts, allowing them to operate independently or to handle different tasks simultaneously without interfering with each other. | ||
+ | |||
+ | {{ :eap:laboratoare:rpsubsystem-8fa22f1ea9fa8ec9021a165ee2640150.png?nolink&600 | dsa}} | ||
+ | |||
+ | On RP2040, only the lower 26 IRQ signals are connected on the NVIC, as seen in the table below, and IRQs 26 to 31 are tied to zero (never firing). The core can still be forced to enter the relevant interrupt handler by writing bits 26 to 31 in the NVIC ISPR register. | ||
+ | |||
+ | {{ :eap:laboratoare:irqrp2040-ae016aa4bd46728ca373d2658729e24d.png?nolink&600 | dsa}} | ||
+ | |||
+ | The priority order is determined for these signals is determined by : | ||
+ | * First, the dynamic priority level configured per interrupt by the NVIC_IPR0-7 registers. The Cortex-M0+ implements the two most significant bits of an 8-bit priority field, so four priority levels are available, and the numerically-lowest level (level 0) is the highest priority. | ||
+ | * Second, for interrupts with the same dynamic priority level, the lower-numbered IRQ has higher priority (using the IRQ numbers given in the table above) | ||
+ | |||
+ | All GPIO pins in Raspberry Pi Pico support interrupts. The interrupts can be classified into three types: | ||
+ | * Level High: An interrupt occurs while a pin is HIGH or at logic 1. | ||
+ | * Level Low: An interrupt occurs while a pin is LOW or at logic 0. | ||
+ | * Rising Edge: Interrupt occurs when a pin transitions from a LOW to HIGH. | ||
+ | * Falling Edge: Interrupt occurs when a pin transitions from HIGH to LOW. | ||
+ | |||
+ | Here's a basic introduction to using interrupts on RP2040 with Pico-SDK, including setup, trigger sources, and handler functions. | ||
+ | * Include necessary libraries: In your C/C++ code, include the hardware/irq.h header from Pico-SDK: | ||
+ | |||
+ | <code> | ||
+ | #include <hardware/irq.h> | ||
+ | </code> | ||
+ | * Globally enable interrupts using the **irq_set_enabled(true)** function. This allows the RP2040 to recognize and respond to interrupt requests. Various hardware events can trigger interrupts, like changes on GPIO pins, timers expiring, or DMA transfers completing. | ||
+ | <code> | ||
+ | // Set up a RX interrupt | ||
+ | int UART_IRQ = UART_ID == uart0 ? UART0_IRQ : UART1_IRQ; | ||
+ | |||
+ | // And set up and enable the interrupt handlers | ||
+ | irq_set_exclusive_handler(UART_IRQ, on_uart_rx); | ||
+ | irq_set_enabled(UART_IRQ, true); | ||
+ | </code> | ||
+ | * Define a function to handle the interrupt. This function is called whenever the configured trigger source (e.g., the GPIO pin change) occurs. The handler function can perform actions like reading the GPIO pin state, triggering other events, or notifying other parts of your program about the interrupt. | ||
+ | <code> | ||
+ | // RX interrupt handler | ||
+ | void on_uart_rx() { | ||
+ | // Perform action based on the interrupt | ||
+ | | ||
+ | // Clear the interrupt flag (important) | ||
+ | } | ||
+ | </code> | ||
+ | |||
==== Timers ==== | ==== Timers ==== | ||
+ | Timers are hardware components that generate periodic interrupts or control signals at defined intervals. Timers offer a wide range of functionalities, including: | ||
+ | * **Generating delays**: Create precise timing delays in your program without busy waiting. | ||
+ | * **Implementing periodic tasks**: Execute code sections at regular intervals (e.g., blinking an LED, reading sensor data). | ||
+ | * **Pulse Width Modulation (PWM)**: Generate square waves with varying pulse widths for controlling motors, LEDs with adjustable brightness, etc. | ||
+ | The system timer is intended to provide a global timebase for software. RP2040 has a number of other programmable counter resources which can provide regular interrupts, or trigger DMA transfers. | ||
+ | * The PWM contains 8× 16-bit programmable counters, which run at up to system speed, can generate interrupts, and can be continuously reprogrammed via the DMA, or trigger DMA transfers to other peripherals. | ||
+ | * 8× PIO state machines can count 32-bit values at system speed, and generate interrupts. | ||
+ | * The DMA has four internal pacing timers, which trigger transfers at regular intervals. | ||
+ | * Each Cortex-M0+ core has a standard 24-bit SysTick timer, counting either the microsecond tick or the system clock. | ||
+ | |||
+ | The timer has a 64-bit counter, but RP2040 only has a 32-bit data bus. This means that the TIME value is accessed | ||
+ | through a pair of registers. These are: | ||
+ | * **TIMEHW** and **TIMELW** to write the time | ||
+ | * **TIMEHR** and **TIMELR** to read the time | ||
+ | These pairs are used by accessing the lower register, L, followed by the higher register, H. In the read case, reading the L register latches the value in the H register so that an accurate time can be read. Alternatively, **TIMERAWH** and **TIMERAWL** can be used to read the raw time without any latching. | ||
+ | |||
+ | The timer has 4 alarms, and outputs a separate interrupt for each alarm. The alarms match on the lower 32 bits of the | ||
+ | 64-bit counter which means they can be fired at a maximum of 232 microseconds into the future. This is equivalent to: | ||
+ | * 232 ÷ 106: ~4295 seconds | ||
+ | * 4295 ÷ 60: ~72 minutes | ||
+ | |||
+ | To enable an alarm: | ||
+ | * Enable the interrupt at the timer with a write to the appropriate alarm bit in INTE: i.e. (1 << 0) for ALARM0 | ||
+ | * Enable the appropriate timer interrupt at the processor (see Section 2.3.2) | ||
+ | * Write the time you would like the interrupt to fire to ALARM0 (i.e. the current value in TIMERAWL plus your desired alarm time in microseconds). Writing the time to the ALARM register sets the ARMED bit as a side effect. | ||
+ | Once the alarm has fired, the ARMED bit will be set to 0. To clear the latched interrupt, write a 1 to the appropriate bit in | ||
+ | INTR. | ||
+ | |||
+ | For using timers on RP2040 with Pico-SDK include the hardware/timer.h header from Pico-SDK: | ||
+ | |||
+ | <code> | ||
+ | #include <hardware/timer.h> | ||
+ | </code> | ||
==== Exercises ==== | ==== Exercises ==== | ||
- Write a program for a Raspberry Pi Pico to communicate with another device via UART. The program will set up the UART with desired settings, send a predefined message, receive incoming data, and process it (like printing or storing). | - Write a program for a Raspberry Pi Pico to communicate with another device via UART. The program will set up the UART with desired settings, send a predefined message, receive incoming data, and process it (like printing or storing). |