The last part of this series of Getting your Linux computer to communicate with a microcontroller is the Linux driver. Linux was introduced to me in my undergrad and I used it too look “cool”. However, my job at Zoho made it necessary for me to use it and ever since, I have loved using it. Linux is simple, elegant and importantly maker friendly.
According to Linux, everything is a file. It does not matter if it is a keyboard or a space telescope. Every operation that can be performed on a normal file can be performed on a device connected to a Linux computer too.
A device driver is an interface between the user and the device[1]. The basic operations are open, release, write and read. For example, file_operations.open defines the set of instructions to be carried out when a device file is opened. This instruction could contain the wake up sequence of a connected device, for instance.
As mentioned in the previous article, the STM32 micrcontroller pertinent to this article series acts as an I2C slave and receives the target frequency at which the LED should blink. So, the Linux computer should be an I2C master that sends the target frequency. Reception of the target frequency from the user and transmission of the same to the microcontroller is implemented in file_operations.write function. So whenever, something is written to the device file, it would be sanity checked and sent to the microcontroller through I2C bus. The .write function is provided as code snippet below.
ssize_t driver_write(struct file *filep, const char __user *buff, size_t count, loff_t *offp){
//printk("The following was written to the device : ");
const uint8_t max_input_count = 4; // 3 digits + 1 null character
char copied_data[max_input_count];
if(count > max_input_count)
count = max_input_count; // copy only first five characters from the userspace buffer
copy_from_user(copied_data, buff, count); // data from the userspace should not be dereferenced in the kernel space.
uint8_t frequency2send = 0;
for(int i=0; i<count; i++){
if(!(copied_data[i] >= '0' && copied_data[i] <= '9'))
break;
frequency2send = (10*frequency2send + (copied_data[i] - '0'));
}
int sent_data_count = i2c_master_send(arm_i2c_client , &frequency2send, 1);
if(sent_data_count == 1){
printk(KERN_INFO "Data communicated successfully through I2C, number of bytes sent %d and the data sent is %u \n", sent_data_count, frequency2send);
}
return count;
}
An important thing to keep in mind while working with kernel code is that the user space variables should not be accessed in kernel space. They have to be copied to a kernel space variable. It is a good practice to package the data as a structure for transmission. However, for the scope of this article, it is unnecessary since we are going to be sending just one byte of data. The data written to the device file is converted into a single byte of numerical value instead of being sent as multiple char bytes. The whole linux driver code can be found in the github repository of this project[2].
There are two ways to get the device driver installed in the Linux OS. It can either be built with the kernel or installed in an existing kernel. Former is used in the industry while shipping the consumer products. Latter is used in this article since this is more of an experiment. Linux makes it really easy to build a device driver. As a matter of fact, the build system is a part of the OS. The following Makefile will bake the code into a driver.
obj-m += src/simplelindriver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Upon running the make command, a kernel object file with .ko extension would be created. It still has not become a part of kernel yet. The created kernel object can be inserted using the insert module command insmod.
insmod simplelindriver.ko
Now that the module has been inserted, the major and minor device number could be found using the list module command lsmod. Based on that major number, a char device node could be created using the command mknod, with the format mknod $device_name $driver_type $major_number $minor_number. The created device file represents the physical device connected to the computer.
mknod /dev/stm32led c $major_number 0
And, our linux kernel module aka device driver is created and running.
Now the frequency at which the LED blinks could be controlled by writing to the device file using any program or application of your choice. For instance, a mere
echo 5 > /dev/stm32led
would make the LED in the microcontroller blink at 5 Hz. Or you can use python
with open('/dev/stm32led', 'w') as device_file:
device_file.write(5)
Or, if you are really crazy like some of my friends, you can use Java.
[1] Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman. 2005. Linux Device Drivers, 3rd Edition. O’Reilly Media, Inc.
[2] https://github.com/rocheparadox/simple-integrated-system