231a ad-finalreport

From CSclasswiki
Jump to: navigation, search

Final Project Report

The Assembly Program

The first thing the program needs is to be able to receive and store notes that make up a melody, given to it by the user.

Keyboard Input

I originally planned to use keyboard input for this, where the user would add notes one at a time by pressing a, b, c, d, e, f, g, or p (for pause) on the keyboard (for simplicity, this program deals with only one octave, and in this version it wasn't even a complete octave since it was missing the high c). Each note would be stored as a "period value" (for the pulse width modulator) in an array that could hold a maximum of thirty-two notes. The assembly program would run in an endless loop, each time going through the array and passing each note off to the Arduino until it reached the last note, then would add the latest note entered by the user and repeat. The result would be that the Arduino would play a repeating musical loop, that would increase in length as the user put in new notes. Once the array had reached its maximum size, the program would no longer allow the user to enter notes, but would continue to play. To exit the program, the user would have the option of telling the program to quit when asked to enter a new note, or could tell the program to quit at anytime after the array was filled while the loop continued to play.

The Art of Assembly explains that Assembly takes keyboard input with the following code

ReadLoop:       mov     ah, 0           ;Read Key opcode
                int     16h
                cmp     al, 0           ;Special function?
                jz      ReadLoop        ;If so, don't echo this keystroke
                cmp     al, 0dh         ;Carriage return (ENTER)?
                jne     ReadLoop

Since this is code is designed for Windows, I replaced int 16h with int 80h.

I put together a a test program to assemble an array via keyboard input, but the keyboard input never worked. The initial messages would display but character input was never processed correctly (the program would either exit when it had not been told to, it endlessly wait for and echo keyboard input).

I tried replacing the assembly code with the C function 'getchar', but again the character was never processed. The program would endlessly wait for and echo keyboard input.

File Input

After spending an entire afternoon unsuccessfully trying to get the keyboard input to work, I decided instead to work on the array processing and return to character input later. I wrote a quick test program with a predefined array containing the period values for the notes to "Mary Had a Little Lamb." The program runs through the array of notes, checking for 78, which signifies that no more notes exist, and would send a new note to the Arduino every half second (since the Arduino wasn't hooked up yet, this is simply left as moving each note to eax).

Instead of returning to keyboard input, I instead decided to use file input, and assumed I would be returning to keyboard input once the rest of the project was working. readMelody.asm (below) reads notes from a text file (melody.txt). The program checks which notes have been read in, and for each note, stores that note's equivalent period value in the array, then runs through the array like in playMary.asm. This allows for the user to still set his or her own melody, once again with a maximum of 32 notes (with no specific reason other than that it keeps things simpler for prototyping), and this time a high C is also defined, allowing for a complete octave. However it means note cannot be added during the program's run.

;;; ; ; readMelody.asm
;;; ; ; Amy Gray 
;;; ; ; December 2008
;;; ; ; 
;;; ; ;
;;; ; ; readMelody.asm
;;; ; ;
;;; ; ; Reads in notes from a txt file and writes
;;; ; ; each note's corresponding period value to
;;; ; ; the array.
;;; ; ;
;;; ; ; to assemble and run:
;;; ; ;
;;; ; ;     nasm -f elf -F  stabs readMelody.asm
;;; ; ;     gcc -o readMelody arduino-serial.c readMelody.o
;;; ; ;     ./readMelody -b 9600 -p /dev/ttyUSB0
;;; ; ; =========================================================

;; %include "asm_io.inc"

;;; External Labels =============================================

extern	buf
extern	serialport_write_period
extern	period

                EXIT    equ             1
                WRITE   equ             4
                STDOUT  equ             1

%assign SYS_EXIT	1
%assign	SYS_WRITE	4
%assign SYS_READ	3
%assign SYS_LSEEK	19
%assign SEEKSET		0
%assign	STDOUT		1
%assign SYS_OPEN        5
%assign SYS_CLOSE	6
%assign SYS_CREATE	8

%assign O_RDONLY	000000q
%assign O_WRONLY        000001q	
%assign O_RDWR          000002q
%assign O_CREAT		000100q

%assign S_IRUSR		00400q
%assign S_IWUSR		00200q
%assign S_IXUSR		00100q

;;; --- MACRO -----------------------------------------------
;;;     print           msg,length
%macro  print           2               ; %1 = address %2 = # of chars
        pushad                          ; save all registers
        mov             edx,%2
        lea             ecx,[%1]
        mov             eax,SYS_WRITE
        mov             ebx,STDOUT
        int             0x80
        popad                           ; restore all registers

;;; --- MACRO -----------------------------------------------
;;;     print2        "quoted string"
%macro  print2        1               ; %1 = immediate string,
        section .data
%%str   db              %1
%%strL  equ             $-%%str
        section .text
        print           %%str, %%strL
;;; --- MACRO -----------------------------------------------
;;; 	openFile filename, handle
%macro	openFile 2
	mov	eax,SYS_OPEN
	mov	ebx,%1
	mov	ecx, O_RDONLY
	int	0x80

	test	eax,eax
	jns	%%readFile
	print2  "Could not open file"
	mov	eax,SYS_EXIT
	mov	ebx,0
	int	0x80		; final system call

	mov	%2, eax		; save handle

;;; --- MACRO -----------------------------------------------
;;; 	readFile handle, buffer, buffer-length, number-bytes-read
%macro  readFile 4
	mov	eax,SYS_READ
	mov	ebx,%1		; file descriptor in bx
	mov	ecx,%2		; buffer address
	mov	edx,%3		; max number of bytes to read
	int	0x80
	mov	%4,eax		; save number bytes read

;;; --- MACRO -----------------------------------------------
;;;	close   handle
%macro	close   1
	mov	eax,SYS_CLOSE
	mov	ebx,%1
	int	0x80

;;; ;  ===========================================================
;;; ;  data areas
;;; ;  ===========================================================
;;; ;  78 is still left over from playNotes.asm, but the value
;;; ;  stored where no notes exist in this array is arbitrary.
;;; ;  This program keeps track of the melody's length in a
;;; ;  in a variable while reading in the file and uses this
;;; ;  variable instead to keep track of how many notes to play.
;;; ;  It does not need to check values to find the end of the
;;; ;  melody, which it would do if the melody length were
;;; ;  increasing while the program ran.
;;; ;  ===========================================================

;; TONES  ================================================== 
;; Defining the relationship between 
;;         note, period, &  frequency. 
;; #define  c     3830    // 261 Hz 
;; #define  d     3400    // 294 Hz 
;; #define  e     3038    // 329 Hz 
;; #define  f     2864    // 349 Hz 
;; #define  g     2550    // 392 Hz 
;; #define  a     2272    // 440 Hz 
;; #define  b     2028    // 493 Hz 
;; #define  C     1912    // 523 Hz 
;; #define  p     0
;; =========================================================
                section .data
melodyLen	dd	0 	; number of notes in melody
fileName	db	"melody.txt",0
array		dd	78,78,78,78,78,78,78,78,78,78,78,78,78,78,78
		dd	78,78,78,78,78,78,78,78,78,78,78,78,78,78,78
		dd	78,78
arrayLen	equ	$-array	; 33
handle		dd	0
noRead		dd	0	; number of characters read from file
bnMessage	db	"Sorry, an invalid note was found in melody.txt"
bnMessageLen	equ	$-bnMessage

		section .bss
MAXBUF	equ	32
buffer	resb	MAXBUF		; program will play no more than 32 notes

;;; ;  =============================================================
;;; ;  code area
;;; ;  =============================================================

                section .text
                global asm_main

;;; skip keyboard input
;;; this version does not check pots

	;; read notes from melody.txt and store in buffer
	openFile	fileName, [handle]
	readFile	[handle], buffer, MAXBUF, [noRead]
	close		[handle]

;;; check notes in buffer and store period values in array
	mov	eax, 0		; clear eax 
	mov	edx, 0
	mov	ecx, 32
	mov	al, byte[buffer + edx]
	cmp	al, 0x63	; low c?
	je 	noteClow
	cmp	al, 0x64	; d?
	je	noteD
	cmp	al, 0x65	; e?
	je	noteE
	cmp	al, 0x66	; f?
	je	noteF
	cmp	al, 0x67	; g?
	je	noteG
	cmp	al, 0x61 	; a?
	je	noteA
	cmp	al, 0x62	; b?
	je	noteB
	cmp	al, 0x63	; high c?
	je	noteChigh
	cmp	al, 0x70	; pause?
	je	notePause

	jmp	arrayPart

	inc	edx 		; move to next note
	inc	dword[melodyLen]
	loop	getNotes
	jmp	arrayPart 	; array is complete, now use it
	mov	dword[array + ebx], 3830
	jmp	next
	mov	dword[array + ebx], 3400
	jmp	next
	mov	dword[array + ebx], 3038
	jmp	next
	mov	dword[array + ebx], 2864
	jmp	next
	mov	dword[array + ebx], 2550
	jmp	next
	mov	dword[array + ebx], 2272
	jmp	next
	mov	dword[array + ebx], 2028
	jmp	next
	mov	dword[array + ebx], 1912
	jmp	next
	mov	dword[array + ebx], 0
	jmp	next
badNote:			; print error and exit
	mov	eax, SYS_WRITE
	mov	ebx, STDOUT
	mov	ecx, bnMessage
	mov	edx, bnMessageLen
	int	0x80
	jmp	exit

;; ARRAY PART =========================================
;; now we deal with the period values that have been
;; stored in the array. These each need to be passed
;; to the Arduino at regular intervals.
;; ====================================================

	mov	ecx, [melodyLen] ;number of notes to be played
	mov	edx, 0		    ;which note to play
;; run through the array
 	push 	ecx
 	mov	eax, [array + edx]
	;; pass eax to Arduino
  	mov	[period], eax
  	call	serialport_write_period
	;; Arduino will play this until it receives the next note

	;; delay before sending next note to allow time for note
	;; to be played.
   	mov	ecx, 500000000 	; pot will be changing this value
				; to increase/decrease tempo
   	loop 	.for
	mov	ecx, 500000000 	; this one too
	loop	.for2
 	add	edx, 4
 	pop	ecx
 	loop	runArray 	; go to next note
 	jmp	exit

Once the debugger showed that all values were being read and processed exactly as they were supposed to, the next step was setting up communication with the Arduino.

Communicating with the Arduino

The Arduino Sketch

The Arduino needs to be set up to receive a value from the Ubuntu PC, and emit a tone using that value for the PWM period.


// GLOBALS ==========================================
// =================================================
char buffer [100];
int len;
char *operation;
char *mode;
char *pin;
char *state;

// PROTOTYPES =======================================
// ==================================================
int process ();

// SETUP ============================================
// Set up speaker on a PWM pin (digital 9, 10 or 11)
// ==================================================
int speakerOut = 9;

void setup() { 
  pinMode(speakerOut, OUTPUT);

//MAIN ================================================
// ====================================================
void loop() {
    int c;
    c = Serial.read(); //gets value from C driver
    // nothing received, nothing to do
    if (c == -1) return;

    // \n terminator received
    if ( c == '\n' ) {
       if ( process () ) { 
       	  Serial.println ("Error");

   // something received but not \n, add to buffer
   buffer[ len ++ ] = c; //puts value in buffer
   buffer[ len ] = '\0';

// PROCESS =============================================
// go here after receiving a message from Ubuntu
// =====================================================
int process () {
    char *p;
    int len = strlen (buffer);
    int intPin;
    int	intState;

    //--- if message less than 1 character, return ---//
    if ( len < 1 ) {
       return 1; //error

    int period = atoi( buffer ); //Arduino receives value as string.  convert to int
    if	( period > 0 ) {
    	digitalWrite( speakerOut, HIGH);  
	delayMicroseconds( period/2 );
                                                                 //play tone with this period value
	digitalWrite( speakerOut, LOW);
	delayMicroseconds( period/2 );
    if ( period == 0 ) {  //0 means pause. play nothing.
       digitalWrite(speakerOut, LOW);

    return 0;     

The C Driver

The C driver is a modification of arduino-serial.c. It contains an additional function that is called by the assembly program and writes a period value (stored in eax in the assembly program and then moved to the C program's buf variable) to the serial port. That value is read by the Arduino sketch.

 * Arduino-serial
 * --------------
 * A simple command-line example program showing how a computer can
 * communicate with an Arduino board. Works on any POSIX system (Mac/Unix/PC) 
 * Compile with something like:
 * gcc -o arduino-serial arduino-serial.c
 * Created 5 December 2006
 * Copyleft (c) 2006, Tod E. Kurt, tod@todbot.com
 * http://todbot.com/blog/
 * Updated 8 December 2006: 
 *  Justin McBride discoevered B14400 & B28800 aren't in Linux's termios.h.
 *  I've included his patch, but commented out for now.  One really needs a
 *  real make system when doing cross-platform C and I wanted to avoid that
 *  for this little program. Those baudrates aren't used much anyway. :)
 * Updated 26 December 2007:
 *  Added ability to specify a delay (so you can wait for Arduino Diecimila)
 *  Added ability to send a binary byte number
 * Update 31 August 2008:
 *  Added patch to clean up odd baudrates from Andy at hexapodia.org
 * Modified 11/06/08 by D. Thiebaut
 *  Force program to wait until a \n is received from Arduino
 *  Added linking with assembly program

#include <stdio.h>    /* Standard input/output definitions */
#include <stdlib.h> 
#include <stdint.h>   /* Standard types */
#include <string.h>   /* String function definitions */
#include <unistd.h>   /* UNIX standard function definitions */
#include <fcntl.h>    /* File control definitions */
#include <errno.h>    /* Error number definitions */
#include <termios.h>  /* POSIX terminal control definitions */
#include <sys/ioctl.h>
#include <getopt.h>

//extern int asm_main( void );

//-------------------------- prototypes ---------------------------
void usage(void);
int serialport_init(const char* serialport, int baud);
int serialport_writebyte( );
int serialport_write( );
int serialport_read_until( );
int displayBuffer( );
int serialport_write_period( );

//---------------------------- globals ----------------------------
int  fd = 0;             // file descriptor for USB device
char serialport[256];    // name of the serial port (/dev/ttyUSB0)
int  baudrate = B9600;   // default baud rate
char buf[1024];          // buffer for sent/receive strings
uint8_t byte;
int  pin;
int  period;

void usage(void) {
    printf("Usage: arduino-serial -p <serialport> [OPTIONS]\n"
    "  -h, --help                   Print this help message\n"
    "  -p, --port=serialport        Serial port Arduino is on\n"
    "  -b, --baud=baudrate          Baudrate (bps) of Arduino\n"
    "  -s, --send=data              Send data to Arduino\n"
    "  -r, --receive                Receive data from Arduino & print it out\n"
    "  -n  --num=num                Send a number as a single byte\n"
    "  -d  --delay=millis           Delay for specified milliseconds\n"
    "Note: Order is important. Set '-b' before doing '-p'. \n"
    "      Used to make series of actions:  '-d 2000 -s hello -d 100 -r' \n"
    "      means 'wait 2secs, send 'hello', wait 100msec, get reply'\n"

int main(int argc, char *argv[]) {
    int rc, n;

    if (argc==1) {

    /*--- parse options ---*/
    int option_index = 0, opt;
    static struct option loptions[] = {
        {"help",       no_argument,       0, 'h'},
        {"port",       required_argument, 0, 'p'},
        {"baud",       required_argument, 0, 'b'},
        {"send",       required_argument, 0, 's'},
        {"receive",    no_argument,       0, 'r'},
        {"num",        required_argument, 0, 'n'},
        {"delay",      required_argument, 0, 'd'}
    /*--- process the command line arguments ---*/
    while(1) {
        opt = getopt_long ( argc, argv, "hp:b:s:rn:d:",
                           loptions, &option_index );
        if (opt==-1) break;
        switch (opt) {
        case '0': break;
        case 'd':
            n = strtol(optarg,NULL,10);
            usleep(n * 1000 ); // sleep milliseconds
        case 'h':
        case 'b':
            baudrate = strtol(optarg,NULL,10);
        case 'p':
            fd = serialport_init( optarg, baudrate );
	      printf( "Could not initialize USB port\n\n" );
	      return -1;
        case 'n':
            n = strtol(optarg, NULL, 10 ); // convert string to number
	    byte = (uint8_t) n;
            rc = serialport_writebyte( );
            if(rc==-1) return -1;
        case 's':
            strcpy( buf, optarg );
            rc = serialport_write( );
            if (rc==-1) return -1;
        case 'r':
	    serialport_read_until( );

    //--- call assembly language program ---

    //--- exit ---

} // end main
int displayBuffer( ) {
  printf( "buffer = [%s]\n", buf );
  return 0;

int serialport_writebyte( ) {
    int n = write( fd, &byte, 1);
    if( n!=1)
        return -1;
    return 0;

int serialport_write() {
  int len;
  strcat( buf, "\n" );
  len = strlen(buf);
  int n = write( fd, buf, len );

  if( n!=len ) 
    return -1;
  return 0;

int serialport_read_until( ) {
    char b[1];
    int i=0, k;
    do { 
        int n = read(fd, b, 1);  // read a char at a time
        if( n==-1) {
	  usleep( 100*1000 );
        if( n==0 ) {
            usleep( 10 * 1000 ); // wait 10 msec try again
	buf[i] = b[0];
    } while( b[0] != '\n' );

    buf[i] = 0;  // null terminate the string
    return 0;

// takes the string name of the serial port (e.g. "/dev/tty.usbserial","COM1")
// and a baud rate (bps) and connects to that port at that speed and 8N1.
// opens the port in fully raw mode so you can send binary data.
// returns valid fd, or -1 on error
int serialport_init(const char* serialport, int baud)
    struct termios toptions;
    int fd;
    //fprintf(stderr,"init_serialport: opening port %s @ %d bps\n",
    //        serialport,baud);

    fd = open(serialport, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1)  {
        perror("init_serialport: Unable to open port ");
        return -1;
    if (tcgetattr(fd, &toptions) < 0) {
        perror("init_serialport: Couldn't get term attributes");
        return -1;
    speed_t brate = baud; // let you override switch below if needed
    switch(baud) {
    case 4800:   brate=B4800;   break;
    case 9600:   brate=B9600;   break;
    #ifdef B14400
    case 14400:  brate=B14400;  break;
    case 19200:  brate=B19200;  break;
    #ifdef B28800
    case 28800:  brate=B28800;  break;
    case 38400:  brate=B38400;  break;
    case 57600:  brate=B57600;  break;
    case 115200: brate=B115200; break;
    cfsetispeed(&toptions, brate);
    cfsetospeed(&toptions, brate);

    // 8N1
    toptions.c_cflag &= ~PARENB;
    toptions.c_cflag &= ~CSTOPB;
    toptions.c_cflag &= ~CSIZE;
    toptions.c_cflag |= CS8;

    // no flow control
    toptions.c_cflag &= ~CRTSCTS;

    toptions.c_cflag |= CREAD | CLOCAL;  // turn on READ & ignore ctrl lines
    toptions.c_iflag &= ~(IXON | IXOFF | IXANY); // turn off s/w flow ctrl

    toptions.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // make raw
    toptions.c_oflag &= ~OPOST; // make raw

    // see: http://unixwiz.net/techtips/termios-vmin-vtime.html
    toptions.c_cc[VMIN]  = 0;
    toptions.c_cc[VTIME] = 20;
    if( tcsetattr(fd, TCSANOW, &toptions) < 0) {
        perror("init_serialport: Couldn't set term attributes");
        return -1;

    return fd;

// writes buf to serial port

int serialport_write_period() {
  int len;
  sprintf(buf, "%d\n", period);
  printf("spwp:%d\n", buf);
  len = strlen(buf);
  int n = write( fd, buf, len);
  if (n != len)
    return -1;
  return 0;

for debugging purposes, a print statement is included to show each time this function has been called.


The buzzer seems to be either deteriorating or simply not suited for this kind of output. The first couple times readNote.asm ran successfully, a new note was played every half second like it was supposed to. Unfortunately, there was only minor alterations in sound between each note, and after a few more tests, differentiation between notes was lost completely, even though no code affecting the tones was changed. Shortly after this, buzzer began to emit notes only sporadically, and with a much weaker sound.

I thought there also might be a chance that pins on the Arduino were being damaged. Originally, when I tested the buzzer with a resistor, too much current was blocked and no sound came out at all. I tried every value of resistor I could find in the cabinet, but they all had the same result, so in the end, my Arduino and buzzer were wired together with no resistor. Rae is using the exact same type of buzzer and hers works with a resistor, so I tested it with her buzzer and resistor wired up to a PWM pin I hadn't yet used on my Arduino (so it was definitely not damaged). This still resulted in no sound being emitted. I tried my buzzer again on an unused PWM pin, and the same sporadic weak sound came out, so this is probably an issue with the buzzer and not the Arduino.


Since outputting tones to the Arduino took *much* more time than anticipated, I was unfortunately never able to get to working with the potentiometers. Here is how they were essentially supposed to work:

The loop in the Arduino sketch would contain the lines

int newTone = AnalogRead(potPin1);
int newTempo = AnalogRead(potPin2);

potPin1 and potPin2 would both be set to analog input pins.

newTone and newTempo would both be passed to the C program using Serial.write();
The C program would store each of these in a buffer (say toneBuf and tempoBuf ), which would allow the assembly program to access them.

The finalized assembly program is actually supposed to run repeatedly. I tested it doing this by having asm_main as an endless loop, but this got it caught up in doing absolutely nothing. The other option would be to have a loop in the body of the C program that repeatedly calls asm_main.

The start of asm_main would check toneBuf and tempoBuf and see if the value had changed since the last time it checked (it would store these values in its own variables and compare these to the current buffer values). When the program found that values had been changed, it would call the appropriate function(s).

changeTempo :
readMelody.asm currently uses two delay loops that each run 500000000 times (approximately .5 seconds each). The finalized version of this program was intended to instead have delay loops that ran differing numbers of times, depending on the value read from the potentiometer controlling tempo. One way would be to have a variable "tempo" whose value is determined by the value contained in tempoBuf. Some mathematical operation would reduce the value in tempoBuf to a small integer, like 1, 2, 3, 4, etc, and this value would be stored in tempo. The delay would consist of an outer loop that ran tempo times, and an inner loop that still maintained the 1/2 second delay. The result of this is that tones are played for longer or shorter durations.

Here, a mathematical operation would be performed to reduce the value in toneBuf to something like 10, 20, 30, etc and store it in a variable toneVal. It would also check if the value in toneBuf was greater or less than the previous value. If greater, it would add toneVal to every entry in the melody array, and if less than, it would subtract toneVal from every entry in the melody array. Then this way, when the array is played, it would have a change in pitch (incidentally it would be possible to reach different octaves this way, but precision would be difficult and it would still be impossible to play a melody that spanned multiple octaves).

A third potentiometer was intended to be used for volume with this project was designed. The second program in the PlayMelody tutorial says it uses analogWrite to control volume. I tried to test this on my Arduino and buzzer, but didn't get a change in volume, and given the buzzer's overall behavior during this project, it's possible the volume simply doesn't affect it. If I *had* been able to work with volume, this function would have remained in the Arduino sketch, using analogWrite like in the tutorial (assuming this method does work), so it would have been something like :

int vol = analogRead(potPin3);

and then when the sound was output:

analogWrite(speakerOut, vol);