Coding Analog Sensors on the Raspberry Pi3

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

In many places, using analog sensors makes better sense than using their digital counterparts. They are low in cost but provide high precision data. In this article, we demonstrate how analog sensors are supported on the globally known single circuit board Raspberry Pi 3(Model B). Raspberry Pi 3 (rPI3) is an arm 64-bit quad core processor board. It provides rich support for interfacing with external peripherals (sensors), but it does not have a built-in ADC (Analog to Digital Converter), making it limited to digital-type sensors only. Analog sensors covered in this article are the following:

  • Joystick
  • Potentiometer

The rPI3 supports various protocols for serial interfacing with external devices; for example, I2C and SPI. In this article, we use ADC chip MCP3008 as a mediator to the rPI3 and an analog sensor. It is an 8-channel input and 10-bit output precision chip that interacts with the rPI3 through a SPI (Serial Peripheral Interface) protocol. To know more on rPI3, I2C, and SPI protocol refer to the article "Raspberry Pi 3 Hardware and System Software Reference."

Aspects of Serial Protocol (SPI, I2C) and Analog Sensor Interfacing

rPI3 has 54 GPIO ports and, by default, it supports parallel port communication. Parallel ports are used in a way that each sensor's digital channel is connected to a separate port. Analog sensors first get mapped to their digital values. The digital value can be of any bit precision, depending upon the nature of the ADC. With a variable number of bits, rPI3 can communicate only with a fixed number of ports. It has to communicate with ADC serially (SPI, I2C). How does this serial communication takes place between rPI3 and ADC? How does the ADC covert analog signals to digital?

User Mode Application vs Kernel Mode Device Driver

The rPI3 communicates with the ADC (MCP3008) through the SPI protocol. SPI is a serial way of data communication, synchronized with a dedicated clock. SPI-related registers are memory mapped in rPI3. These memory-mapped addresses can be mapped in the kernel space (with the driver spi-bcm2708) or a virtual address space in user mode. Reading and writing of the mapped address can be done in both ways. Even though it is difficult to make protocol interfacing transparent to the user, here we discuss mapping memory mapped addresses to a virtual address space (through the system API mmap) in user mode. For more on SPI registers, refer to the earlier article "Raspberry Pi 3 Hardware and System Software Reference."

About Raspberry Pi 3

Refer to the previous article "Raspberry Pi 3 Hardware and System Software Reference."

Implementation

Sensor code running on a Rapsberry Pi 3 has inputs from SPI interfacing whereas output goes to a qt-based 2D drawing engine in real time fashion. To know more about Qt 2D drawing support on the rPI3, refer to "Using the Qt 2D Display on a Raspberry Pi3."

Design

To map the memory mapped address to process address space and then start tweaking the bits for use with various devices, a bridge cum mediator hybrid design is proposed as follows.

A bridge cum mediator hybrid design
Figure 1: A bridge cum mediator hybrid design

mainwindow and the rPI3 make a bridge whereas mainwindow behaves as a mediator between the rPI3 derivative and the chart. rPI3 is abstract class and further subclassed to abstract classes as SPI, I2C, and GPIO. SPI, along with I2C and GPIO, is further subclassed to concrete sensor classes. For more on mainwindow and chart refer to "Using the Qt 2D Display on a Raspberry Pi3."

Memory Mapping and Mapping to Process Address Space

The rPI3 memory maps the device to address 0x7E00_0000 at 0X3F00_0000 in physical memory. Class rPI3 opens (for the root user only) the physical memory device file /dev/mem and then maps 0x3F20_0000 to virtual address space in volatile address field addr, which is declared static. In this article, we use SPI interfacing in the code. rPI3 has only one SPI interface, SPI0 which maps at 0x3F204000 of physical memory. For more on this refer to "Raspberry Pi 3 Hardware and System Software Reference."

Code

Following is an illustration of each class.

Class rPI3

Class rPI3
Figure 2: Class rPI3

struct rpi3{
   rpi3();
   virtual ~rpi3();
   c_t _ct;
   int _min1,_max1,_step1,_min2,_max2,_step2;
   /* Mapped address in virtual space*/
   static volatile unsigned int *addr;
    /* Process the read. Data will be stored in _ct.y1, _ct.y2*/
   virtual int read(void*)=0;
    /*void* deals with various write requirements in different
     * protocols*/
   virtual int write(void*)=0;
   virtual int process(void*)=0;   /* Process the request*/
};

'rPI3' declares min and max values for the y1 and y2 axes displayed in the chart. The step declares resolution in the scale.

'rPI3' maps the memory-mapped address 0x3F20_0000 to the process address space by opening the /dev/mem file and then mapping the file descriptor with mmap API in shared mode. This class provides three pure virtual functions. read() and write() are implemented as per protocol or sensor, process() typically returns 1, giving a hint for drawing the chart, whereas 0 hints for no draw.

Class spi

Class spi
Figure 3: Class spi

// GPIO setup macros. Always use INP_GPIO(x) before using
//  OUT_GPIO(x)
#define SPI0_BASE (BCM2709_PERI_BASE+0x00204000)
#define SET_GPIO_ALT(g,a) *(addr + (((g)/10))) |= (((a)<=3?(a)
   + 4:(a)==4?3:2)<<(((g)%10)*3))
INP_GPIO(g)  *(addr + ((g)/10)) &= ~(7<<(((g)%10)*3))
#define SPI0_CS 0x0000     /* SPI Master Control and Status*/
#define SPI0_FIFO 0x0004   /* SPI Master TX and RX fifos*/
#define SPI0_CLK 0x0008    /* SPI Master Clock Divider */
#define SPI0_CS_CLEAR 0x00000030   /* CLear FIFO Clear RX and TX */
#define SPI0_CS_TA    0x00000080   /* Transfer Active */
/* TXD Tx FIFO can accept Data */
#define SPI0_CS_TXD   0x00040000
#define SPI0_CS_RXD   0x00020000   /* RXD RX FIFO contains Data */
#define SPI0_CS_DONE  0x00010000   /* Done transfer Done */
#define SPI0_CS_CPOL  0x00000008   /* Clock Polarity*/
#define SPI0_CS_CPHA  0x00000004   /* Clock Phase*/
#define SPI0_MODE0 0               /* CPOL=0 CPHA=0*/
#define SPI0_CS_CS    0x00000003   /* Chip Select*/
#define SPI0_CS0 0                 /* Chip Select CS0 GPIO8*/
#define SPI0_LOW 0x0
struct spi:rpi3{
   spi();
   virtual ~spi();
   static volatile uint32_t* spi0addr;
   virtual int read(void*);
   virtual int write(void*);
   void peri_set_bits(volatile uint32_t *paddr, uint32_t value,
      uint32_t mask);
};

spi.h provides #define macros for various flags in SPI protocol. Refer to "Raspberry Pi 3 Hardware and System Software Reference" for additional details. SPI0_BASE is 0x00204000 offset from the peripherial base physical memory-mapped address of 0x3F00_0000. The 'spi' class opens the /dev/mem file and then maps the physical memory (through a file descriptor) at address SPI0_BASE to the virtual address 'volatile uint32_t api0addr'. Control, FIFO, and clock registers in SPI protocol are manipulated through it.

// Open /dev/mem
if(!spi0addr){
   if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
      // printf("Failed to open /dev/mem,
      // try checking permissions.\n");
      throw std::runtime_error("failed to open /dev/mem,
         try checking permissions.\n");
}

map = mmap(
   NULL,
   BLOCK_SIZE,
   PROT_READ|PROT_WRITE,
   MAP_SHARED,
   // File descriptor to physical memory virtual file '/dev/mem'
   mem_fd,
   // Address in physical map that we want this memory block
   // to expose
   SPI0_BASE
);

close(mem_fd);

if (map == MAP_FAILED) {
   // perror("mmap");
   throw std::runtime_error("spi0 mmap error\n");
}

spi0addr = (volatile unsigned int *)map;

The 'spi' class constructor further calls the following:

#define SET_GPIO_ALT(g,a) *(addr + (((g)/10))) |=
   (((a)<=3?(a) + 4:(a)==4?3:2)<<(((g)%10)*3))

to set the alternate function '0' for gpio 8,9,10,11. This sets SPI0_CS0, SPI0_MISO, SPI0_MOSI and SPI0_CLK to the respective gpio.

The 'spi class derives rpi3 and overrides the read() and write() functions with an empty statement.

Class jstk

Class jstk
Figure 4: Class jstk

struct jstk:spi{
   jstk();
   int process(void*);
};

int jstk::process(void*){
   // Trace("><jstk::process"
   static int xvalue=0,yvalue=0;
   volatile uint32_t *addr_cs=spi0addr+SPI0_CS/4;
   volatile uint32_t *addr_fifo=spi0addr+SPI0_FIFO/4;
   uint8_t mosi[2][3]={{0x01, 0x80, 0x00},{0x01,0x90,0x00}};
   uint8_t miso[2][3]={0};
   uint32_t txcnt=0,rxcnt=0;
   int i=0;
   for(i=8;i<12;i++){
      INP_GPIO(i);
      SET_GPIO_ALT(i,0);
}
for(i=0;i<2;i++){
   txcnt=rxcnt=0;
   peri_set_bits(addr_cs,SPI0_CS_CLEAR,SPI0_CS_CLEAR);
   peri_set_bits(addr_cs,SPI0_CS_TA,SPI0_CS_TA);
   while(txcnt<3 || rxcnt<3){
      while(((*addr_cs) & SPI0_CS_TXD)&&(txcnt<3))
         *(addr_fifo)=mosi[i][txcnt++];
      while(((*addr_cs) & SPI0_CS_RXD)&&(rxcnt<3))
         miso[i][rxcnt++]=*(addr_fifo);
   }
   while(!((*addr_cs)& SPI0_CS_DONE)) ;
   peri_set_bits(addr_cs,0,SPI0_CS_TA);
}
if((xvalue+yvalue -miso[0][2]-((miso[0][1]&3)<<8)
      - miso[1][2]-((miso[1][1]&3)<<8))>10 ||
      (miso[0][2]+((miso[0][1]&3)<<8) + miso[1][2]+
      ((miso[1][1]&3)<<8)-xvalue-yvalue)>10) {
         _ct.y1=xvalue=miso[0][2]+((miso[0][1]&3)<<8);
         _ct.y2=yvalue=miso[1][2]+((miso[1][1]&3)<<8);
      return 1;
   }
   // Trace("<>jstk::process")
   return 0;
}

The 'jstk' class represents a two-dimensional joystick. It drives 'spi' and overrides process() from rPI3. Every time process is called, it clears the TX and RX bits in the Control and Status (CS) register to clear the FIFO. It sets TA bit in CS to start the transfer. 0x01 is sent to make MCP3008 ready for communication. 0x08 is sent for channel number 0 (X axis) to be selected and then 0x00 is sent to fetch the channel 0 data in a round FIFO data transfer method. A similar procedure is called for channel number 1 (Yaxis) where the second byte sent is to 0x90. The second byte received consists of bits 8 and 9 whereas the third byte gets bit 0-7. process() returns 1 only if the difference in data is more than 10; otherwise, 0 is returned. 1 draws the entry in Qt graphics. Refer to "Raspberry Pi 3 Hardware and System Software Reference" and Using the Qt 2D Display on a Raspberry Pi3."

Class pmtr

Class pmtr
Figure 5: Class pmtr

struct pmtr:spi{
   pmtr();
   int process(void*);
};
int pmtr::process(void*){
   ....
   if((lastvalue-(miso[2]+((miso[1]&3)<<8)))>10 ||
         (miso[2]+((miso[1]&3)<<8)-lastvalue)>10){
      _ct.y1=(double)3.3*(lastvalue=miso[2]+
         ((miso[1]&3)<<8))/1024;
      return 1;
   }
   return 0;
}

The 'pmtr' class represents a potentiometer sensor. It handles the potentiometer where the output voltage varies from 0 to Max as the knob rotates. The process() function sends and receives data from mcp3008 in a similar fashion to the jstk::process() except that it uses channel 0 only. It returns 1 when the differential data is more than 10.

Experimental Output

Setting Up a Build Environment

Refer to "Raspberry Pi 3 Hardware and System Software Reference."

Qt

Real-time data display is developed through QPainter painting in Qt5.3 (default through raspibian). Refer to Using the Qt 2D Display on a Raspberry Pi3."

Graph

Joystick

JoyStick circuit diagram
Figure 6: JoyStick circuit diagram

JoyStick graph
Figure 7: JoyStick graph

Potentiometer

Potentiometer at half mark
Figure 8: Potentiometer at half mark

Potentiometer at full mark
Figure 9: Potentiometer at full mark

Potentiometer graph
Figure 10: Potentiometer graph

Summary

This article described analog sensors interfacing with rPI3 through the SPI interface. MCP3008 ADC is used; it supports the SPI protocol and converts an analog signal to 10-bit digital output (values ranging from 0 to 1024). The user mode virtual address mapping of device memory mapped address is used instead of relying upon built-in device driver support.

Interrupt handling is not supported. This makes the SPI protocol usage transparent to the user. This article showed output of the joystick and potentiometer analog devices. Real-time output is drawn on Qt 2D graphics.

Download the Code

Please feel free to download the accompanying .tar file. It contains the code to complete this project.

References



About the Author

Pravin Kumar Sinha

Pravin Kumar Sinha is embedded C++ developer and training consultant with 18 years of experience in Object Oriented Technology mostly in C++, Qt, Python. Pravin has publications in Algorithms (Stack-based breadth-first search tree traversal), Design Patterns (Perl), High volume C++ generic logger, Qt:Chain of Responsibility and Qt:3D OpenGL drawing on Google Maps.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The hunger for IIoT-enabled solutions is driving companies to seek out reliable, secure IIoT platforms that can handle industrial-grade IoT capabilities. What features and capabilities should companies expect in an IIoT platform? Until now, developing an IIoT solution has required the costly, time-intensive effort of platform building, as developers create technology stacks from scratch to handle the hardware, firmware, software, edge computing, analytics, business systems integration, and more. This …

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date