Input Devices

Assignment for this week

This page is organized as follows:

Measuring a distance

The sensor

For this week's assignment, I decided to redesign the Hello.HC-SR04 board that uses a HC-SR04 sensor based on ultrasonic transducers (emitter/receiver) to mesure a distance. The sensor datasheet provides the necessary information on how to use the sensor.

The sensor has 4 pins (the supply pins Vcc and GND, and 2 signal pins trig). When properly operated and trigged (by applying a 10 us pulse on the trig pin), the HC-SR04 generates 40 kHz ultrasonic pulses and listens to echos reflected by a distant object. From these echos, the sensor generates a pulse on the echo pin whose width is the total traveling time of the ultrasonic wave. The distance between the sensor and the object can then be calculated from this traveling time by multiplying by the sound velocity (340 m/s) and dividing by 2.

Niel's board

The Sonar board example presented during the class that uses the HC-SR04 sensor is the following:

It is based on a ATTiny45 microcontroller and includes a 6 pin header (on the left of the figure) for ISP programming as well as 6 pin FTDI header (on the right) for serial RX/TX communication. As usual, a 10k resistor between the Reset pin and the VCC and an 1 uF capacitor for decoupling the power supply. A 4 pin header (GND, echo, trig, Vcc) with echo and trig being connected to pins PB4 and PB4 on the t45 is used to connect the board and the sensor.

I etched the board and soldered the components.

My board

Since the assignment consisted in redesigning a board that would integrate an input device, make it and read it, I decided to switch from the ATTiny45 with its 8 pins (with only 2 availables for input/output) to ATTiny44 that has 14 pins.

I used Eagle.

I have a t44 with a 20 MHz crystal and the ISP and FTDI connectors and I added a 4 pin connector JP2 for the sensor (trig and echo are connected to PA2 and PA3 on the t44. I also added a 2 pin connector JP2 (connected to PA5 and GND) to allow for output such as a LED for instance. PA6 remains available and is not used in this design.

For the PCB layout, I wanted the ISP and FTDI headers to be on two opposite sides of the board. I thus adapted the basic Hello board from the Electonics Design assignment. I used chemical etching to make the board, soldered the different parts and then checked every solder joint with a DMM (one needle tip on the trace, the other on the part). This last step is absolutely critical as bad joints are frequent.

ISP programming

I looked at the (hello.HC-SR04.c, hello.HC-SR04.make) and the (hello.ftdi.44.echo.c, hello.ftdi.44.echo.c.make) codes. The idea was to adapt the code given for the t45 to the t44. The makefile for the t45 is starting with

PROJECT=hello.HC-SR04
SOURCES=$(PROJECT).c
MMCU=attiny45
F_CPU = 8000000

CFLAGS=-mmcu=$(MMCU) -Wall -Os -DF_CPU=$(F_CPU)

$(PROJECT).hex: $(PROJECT).out
	avr-objcopy -O ihex $(PROJECT).out $(PROJECT).c.hex;\
	avr-size --mcu=$(MMCU) --format=avr $(PROJECT).out
 
$(PROJECT).out: $(SOURCES)
	avr-gcc $(CFLAGS) -I./ -o $(PROJECT).out $(SOURCES)
...

I replaced MMCU=attiny45 by MMCU=attiny44 and changed F_CPU to 20000000. The rest of the code is composed of the avrdude commands for the different programmers. For usbtiny,

program-usbtiny: $(PROJECT).hex
	avrdude -p t45 -P usb -c usbtiny -U flash:w:$(PROJECT).c.hex

Looking at the makefile for the t44, I noticed two lines instead of one for the avrdude call.

program-usbtiny: $(PROJECT).hex
	avrdude -p t44 -P usb -c usbtiny -U flash:w:$(PROJECT).c.hex

program-usbtiny-fuses: $(PROJECT).hex
	avrdude -p t44 -P usb -c usbtiny -U lfuse:w:0x5E:m

So I added those lines. Then the main hello.HC-SR04.c code consist of three main parts. One finds definitions at the beginning (#define directives), then a put_char(volatile unsigned char *port, unsigned char pin, char txchar) function (to serial send a character txchar) and the main function contain the main commands to interact with the sensor and send character on the serial port. The hello.ftdi.44.echo.c has the same structure. In addition to the put_char function, a get_char and a put_string functions are also implemented. The main function in this case reads from serial and then send back the string read to serial.

I replace all the references to PORTB in the t45 by PORTA for the t44. To ISP program the t44 I used the command

make -f hello44.HC-SR04.c.make program-usbtiny

And .... I had this error.

pc27:hello_HC-SR04 vjmdupuis$ make -f hello44.HC-SR04.c.make program-usbtiny
avr-gcc -mmcu=attiny44 -Wall -Os -DF_CPU=20000000 -I./ -o hello44.HC-SR04.out hello44.HC-SR04.c
hello44.HC-SR04.c: In function 'main':
hello44.HC-SR04.c:144:7: error: 'TIFR' undeclared (first use in this function)
       TIFR |= (1 << TOV0);
       ^
hello44.HC-SR04.c:144:7: note: each undeclared identifier is reported only once for each function it appears in
make: *** [hello44.HC-SR04.out] Error 1

Debugging

What to do when you get this kind of error ? Well you tried to google the error and you spent a lot of time searching forums, ... In the end I said to myself, ok let's learn more deeply how timers work on AVR. I can recommend these links 1 and 2. The code in the main function is the following:

// start counter
   //
   TCCR0B |= (1 << CS00); // prescale /1
   //
   // main loop
   //
   while (1) {
      //
      // trigger pulse
      //
      set(trigger_port,trigger_pin);
      _delay_us(10);
      clear(trigger_port,trigger_pin);  
      //
      // wait for echo rising edge
      //
      high = 0;
      TCNT0 = 0;
      TIFR |= (1 << TOV0);
      while (1) {
         if ((echo_pins & echo_pin) != 0) // check for rising edge
            break;
         if ((TIFR & (1 << TOV0)) != 0) { // check for counter overflow
            high += 1;
            if (high == timeout)
               break;
            TIFR |= (1 << TOV0);
            }
         }
      //

I decided to get a deeper understanding of the Niel's code. I learned that TCCR0B is the Timer Counter Control Register and is used to configure the timer. CS00 is the first bit of this 8 bit register and actually it is possible to set the frequency of the timer by 8, 64, 256 by choosing the value of the first 3 bits CS00, CS01 and CS02. In our case 001 means no division which is consistent with Neil's comment. Then comes TCNT0. This is the Timer Counter register that contains the value of the counter. In the code it is resetted to 0. The comes TIFR is the Timer/Counter Interrupt Flag Register whose first bit TOV0 is set to 1 when the timer overflows.

Since it is related to interruptions and that the error message just means that avr gcc just does not know what TIFR is, and following code examples on the internet I tried to add a #include < avr/interrupt.h > which should do no harm. Nope. In the end I just tried to replace TIFR by TIFR0 and ... the error went out. I could check with the terminal that the board was firing data on the Serial port. The hello.HC-SR04.py script gives something but not exactly what Niel observed in his video.

Measuring a temperature

The temperature board example presented during the class that uses a NTC temperature sensor mounted in a bridge configuration with three 10k resistors to increase the sensitivity. As usual, the 6 pin connector for ISP programming on the left and the FDTI connector on the right for serial communication.

I etched the board and soldered the different components. The board before and after soldering is shown below.

To program the board I used my USBTiny programmer and the 6 pin ISP connector. I used the makefile provided on the class page, plugged my USBTiny programmer on the 6 pin ISP header and flashed the board using

make -f hello.temp.45.make programm-usbtiny

The c code provided by Neil is the following. It consists in the serial put_char function followed by the main function that configures the ADC with PB3 and PB4 as input pins and send the data (2 bytes H and L) preceeded by a 1,2,3,4 framing sequence.

//
//
// hello.temp.45.c
//
// thermistor hello-world
//    9600 baud FTDI interface
//
// Neil Gershenfeld
// 10/27/10
//
// (c) Massachusetts Institute of Technology 2010
// This work may be reproduced, modified, distributed,
// performed, and displayed for any purpose. Copyright is
// retained and must be preserved. The work is provided
// as is; no warranty is provided, and users accept all 
// liability.
//

#include 
#include 

#define output(directions,pin) (directions |= pin) // set port direction for output
#define set(port,pin) (port |= pin) // set port pin
#define clear(port,pin) (port &= (~pin)) // clear port pin
#define pin_test(pins,pin) (pins & pin) // test for port pin
#define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set
#define bit_delay_time 102 // bit delay for 9600 with overhead
#define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay
#define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay
#define char_delay() _delay_ms(10) // char delay

#define serial_port PORTB
#define serial_direction DDRB
#define serial_pin_out (1 << PB2)

void put_char(volatile unsigned char *port, unsigned char pin, char txchar) {
   //
   // send character in txchar on port pin
   //    assumes line driver (inverts bits)
   //
   // start bit
   //
   clear(*port,pin);
   bit_delay();
   //
   // unrolled loop to write data bits
   //
   if bit_test(txchar,0)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,1)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,2)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,3)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,4)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,5)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,6)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,7)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   //
   // stop bit
   //
   set(*port,pin);
   bit_delay();
   //
   // char delay
   //
   bit_delay();
   }

int main(void) {
   //
   // main
   //
   static char chr;
   //
   // set clock divider to /1
   //
   CLKPR = (1 << CLKPCE);
   CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
   //
   // initialize output pins
   //
   set(serial_port, serial_pin_out);
   output(serial_direction, serial_pin_out);
   //
   // init A/D
   //
   ADMUX = (0 << REFS2) | (0 << REFS1) | (0 << REFS0) // VCC ref
      | (0 << ADLAR) // right adjust
      | (0 << MUX3) | (1 << MUX2) | (1 << MUX1) | (1 << MUX0); // 20(PB4-PB3)
   ADCSRA = (1 << ADEN) // enable
      | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // prescaler /128
   ADCSRB = (1 << BIN); // bipolar mode
   //
   // main loop
   //
   while (1) {
      //
      // send framing
      //
      put_char(&serial_port, serial_pin_out, 1);
      char_delay();
      put_char(&serial_port, serial_pin_out, 2);
      char_delay();
      put_char(&serial_port, serial_pin_out, 3);
      char_delay();
      put_char(&serial_port, serial_pin_out, 4);
      char_delay();
      //
      // initiate conversion
      //
      ADCSRA |= (1 << ADSC);
      //
      // wait for completion
      //
      while (ADCSRA & (1 << ADSC))
         ;
      //
      // send result
      //
      chr = ADCL;
      put_char(&serial_port, serial_pin_out, chr);
      char_delay();
      chr = ADCH;
      put_char(&serial_port, serial_pin_out, chr);
      char_delay();
      }
   }

The python script provided by Neil is the following:

#
# hello.temp.45.py
#
# receive and display temperature
# hello.temp.45.py serial_port
#
# Neil Gershenfeld
# CBA MIT 3/27/12
#
# (c) Massachusetts Institute of Technology 2012
# Permission granted for experimental and personal use;
# license for commercial sale available from MIT
#

from Tkinter import *
from numpy import log
import serial

WINDOW = 600 # window size
eps = 0.5 # filter time constant
filter = 0.0 # filtered value

def idle(parent,canvas):
   global filter, eps
   #
   # idle routine
   #
   byte2 = 0
   byte3 = 0
   byte4 = 0
   ser.flush()
   while 1:
      #
      # find framing 
      #
      byte1 = byte2
      byte2 = byte3
      byte3 = byte4
      byte4 = ord(ser.read())
      if ((byte1 == 1) & (byte2 == 2) & (byte3 == 3) & (byte4 == 4)):
         break
   low = ord(ser.read())
   high = ord(ser.read())
   value = 256*high + low
   if (value > 511):
      value -= 1024
   V = 2.5 - value*5.0/(20.0*512.0)
   R = 10000.0/(5.0/V-1.0)
   # NHQ103B375R5
   # R25 10000 (O)
   # B (25/85) 3750 (K)
   # R(T(C)) = R(25)*exp(B*(1/(T(C)+273.15)-(1/(25+273.15))))
   B = 3750.0
   R25 =  10000.0
   T = 1.0/(log(R/R25)/B+(1/(25.0+273.15))) - 273.15
   filter = (1-eps)*filter + eps*T
   x = int(.2*WINDOW + (.9-.2)*WINDOW*(filter-20.0)/10.0)
   canvas.itemconfigure("text",text="%.2f"%filter)
   canvas.coords('rect1',.2*WINDOW,.05*WINDOW,x,.2*WINDOW)
   canvas.coords('rect2',x,.05*WINDOW,.9*WINDOW,.2*WINDOW)
   canvas.update()
   parent.after_idle(idle,parent,canvas)

#
#  check command line arguments
#
if (len(sys.argv) != 2):
   print "command line: hello.temp.45.py serial_port"
   sys.exit()
port = sys.argv[1]
#
# open serial port
#
ser = serial.Serial(port,9600)
ser.setDTR()
#
# start plotting
#
root = Tk()
root.title('hello.temp.45.py (q to exit)')
root.bind('q','exit')
canvas = Canvas(root, width=WINDOW, height=.25*WINDOW, background='white')
canvas.create_text(.1*WINDOW,.125*WINDOW,text=".33",font=("Helvetica", 24),tags="text",fill="#0000b0")
canvas.create_rectangle(.2*WINDOW,.05*WINDOW,.3*WINDOW,.2*WINDOW, tags='rect1', fill='#b00000')
canvas.create_rectangle(.3*WINDOW,.05*WINDOW,.9*WINDOW,.2*WINDOW, tags='rect2', fill='#0000b0')
canvas.pack()
root.after(100,idle,root,canvas)
root.mainloop()

Connecting the FTDI cable and invoking the python script. I could check that the board was properly working.

Update after first evaluation

After 1st evaluation round, I was asked by the reviewer to make a MC input board and provide the design files and firmware code. I decided to design and make a board that integrates an ozone sensor MICS2614.

The sensor behaves as a resistor whose resistance value increases with the ozone concentration. It also includes a heater that needs to be fed with an appropriate current. The supply circuit and measurement circuits are as follows:

The idea is to associate the MICS2614 with a load resistance, measure the voltage across the load (using a microcontraller ADC) and deduce the value of sensor's resistance and then the ozone concentration. I used Kicad to design my board. The schematics is inspired from the hello.mag.45 board seen in class. The voltage across the load (which I named SENSE) is fed to the PB4 pin of an ATTiny85 microcontroller.

The board layout is shown below. I included a ground (GND) plane.

I etched the board and soldered the different electronic components. I checked all the joints and everything was ok.

I then turned to programming. Again, I looked at the code written by Neil for the hello.mag.45 example. This code uses PB2 for serial out and accumulate nsamples = 100 readings on ADC4 before sending the sum on serial with a 1 2 3 4 framing.

//
// hello.mics.85.c 
// adapted from hello.mag.45.c
//
// Hall effect magnetic sensing hello-world
//    9600 baud FTDI interface
//
// Neil Gershenfeld 11/3/13
// (c) Massachusetts Institute of Technology 2013
//
// This work may be reproduced, modified, distributed,
// performed, and displayed for any purpose. Copyright is
// retained and must be preserved. The work is provided
// as is; no warranty is provided, and users accept all 
// liability.
//

#include <avr/io.h>
#include <util/delay.h>

#define output(directions,pin) (directions |= pin) // set port direction for output
#define set(port,pin) (port |= pin) // set port pin
#define clear(port,pin) (port &= (~pin)) // clear port pin
#define pin_test(pins,pin) (pins & pin) // test for port pin
#define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set
#define bit_delay_time 102 // bit delay for 9600 with overhead
#define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay
#define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay
#define char_delay() _delay_ms(10) // char delay

#define serial_port PORTB
#define serial_direction DDRB
#define serial_pin_out (1 << PB2)

#define nsamples 100 // number of samples to accumulate

void put_char(volatile unsigned char *port, unsigned char pin, char txchar) {
   //
   // send character in txchar on port pin
   //    assumes line driver (inverts bits)
   //
   // start bit
   //
   clear(*port,pin);
   bit_delay();
   //
   // unrolled loop to write data bits
   //
   if bit_test(txchar,0)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,1)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,2)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,3)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,4)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,5)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,6)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   if bit_test(txchar,7)
      set(*port,pin);
   else
      clear(*port,pin);
   bit_delay();
   //
   // stop bit
   //
   set(*port,pin);
   bit_delay();
   //
   // char delay
   //
   bit_delay();
   }

int main(void) {
   //
   // main
   //
   static uint16_t count;
   static uint32_t accum;
   //
   // set clock divider to /1
   //
   CLKPR = (1 << CLKPCE);
   CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0);
   //
   // initialize output pins
   //
   set(serial_port, serial_pin_out);
   output(serial_direction, serial_pin_out);
   //
   // init A/D
   //
   ADMUX = (0 << REFS2) | (0 << REFS1) | (0 << REFS0) // Vcc ref
      | (0 << ADLAR) // right adjust
      | (0 << MUX3) | (0 << MUX2) | (1 << MUX1) | (0 << MUX0); // ADC4
   ADCSRA = (1 << ADEN) // enable
      | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // prescaler /128
   //
   // main loop
   //
   while (1) {
      //
      // accumulate samples
      //
      accum = 0;
      for (count = 0; count < nsamples; ++count) { 
         //
         // initiate conversion
         //
         ADCSRA |= (1 << ADSC);
         //
         // wait for completion
         //
         while (ADCSRA & (1 << ADSC))
            ;
         //
         // add result
         //
         accum += ADC;
         }
      //
      // send framing
      //
      put_char(&serial_port, serial_pin_out, 1);
      char_delay();
      put_char(&serial_port, serial_pin_out, 2);
      char_delay();
      put_char(&serial_port, serial_pin_out, 3);
      char_delay();
      put_char(&serial_port, serial_pin_out, 4);
      char_delay();
      //
      // send result
      //
      put_char(&serial_port, serial_pin_out, (accum & 255));
      char_delay();
      put_char(&serial_port, serial_pin_out, ((accum >> 8) & 255));
      char_delay();
      put_char(&serial_port, serial_pin_out, ((accum >> 16) & 255));
      char_delay();
      }
   }

I also modified the makefile to compile for ATTiny85 and flashed the board using make -f hello.mics.85.make program-usbtiny. I obtained the following log:

avr-gcc -mmcu=attiny85 -Wall -Os -DF_CPU=8000000 -I./ -o hello.mics.85.out hello.mics.85.c
avr-objcopy -O ihex hello.mics.85.out hello.mics.85.c.hex;\
	avr-size --mcu=attiny85 --format=avr hello.mics.85.out
AVR Memory Usage
----------------
Device: attiny85

Program:     662 bytes (8.1% Full)
(.text + .data + .bootloader)

Data:          6 bytes (1.2% Full)
(.data + .bss + .noinit)


avrdude -p t85 -P usb -c usbtiny -U flash:w:hello.mics.85.c.hex

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e930b
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "hello.mics.85.c.hex"
avrdude: input file hello.mics.85.c.hex auto detected as Intel Hex
avrdude: writing flash (662 bytes):

Writing | ################################################## | 100% 0.84s

avrdude: 662 bytes of flash written
avrdude: verifying flash memory against hello.mics.85.c.hex:
avrdude: load data flash data from input file hello.mics.85.c.hex:
avrdude: input file hello.mics.85.c.hex auto detected as Intel Hex
avrdude: input file hello.mics.85.c.hex contains 662 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 1.06s

avrdude: verifying ...
avrdude: 662 bytes of flash verified

avrdude: safemode: Fuses OK (H:FF, E:DF, L:62)

avrdude done.  Thank you.
   

I then tried to use the python script written by Neil again for the hello.mag. I use ls /dev/tty.usb* to grab the name of my USB to Serial FTDI cable and ran python hello.mics.85.py myserial.

I could see the graphic window with the red bar. It does not change a lot because hopefully the ozone concentration in my office does not change. I tried to touch the sensor and then I could see a change meaning that the sensor is sensitive also temperature and potentially humidity. In the python script, the scale is 0-1023 and the max value should correspond to a 5V voltage. This gave me about 146 mV for a reading of 30. With 1k load resistance, I could calculate a value of the sensor resistance of (5/(30/1024*5)-1) = 33k which is in the range indicated in the datasheet (although a bit high).

The kicad files, ATTiny firmware and python script can be downloaded below (or in the section Files to download, top right panel):

EVALUATION CHECKLIST 2016 -
INPUT DEVICES

Skills Acquired:

Documentation Required for Completion:

At Minimum (electronics beginners):

Students with Previous Electronics Experience

What’s the Point?

Files to download