Interrupts Example Program in Linux Kernel – Linux Device Driver Tutorial – Part 13

This article is a continuation of the Series on Linux Device Driver and carries the discussion on Linux device drivers and their implementation. The aim of this series is to provide easy and practical examples that anyone can understand.

In our previous tutorial, we have seen the What is an Interrupt in the Linux kernel. Now, let’s write an interrupt program. This is the Interrupt Example Program in Linux kernel driver – Linux Device Driver Tutorial Part 13.

Interrupt Example Program in Linux Kernel

Before writing any interrupt program, you should keep the following points in mind.

  1. Interrupt handlers can not enter sleep, so to avoid calls to some functions which has sleep.
  2. When the interrupt handler has part of the code to enter the critical section, use spinlocks lock, rather than mutexes. Because if it can’t take mutex it will go to sleep until it takes the mute.
  3. Interrupt handlers can not exchange data with the userspace.
  4. The interrupt handlers must be executed as soon as possible. To ensure this, it is best to split the implementation into two parts, the top half and the bottom half. The top half of the handler will get the job done as soon as possible and then work late on the bottom half, which can be done with softirq or tasklet or workqueue.
  5. Interrupt handlers can not be called repeatedly. When a handler is already executing, its corresponding IRQ must be disabled until the handler is done.
  6. Interrupt handlers can be interrupted by higher authority handlers. If you want to avoid being interrupted by a highly qualified handler, you can mark the interrupt handler as a fast handler. However, if too many are marked as fast handlers, the performance of the system will be degraded because the interrupt latency will be longer.

Before programming, we should know the basic functions which are useful for interrupts. This table explains the usage of all functions.

FunctionDescription
request_irq
(
unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id)
Register an IRQ, the parameters are as follows:

irq: IRQ number to allocate.

handler: This is Interrupt handler function.This function will be invoked whenever the operating system receives the interrupt.The data type of return is irq_handler_t, if its return value is IRQ_HANDLED, it indicates that the processing is completed successfully, but if the return value is IRQ_NONE, the processing fails.

flags: can be either zero or a bit mask of one or more of the flags defined in linux/interrupt.h. The most important of these flags are:
IRQF_DISABLED
IRQF_SAMPLE_RANDOM
IRQF_SHARED
IRQF_TIMER
(Explained after this table)

name: Used to identify the device name using this IRQ, for example, cat / proc / interrupts will list the IRQ number and device name.

dev_id: IRQ shared by many devices. When an interrupt handler is freed, dev provides a unique cookie to enable the removal of only the desired interrupt handler from the interrupt line. Without this parameter, it would be impossible for the kernel to know which handler to remove on a given interrupt line. You can pass NULL here if the line is not shared, but you must pass a unique cookie if your interrupt line is shared. This pointer is also passed into the interrupt handler on each invocation. A common practice is to pass the driver’s device structure. This pointer is unique and might be useful to have within the handlers.

Return
returns zero on success and nonzero value indicates an error.

request_irq() cannot be called from interrupt context (other situations where code cannot block), because it can block.
free_irq(
unsigned int irq,
void *dev_id)
Release an IRQ registered by request_irq() with the following parameters:

irq: IRQ number.
dev_id: is the last parameter of request_irq.

If the specified interrupt line is not shared, this function removes the handler and disables the line.
If the interrupt line is shared, the handler identified via dev_id is removed, but the interrupt line is disabled only when the last handler is removed. With shared interrupt lines, a unique cookie is required to differentiate between the multiple handlers that can exist on a single line and enable free_irq() to remove only the correct handler.
In either case (shared or unshared), if dev_id is non-NULL, it must match the desired handler. A call to free_irq() must be made from process context.
enable_irq(unsigned int irq)Re-enable interrupt disabled by disable_irq or disable_irq_nosync.
disable_irq(unsigned int irq)Disable an IRQ from issuing an interrupt.
disable_irq_nosync(unsigned int irq)Disable an IRQ from issuing an interrupt, but wait until there is an interrupt handler being executed.
in_irq()returns true when in interrupt handler
in_interrupt()returns true when in interrupt handler or bottom half

Interrupts Flags:

These are the second parameters of the function. It has several flags. Here I explained important flags.

  • IRQF_DISABLED:
    • When set, this flag instructs the kernel to disable all interrupts when executing this interrupt handler.When unset, interrupt handlers run with all interrupts except their own enabled.

    Most interrupt handlers do not set this flag, as disabling all interrupts is bad form. Its use is reserved for performance-sensitive interrupts that execute quickly. This flag is the current manifestation of the SA_INTERRUPT flag, which in the past distinguished between “fast” and “slow” interrupts.

  • IRQF_SAMPLE_RANDOM: This flag specifies that interrupts generated by this device should contribute to the kernel entropy pool. The kernel entropy pool provides truly random numbers derived from various random events. If this flag is specified, the timing of interrupts from this device is fed to the pool as entropy. Do not set this if your device issues interrupt at a predictable rate (e.g. the system timer) or can be influenced by external attackers (e.g. a networking device). On the other hand, most other hardware generates an interrupt at non-deterministic times and is, therefore, a good source of entropy.
  • IRQF_TIMER: This flag specifies that this handler process interrupts the system timer.
  • IRQF_SHARED: This flag specifies that the interrupt line can be shared among multiple interrupt handlers. Each handler registered on a given line must specify this flag; otherwise, only one handler can exist per line.

Registering an Interrupt Handler

#define IRQ_NO 11

if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))) {
            printk(KERN_INFO "my_device: cannot register IRQ ");
            goto irq;
}

Freeing an Interrupt Handler

free_irq(IRQ_NO,(void *)(irq_handler));

Interrupt Handler

static irqreturn_t irq_handler(int irq,void *dev_id) {
  printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
  return IRQ_HANDLED;
}

Interrupt Example Program in Linux Kernel – Programming

The interrupt can be coming from anywhere (any hardware) and anytime. In our tutorial, we are not going to use any hardware. Here, instead of using hardware, we are going to trigger interrupt by simulating.

If you have only a PC (without hardware), but you want to play around with Interrupts in Linux, you can follow our method. If you have the hardware, then I would request not to use this method.

Instead, you can use the hardware interrupts. We have covered the hardware interrupt in this tutorial.

Triggering Hardware Interrupt through Software

Intel processors handle interrupts using IDT (Interrupt Descriptor Table). The IDT consists of 256 entries, with each entry corresponding to a vector, and of 8 bytes. All the entries are a pointer to the interrupt handling function. The CPU uses IDTR to point to IDT. The relation between those two can be depicted as below,

Triggering Hardware Interrupt through Software

An interrupt can be programmatically raised using ‘int’ instruction. For example, the Linux system call was using int $0x80.

In Linux IRQ to vector, mapping is done in arch/x86/include/asm/irq_vectors.h. The used vector range is as follows,

Linux IRQ to vector mapping

The IRQ0 is mapped to vector using the macro,

#define IRQ0_VECTOR (FIRST_EXTERNAL_VECTOR + 0x10)

where, FIRST_EXTERNAL_VECTOR = 0x20 (32 in decimal)

So if we want to raise an interrupt IRQ11, programmatically we have to add 11 to a vector of IRQ0.

0x20 + 0x10 + 11 = 0x3B (59 in Decimal).

Hence, executing “asm("int $0x3B")“, will raise interrupt IRQ 11.

The instruction will be executed while reading the device file of our driver (/dev/etx_device).

Driver Source Code

Here I took the old source code from the sysfs tutorial. In that source, I have just added interrupt code like request_irq, free_irq along with the interrupt handler.

In this program, the interrupt will be triggered whenever we are reading the device file of our driver (/dev/etx_device).

Whenever Interrupt triggers, it will print the “Shared IRQ: Interrupt Occurred” Text.

[Get the source from the GitHub]

/***************************************************************************//**
*  \file       driver.c
*
*  \details    Interrupt Example
*
*  \author     EmbeTronicX
*
*******************************************************************************/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include<linux/slab.h>                 //kmalloc()
#include<linux/uaccess.h>              //copy_to/from_user()
#include<linux/sysfs.h> 
#include<linux/kobject.h> 
#include <linux/interrupt.h>
#include <asm/io.h>
#include <linux/err.h>
#define IRQ_NO 11

//Interrupt handler for IRQ 11. 
static irqreturn_t irq_handler(int irq,void *dev_id) {
  printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
  return IRQ_HANDLED;
}

volatile int etx_value = 0;
 
dev_t dev = 0;
static struct class *dev_class;
static struct cdev etx_cdev;
struct kobject *kobj_ref;
 
static int __init etx_driver_init(void);
static void __exit etx_driver_exit(void);

/*************** Driver Fuctions **********************/
static int etx_open(struct inode *inode, struct file *file);
static int etx_release(struct inode *inode, struct file *file);
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len,loff_t * off);
static ssize_t etx_write(struct file *filp, 
                const char *buf, size_t len, loff_t * off);

/*************** Sysfs Fuctions **********************/
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf);
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count);

struct kobj_attribute etx_attr = __ATTR(etx_value, 0660, sysfs_show, sysfs_store);
 
static struct file_operations fops =
{
        .owner          = THIS_MODULE,
        .read           = etx_read,
        .write          = etx_write,
        .open           = etx_open,
        .release        = etx_release,
};
 
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf)
{
        printk(KERN_INFO "Sysfs - Read!!!\n");
        return sprintf(buf, "%d", etx_value);
}

static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count)
{
        printk(KERN_INFO "Sysfs - Write!!!\n");
        sscanf(buf,"%d",&etx_value);
        return count;
}

static int etx_open(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Opened...!!!\n");
        return 0;
}
 
static int etx_release(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Closed...!!!\n");
        return 0;
}
 
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Read function\n");
        asm("int $0x3B");  // Corresponding to irq 11
        return 0;
}

static ssize_t etx_write(struct file *filp, 
                const char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Write Function\n");
        return len;
}
 
static int __init etx_driver_init(void)
{
        /*Allocating Major number*/
        if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){
                printk(KERN_INFO "Cannot allocate major number\n");
                return -1;
        }
        printk(KERN_INFO "Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
 
        /*Creating cdev structure*/
        cdev_init(&etx_cdev,&fops);
 
        /*Adding character device to the system*/
        if((cdev_add(&etx_cdev,dev,1)) < 0){
            printk(KERN_INFO "Cannot add the device to the system\n");
            goto r_class;
        }
 
        /*Creating struct class*/
        if(IS_ERR(dev_class = class_create(THIS_MODULE,"etx_class"))){
            printk(KERN_INFO "Cannot create the struct class\n");
            goto r_class;
        }
 
        /*Creating device*/
        if(IS_ERR(device_create(dev_class,NULL,dev,NULL,"etx_device"))){
            printk(KERN_INFO "Cannot create the Device 1\n");
            goto r_device;
        }
 
        /*Creating a directory in /sys/kernel/ */
        kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj);
 
        /*Creating sysfs file for etx_value*/
        if(sysfs_create_file(kobj_ref,&etx_attr.attr)){
                printk(KERN_INFO"Cannot create sysfs file......\n");
                goto r_sysfs;
        }
        if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))) {
            printk(KERN_INFO "my_device: cannot register IRQ ");
                    goto irq;
        }
        printk(KERN_INFO "Device Driver Insert...Done!!!\n");
    return 0;

irq:
        free_irq(IRQ_NO,(void *)(irq_handler));

r_sysfs:
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
 
r_device:
        class_destroy(dev_class);
r_class:
        unregister_chrdev_region(dev,1);
        cdev_del(&etx_cdev);
        return -1;
}
 
static void __exit etx_driver_exit(void)
{
        free_irq(IRQ_NO,(void *)(irq_handler));
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
        device_destroy(dev_class,dev);
        class_destroy(dev_class);
        cdev_del(&etx_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_INFO "Device Driver Remove...Done!!!\n");
}
 
module_init(etx_driver_init);
module_exit(etx_driver_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <[email protected]>");
MODULE_DESCRIPTION("A simple device driver - Interrupts");
MODULE_VERSION("1.9");

MakeFile:

obj-m += driver.o
 
KDIR = /lib/modules/$(shell uname -r)/build
 
all:
    make -C $(KDIR)  M=$(shell pwd) modules
 
clean:
    make -C $(KDIR)  M=$(shell pwd) clean

Building and Testing Driver

  • Build the driver by using Makefile (sudo make)
  • Load the driver using sudo insmod driver.ko
  • To trigger the interrupt read device file (sudo cat /dev/etx_device)
  • Now see the Dmesg (dmesg)
linux@embetronicx-VirtualBox: dmesg

[19743.366386] Major = 246 Minor = 0
[19743.370707] Device Driver Insert...Done!!!
[19745.580487] Device File Opened...!!!
[19745.580507] Read function
[19745.580531] Shared IRQ: Interrupt Occurred
[19745.580540] Device File Closed...!!!
  • We can able to see the print “Shared IRQ: Interrupt Occurred
  • Unload the module using sudo rmmod driver

Problem in Linux kernel [Solution]

If you are using the newer Linux kernel, then this may not work properly. You may get something like below.

do_IRQ: 1.59 No irq handler for vector

In order to solve that, you have to change the Linux kernel source code, Compile it, then install it.

Note: The complete process will take more than an hour and you need to download the Linux kernel source also. 

Modify and Build the Linux Kernel

Step 1: Previously, I have used an old kernel. Now I am updating the kernel to 5.4.47 with Ubuntu 18.04 (Virtualbox). First, you need to download the Linux kernel source code using the below command.

wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.4.47.tar.xz

Step 2: Once you have downloaded the kernel source, then extract it using the below command.

sudo tar -xvf ../linux-5.4.47.tar

Step 3: Get into the directory and copy the config.

cd linux-5.4.47/
cp -v /boot/config-$(uname -r) .confi

Step 4: Let’s install the required tools to compile the source code.

sudo apt install build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache flex libelf-dev bison libncurses-dev

Step 5: Now we have the source code and tools that are needed to compile. Let’s do our modification. Add the below line in the downloaded Linux kernel file arch/x86/kernel/irq.c right after all the included lines.

EXPORT_SYMBOL (vector_irq);

Step 6: Now build the config using the below commands.

make oldconfig
make menuconfig

Step 7: Let’s start compiling the kernel using the below command.

sudo make

If you want to speed up the compilation time, just use like below.

sudo make -j 4

You have to have more patience as a compilation takes more time. The build time depends upon your system’s resources such as the available CPU core and the current system load. For me, it took more than 2 hours as I was building on Virtualbox.

Install the Modified kernel

Step 8: Enter into the admin mode and Install the kernel modules.

sudo su
make modules_install

Step 9: Install the modified Linux kernel using the below command.

sudo make install

Step 10: Update the grub config using the below commands.

sudo update-initramfs -c -k 5.4.47
sudo update-grub

Finally, Here we are. We have installed the new kernel. In order to reflect the changes, reboot it. Then check the kernel version.

reboot

uname-r

You should see the updated kernel version if there is no issues in compilation and installation like below.

owl@owl-VirtualBox:~/Desktop/LDD$ uname -r
5.4.47

If you have any doubts, please refer.

Driver source code for a modified kernel

We have customized the kernel. Let’s take the below source code and try it.

[Get the source from the GitHub]

/***************************************************************************//**
*  \file       driver.c
*
*  \details    Interrupt Example
*
*  \author     EmbeTronicX
*
*  \Tested with kernel 5.4.47
*
*******************************************************************************/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include<linux/slab.h>                 //kmalloc()
#include<linux/uaccess.h>              //copy_to/from_user()
#include<linux/sysfs.h> 
#include<linux/kobject.h> 
#include <linux/interrupt.h>
#include <asm/io.h>
#include <asm/hw_irq.h>
#include <linux/err.h>
#define IRQ_NO 11
 
//Interrupt handler for IRQ 11. 
static irqreturn_t irq_handler(int irq,void *dev_id) {
  printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
  return IRQ_HANDLED;
}
 
volatile int etx_value = 0;
 
 
dev_t dev = 0;
static struct class *dev_class;
static struct cdev etx_cdev;
struct kobject *kobj_ref;
 
static int __init etx_driver_init(void);
static void __exit etx_driver_exit(void);
 
/*************** Driver Fuctions **********************/
static int etx_open(struct inode *inode, struct file *file);
static int etx_release(struct inode *inode, struct file *file);
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len,loff_t * off);
static ssize_t etx_write(struct file *filp, 
                const char *buf, size_t len, loff_t * off);
 
/*************** Sysfs Fuctions **********************/
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf);
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count);
 
struct kobj_attribute etx_attr = __ATTR(etx_value, 0660, sysfs_show, sysfs_store);
 
static struct file_operations fops =
{
        .owner          = THIS_MODULE,
        .read           = etx_read,
        .write          = etx_write,
        .open           = etx_open,
        .release        = etx_release,
};
 
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf)
{
        printk(KERN_INFO "Sysfs - Read!!!\n");
        return sprintf(buf, "%d", etx_value);
}
 
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count)
{
        printk(KERN_INFO "Sysfs - Write!!!\n");
        sscanf(buf,"%d",&etx_value);
        return count;
}
 
static int etx_open(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Opened...!!!\n");
        return 0;
}
 
static int etx_release(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Closed...!!!\n");
        return 0;
}
 
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len, loff_t *off)
{
        struct irq_desc *desc;

        printk(KERN_INFO "Read function\n");
        desc = irq_to_desc(11);
        if (!desc) 
        {
            return -EINVAL;
        }
        __this_cpu_write(vector_irq[59], desc);
        asm("int $0x3B");  // Corresponding to irq 11
        return 0;
}

static ssize_t etx_write(struct file *filp, 
                const char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Write Function\n");
        return len;
}
 
static int __init etx_driver_init(void)
{
        /*Allocating Major number*/
        if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){
                printk(KERN_INFO "Cannot allocate major number\n");
                return -1;
        }
        printk(KERN_INFO "Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
 
        /*Creating cdev structure*/
        cdev_init(&etx_cdev,&fops);
 
        /*Adding character device to the system*/
        if((cdev_add(&etx_cdev,dev,1)) < 0){
            printk(KERN_INFO "Cannot add the device to the system\n");
            goto r_class;
        }
 
        /*Creating struct class*/
        if(IS_ERR(dev_class = class_create(THIS_MODULE,"etx_class"))){
            printk(KERN_INFO "Cannot create the struct class\n");
            goto r_class;
        }
 
        /*Creating device*/
        if(IS_ERR(device_create(dev_class,NULL,dev,NULL,"etx_device"))){
            printk(KERN_INFO "Cannot create the Device 1\n");
            goto r_device;
        }
 
        /*Creating a directory in /sys/kernel/ */
        kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj);
 
        /*Creating sysfs file for etx_value*/
        if(sysfs_create_file(kobj_ref,&etx_attr.attr)){
                printk(KERN_INFO"Cannot create sysfs file......\n");
                goto r_sysfs;
        }
        if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))) {
            printk(KERN_INFO "my_device: cannot register IRQ ");
                    goto irq;
        }
        printk(KERN_INFO "Device Driver Insert...Done!!!\n");
    return 0;
 
irq:
        free_irq(IRQ_NO,(void *)(irq_handler));
 
r_sysfs:
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
 
r_device:
        class_destroy(dev_class);
r_class:
        unregister_chrdev_region(dev,1);
        cdev_del(&etx_cdev);
        return -1;
}
 
static void __exit etx_driver_exit(void)
{
        free_irq(IRQ_NO,(void *)(irq_handler));
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
        device_destroy(dev_class,dev);
        class_destroy(dev_class);
        cdev_del(&etx_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_INFO "Device Driver Remove...Done!!!\n");
}
 
module_init(etx_driver_init);
module_exit(etx_driver_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <[email protected]>");
MODULE_DESCRIPTION("A simple device driver - Interrupts");
MODULE_VERSION("1.9");

This is a simple example of using Interrupts in the device drivers. This is just basic. You can also try using hardware. I hope this might help you.

In our next tutorial, we will discuss one of the bottom half, which is workqueue. You can also look at the GPIO interrupt tutorial.

Please find the other Linux device driver tutorials here.

You can also read the below tutorials.

Linux Device Driver TutorialsC Programming Tutorials
FreeRTOS TutorialsNuttX RTOS Tutorials
RTX RTOS TutorialsInterrupts Basics
I2C Protocol – Part 1 (Basics)I2C Protocol – Part 2 (Advanced Topics)
STM32 TutorialsLPC2148 (ARM7) Tutorials
PIC16F877A Tutorials8051 Tutorials
Unit Testing in C TutorialsESP32-IDF Tutorials
Raspberry Pi TutorialsEmbedded Interview Topics
Reset Sequence in ARM Cortex-M4BLE Basics
VIC and NVIC in ARMSPI – Serial Peripheral Interface Protocol
STM32F7 Bootloader TutorialsRaspberry PI Pico Tutorials
STM32F103 Bootloader TutorialsRT-Thread RTOS Tutorials
Zephyr RTOS Tutorials – STM32Zephyr RTOS Tutorials – ESP32
AUTOSAR TutorialsUDS Protocol Tutorials
Product ReviewsSTM32 MikroC Bootloader Tutorial
VHDL Tutorials
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

35 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Table of Contents