Making an Embedded Linux Kernel Device Driver

spitest_dut.JPG
I bought this nice little embedded Linux development board some months back. It's called Carambola and it's based on a MIPS SoC manufactured by Ralink and designated RT3050. The board has 8MB flash, 32MB ram, two ethenet ports, 802.11bgn wifi, USB host, 2x uart, 1x SPI, 1x I2C and some GPIO pins. The chipset is commonly used in small home routers and such, but it seems to work very nicely as a generic embedded linux development platform.

I wanted to do something with the SPI bus of the board, purely as a learning experience. More specifically, I wanted to write a kernel module that does something with the SPI bus. You can actually control the SPI bus from Linux user space as well, and quite easily at that, so why do I want to do it from inside the kernel? First of all, I've been using Linux as my desktop operating system for years, so I'm quite familiar with how Linux operates in the user space, but my experiences with operating in the kernel space have been a lot more limited. This would be a perfect time to learn. Also, if you want to make actual Linux device drivers for whatever you're interfacing with, those need to be done in the kernel.

In any case, for the first try I wanted to interface with something easy. I decided to interface a very simple shift register and some LEDs to the carambola board. The goal: make a larson scanner.

The hardware

The first thing I did was create the very simple shift-register-LED contraption.

Here's the schematic:
spitest_circuit.JPG
Not described in the schematic is that the nCLR signal is connected to +5v. SN74LS164N was chosen simply because it was the first chip I found in my parts bin that could do the job.

This is how it works: on the rising edge of SCK, the value on the MOSI line is shifted into the shift register. The LEDs show the value in the shift register. No chip select lines, no output enable lines, just clock and the data in line. Couldn't be simpler.

And here's the actual board:
spitest_shiftrleds.JPG

Development environment

To start developing kernel modules, an environment where we can compile the Linux kernel and modules for the MIPS architechture is required.

The Carambola runs OpenWRT by default and the developers of Carambola maintain a fork of OpenWRT buildroot environment on GitHub. There's a wiki page on Carambola wiki about installing the build environment. Essentially you clone the git repo, cd into it and the simple command "make" handles compiling the whole toolset required for compiling the kernel and then compiles the kernel. "make menuconfig" and "make kernel_menuconfig" allow you to decide what gets compiled into the firmware and into the kernel.

Unfortunately I started having problems right there. I got some compilation errors, " 'gets' undeclared here (not in a function) ". Long story short, I'm running a linux distro called Arch Linux, which is a rolling release bleeding edge distro. That means I usually have the latest versions of applications and libraries. The problem here relates to the latest glibc library. The applications m4 and bison that the buildroot tried to compile include a file "stdio.h" where the much-loathed function "gets" is undeclared to make sure no-one uses it. Only problem, the latest glibc doesn't even declare it anymore when compiling with GNU or C11 standards. So the header file tries to undeclare a function that is never declared in the first place.

I made a quick patch that fixes the issue by removing the undeclaration.

tools/m4/patches/001-gets-undeclared-fix.patch and tools/bison/patches/001-gets-undeclared-fix.patch

  1. --- a/lib/stdio.in.h 2012-07-16 22:08:08.841972806 +0300
  2. +++ b/lib/stdio.in.h 2012-07-16 22:11:02.111034127 +0300
  3. @@ -158,11 +158,6 @@
  4. "use gnulib module fflush for portable POSIX compliance");
  5. #endif
  6.  
  7. -/* It is very rare that the developer ever has full control of stdin,
  8. - so any use of gets warrants an unconditional warning. Assume it is
  9. - always declared, since it is required by C89. */
  10. -#undef gets
  11. -_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
  12.  
  13. #if @GNULIB_FOPEN@
  14. # if @REPLACE_FOPEN@

Then it compiled. It also took something like an hour on my 3-core machine. Not exactly fast.

The kernel device driver

After running "make" successfully, the folder "build-dir" is populated with the kernel source and the coding can commence.

Here's the driver source code. It's not exactly easy to understand and it probably could use some comments.

build_dir/linux-ramips_rt305x/linux-3.3.8/drivers/misc/spitest.c

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/init.h>
  4. #include <linux/workqueue.h>
  5. #include <linux/spi/spi.h>
  6.  
  7. struct spitest {
  8. struct delayed_work work;
  9. struct device *dev;
  10. struct spi_device *spidev;
  11. unsigned char increment;
  12. unsigned char direction;
  13. };
  14.  
  15. struct workqueue_struct *workqueue;
  16.  
  17. static void spitest_loop( struct delayed_work *ptr )
  18. {
  19. struct spitest *item;
  20. item = (struct spitest *)ptr;
  21.  
  22. if (item->increment == 0x80)
  23. item->direction = 'd';
  24. else if (item->increment == 1)
  25. item->direction = 'u';
  26.  
  27. if (item->direction == 'u')
  28. item->increment <<=1;
  29. else if (item->direction == 'd')
  30. item->increment >>=1;
  31.  
  32. spi_write(item->spidev,&(item->increment),1);
  33.  
  34. queue_delayed_work(workqueue,ptr,10);
  35. }
  36.  
  37. static int __init spitest_probe(struct spi_device *dev)
  38. {
  39. int ret = 0;
  40. struct spitest *item;
  41.  
  42. printk("Spitest probe started\n");
  43.  
  44. item = kzalloc(sizeof(struct spitest), GFP_KERNEL);
  45. if (!item) {
  46. dev_err(&dev->dev,
  47. "%s: unable to kzalloc for spitest\n", __func__);
  48. ret = -ENOMEM;
  49. goto out;
  50. }
  51.  
  52. item->spidev = dev;
  53. item->dev = &dev->dev;
  54. item->direction ='u';
  55. item->increment = 1;
  56. dev_set_drvdata(&dev->dev, item);
  57. dev_info(&dev->dev, "spi registered, item=0x%p\n", (void *)item);
  58.  
  59. INIT_DELAYED_WORK(&(item->work),spitest_loop);
  60. queue_delayed_work(workqueue,&(item->work),10);
  61.  
  62. out:
  63. return ret;
  64. }
  65.  
  66. static int spitest_remove(struct spi_device *spi)
  67. {
  68. /*should be implemented but I'm lazy and this is just a test */
  69. return 0;
  70. }
  71.  
  72. static int spitest_suspend(struct spi_device *spi, pm_message_t state)
  73. {
  74. /*should be implemented but I'm lazy and this is just a test */
  75. return 0;
  76. }
  77.  
  78. static int spitest_resume(struct spi_device *spi)
  79. {
  80. /*should be implemented but I'm lazy and this is just a test */
  81. return 0;
  82. }
  83.  
  84. static struct spi_driver spi_test_driver = {
  85. .driver = {
  86. .name = "spitest",
  87. .bus = &spi_bus_type,
  88. .owner = THIS_MODULE,
  89. },
  90. .probe = spitest_probe,
  91. .remove = spitest_remove,
  92. .suspend = spitest_suspend,
  93. .resume = spitest_resume,
  94. };
  95.  
  96. static int __init spitest_init( void )
  97. {
  98. int ret = 0;
  99.  
  100. printk("Spitest module installing\n");
  101.  
  102. workqueue = create_workqueue("Spitest queue");
  103. if (workqueue == NULL){
  104. pr_err("%s: unable to create workqueue\n", __func__);
  105. ret = -1;
  106. goto out;
  107. }
  108.  
  109.  
  110. ret = spi_register_driver(&spi_test_driver);
  111. if (ret)
  112. pr_err("%s: problem at spi_register_driver\n", __func__);
  113.  
  114. out:
  115. return ret;
  116. }
  117.  
  118. static void __exit spitest_exit( void )
  119. {
  120. /*should be implemented but I'm lazy and this is just a test */
  121. }
  122.  
  123. MODULE_LICENSE("GPL");
  124. MODULE_AUTHOR("Tuomas Nylund");
  125. MODULE_DESCRIPTION("spitest");
  126. MODULE_VERSION("0.1");
  127.  
  128. module_init(spitest_init);
  129. module_exit(spitest_exit);

That thing there isn't enough by itself. To enable the device driver, we need to add some lines into a couple files.

build_dir/linux-ramips_rt305x/linux-3.3.8/drivers/misc/Kconfig

This file needs to be modified for us to be able to see the new driver in "make kernel_menuconfig" and enable it.

  1. [...]
  2. config SPITEST
  3. tristate "A simple SPI test"
  4. [...]

build_dir/linux-ramips_rt305x/linux-3.3.8/drivers/Kconfig

This file also needs to be modified for the driver to be compiled.

  1. [...]
  2. obj-$(CONFIG_SPITEST) += spitest.o
  3. [...]

build_dir/linux-ramips_rt305x/linux-3.3.8/arch/mips/ralink/rt305x/mach-carambola.c

This file provides support for the carambola board for the kernel. Among other things, it initializes the SPI. A couple of things need to be changed here. There is a struct in the file that tells how the SPI should be initialized. The member "max_speed_hz" needed changing; the original value of 0 resulted in a blazing clock speed of 50MHz, which was a bit too much for the shift register to keep up with. A new value of 2000000 provided a much more manageable clock speed of 2MHz. Also, I changed the "modalias" to something a bit more unique from the default value of "spidev".

  1. [...]
  2. static struct spi_board_info carambola_spi_info[] = {
  3. {
  4. .bus_num = 0,
  5. .chip_select = 0,
  6. .max_speed_hz = 2000000,
  7. .modalias = "spitest",
  8. }
  9. };
  10. [...]

After all of that, it's just a matter of compiling and uploading the fimware to the Carambola. I use the bootloader to download the new firmware with TFTP over ethernet. There's an article about it in the Carambola wiki.

The results


It works! I know, it's a bit under whelming, considering the amount of work I had to do to get there, but I learned a lot in the way and that was the point of the whole exercise.