Comenzaremos con la breakout board de Sparkfun. Esta placa tiene todos los pines del BMA180 separados y dos capacitores como filtros.
Descripción de los pines
VDD: Alimentación del sensor.GND: Conexión a tierra.
INT: Salida Digital. Indica si un evento de interrupción ha ocurrido. Lógica positiva.
CS: Entrada digital.Chip select. Corresponde al pin CSB. Cuando este pin esta a 0V, se activa la funcionalidad SPI.
SCK: Entrada digital. Entrada de la señal de reloj.
SDO: Entrada/salida digital. Salida SPI o configuración de la dirección I2C.
SDI: Entrada/salida digital. Entrada SPI o datos seriales I2C.
VIO: Entrada de alimentación digital. Corresponde al pin VDDIO. La interfaz serial con el microcontrolador puede manejar voltajes diferentes que la alimentación del sensor. Por ejemplo, el sensor pude ser alimentado a 2.0V y la interfaz serie puede trabajar a 3.6V.
Comunicación SPI
Escogí el protocolo SPI sobre el I2C por que el primero es mas rápido y mas sencillo de implementar, ocupa menos componentes y la velocidad de transmisión de datos es mayor. Pero si quisiera usar otros componentes extras como un reloj en tiempo real, lo mejor sería usar el I2C ya que el direccionamiento de cada dispositivo se hace por software y no por hardware.La comunicación se hace de la siguiente manera:
- CSB se pone a cero
- El comando en SDI es fijo (latched) por el BMA180 en el flanco de subida de SCK y SDO cambia en el flanco de bajada de SCK.
- La comunicación inicia cuando CSB se pone a cero y se detiene cuando CSB se pone a uno.
- Durante estas transiciones en CSB, SCK debe estar en alto.
- Cuando CSB=1, no se permite ningún cambio en SDI (los comandos enviados no hacen nada) cuando SCK=1 (para evitar cualquier condición de arranque o de paro para la interfaz I2C).
Para la lectura, la secuencia consiste un un byte de control que define la primera dirección a ser leída seguida por los bytes de datos. Las direcciones son incrementadas automáticamente.
Registros
La siguiente imagen muestra el mapa de memoria de todos los registros que son accesibles externamente y son necesarios para la operación del BMA180.Las columnas de la izquierda dan información acerca de las direcciones de memoria. Las columnas restantes muestran el contenido de cada uno de los bits. Los colores de los bits indican si son solo lectura, solo escritura o lectura/escritura. La parte de memoria que no es EEPROM es volátil así que cada registro deberá ser reescrito después de cada encendido.
Mapeo de la memoria global: información general
El mapa de la memoria global del BMA180 proporciona tres niveles de acceso:Región de la memoria | Contenido | Nivel de acceso |
Registros operacionales | Registros de datos, registros de control, registros de estado, configuración de la interrupción. | Acceso directo vía la interfaz serie. |
Registros de la configuración por defecto. | Valores por defecto para los registros operacionales, aceleración y valores de calibración de la temperatura. | Acceso bloqueado por defecto; el acceso se habilita poniendo el bit de control en los registros operacionales por medio de la interfaz serie. |
Registros reservados de Bosch Sensortec | Registros internos de calibración. | Protegidos. |
La EEPROM puede ser usada para establecer los valores por defecto para la operación del sensor. La EEPROM es de escritura indirecta solamente. Los valores de los registros de la EEPROM son copiados al registro imagen después del encendido o de un soft reset. La descarga de los bytes de la EEPROM a la imagen también se hace cuando el contenido de la EEPROM ha cambiado por un comando de escritura. Después de cada comando de escritura la EEPROM debe ser reiniciada por un soft reset.
Circuito de prueba
Todo el circuito se alimenta con 3.3V, ya que el sensor puede funcionar con un máximo de 3.6VDC. Los pines TX y RX los conecto al conversor serial a usb. Como el pin RX del microcontrolador es el mismo que el pin SDO, necesario para la comunicación SPI, tengo que hacer uso de RS232 por software, y el puerto B para detectar la recepción de datos por medio del puerto serie.Firmware
En este ejemplo el microcontrolador se comunica con el sensor BMA180 a través de su interfaz SPI por hardware. Cuando el sensor se encuentra en una condición de alta aceleración, se activa la interrupción. El pin de interrupción del sensor esta conectado al microcontrolador. Cuando se produce esta interrupción, el cambio de estado en el puerto B genera una interrupción dentro del microcontrolador que enciende el LED.La comunicación entre la PC u otro dispositivo compatible con el estándar RS232 es asíncrona, a una velocidad de 9600bps.
A continuación el código:
main4550gui.h
main4550gui.c
Esta tabla muestra las equivalencias entre las diferentes configuraciones de la polaridad, según el fabricante:
MOTOROLA | MICROCHIP | CCS ----------------------------------------------------------------------- SPI Mode 0,0 | CKP = 0, CKE = 1 | SPI_L_TO_H | SPI_XMIT_L_TO_H SPI Mode 0,1 | CKP = 0, CKE = 0 | SPI_L_TO_H SPI Mode 1,0 | CKP = 1, CKE = 1 | SPI_H_TO_L SPI Mode 1,1 | CKP = 1, CKE = 0 | SPI_H_TO_L | SPI_XMIT_L_TO_HEn el capítulo 8.4.1 de la hoja de datos se describe la transmisión SPI. En la gráfica se puede apreciar que la recepción de datos por el puerto SPI (SDI) va de ALTO a BAJO y leyendo la descripción, se deduce que la transmisión (SDO) va de BAJO a ALTO. Esto en CCS corresponde a la configuración SPI_H_TO_L | SPI_XMIT_L_TO_H y si queremos configurar directamente los registros del microcontrolador, tomamos CKP = 1, CKE = 0.
La instrucción
#use fixed_io(b_outputs=pin_b1)
se utiliza para que el pin B1 siempre sea una salida. Cuando ocurre una interrupción en el puerto B, para limpiar la bandera, tenemos que leer lo que exista en el puerto, convirtiendo los pines en entradas. Esto no lo podemos permitir por que SDO se encuentra entre estos pines y es la salida de datos hacia el sensor.
Después definimos los alias con la instrucción #define. Esto lo hago para tener mas claridad en el código, pues en lugar de tener que escribir el numero hexadecimal de la dirección del registro que queremos leer o escribir, escribimos su equivalente en forma de texto:
#define BMA180_ID 0x00 #define BMA180_ACC_X_LSB 0x02 #define BMA180_ACC_X_MSB 0x03 #define BMA180_ACC_Y_LSB 0x04 #define BMA180_ACC_Y_MSB 0x05 #define BMA180_ACC_Z_LSB 0x06 #define BMA180_ACC_Z_MSB 0x07 #define BMA180_TEMP 0X08 #define BMA180_CTRL_REG0 0X0D #define BMA180_CTRL_REG3 0X21 //registro para habilitar las interrupciones #define BMA180_HY 0x23 #define BMA180_HIGH_LOW_INFO 0X25 #define BMA180_LOW_DUR 0X26 #define BMA180_HIGH_DUR 0X27 #define BMA180_LOW_TH 0X29 #define BMA180_HIGH_TH 0X2A #define BMA180_OFFSET_T 0x37 #define True 0x00 #define False 0x01 #define dummy_data 0x55 #define INTCON 0xFF2 #define CS PIN_C0 #define high_th 0xff //define el valor minimo para activar la interrupcion high G, de 0x00 a 0xff #define hli_h_xyz 0xE0 //activa las interrupciones high g de los tres ejes y desactiva el filtro #define hli_h_x 0x80 //activa solo las interrupciones high g del eje x y desactiva el filtro #define hli_h_y 0x40 #define hli_h_z 0x20 #define offset_t 0x68 //el primer read es el que indica si es de 12 o 14 bits //asi que deberia estar en cero, los otros siete corresponden al offset
void rb_isr(void)
#INT_RB void rb_isr(void) { int dummy; //neceario para la instruccion input_b output_high(pin_A1); delay_ms(100); output_low(pin_A1); dummy = input_b();//es necesario leer del puerto b para limpiar la bandera //fprintf(COM1,"interrupcion"); clear_interrupt(INT_RB); gbl_int= 1; }Cuando ocurre una interrupción lanzada por un cambio en el puerto B debido a que el pin INT del sensor se activó, se ejecuta esta función. En este caso solo se enciende el LED que indica una condición de interrupción, como lo es una alta aceleración. Es necesario que la detección se haga de esta forma por la rapidez que se necesita para ejecutar una acción debido a un cambio en la aceleración.
void int_ext_isr(void)
#INT_EXT2 void int_ext_isr(void) { int recibido; int templsb; int temp; int tempmsb; int16 tempw; recibido = fgetc(COM1); if (recibido == 0x31) { fputc(0x31,COM1); temp = readData(BMA180_ID); fputc(temp,COM1); fputc(0x0a,COM1); } else if(recibido ==0x32) { fputc(0x32,COM1); temp= readData(BMA180_acc_x_lsb); fputc(temp,COM1); temp= readData(BMA180_acc_x_msb); fputc(temp,COM1); temp= readData(BMA180_acc_y_lsb); fputc(temp,COM1); temp= readData(BMA180_acc_y_msb); fputc(temp,COM1); temp= readData(BMA180_acc_z_lsb); fputc(temp,COM1); temp= readData(BMA180_acc_z_msb); fputc(temp,COM1); temp = readData(BMA180_TEMP); fputc(temp,COM1); fputc(gbl_int); fputc(0x0A,COM1); //readline de python gbl_int = 0; } else if(recibido ==0x33) { templsb= readData(BMA180_acc_x_lsb); tempmsb= readData(BMA180_acc_x_msb); tempw = make16(tempmsb,templsb); tempw = tempw >>2; //si el primer bit de la temperatura es un uno if (tempw & 0x2000) //quitar el complemento a dos tempw |= 0b1100000000000000; printf("X:%Ld ",tempw); templsb= readData(BMA180_acc_y_lsb); tempmsb= readData(BMA180_acc_y_msb); tempw = make16(tempmsb,templsb); tempw = tempw >>2; if (tempw & 0x2000) tempw |= 0b1100000000000000; printf("Y:%Ld ",tempw); templsb= readData(BMA180_acc_z_lsb); tempmsb= readData(BMA180_acc_z_msb); tempw = make16(tempmsb,templsb); tempw = tempw >>2; if (tempw & 0x2000) tempw |= 0b1100000000000000; printf("Z:%Ld ",tempw); temp = readData(BMA180_TEMP); printf("Temp:%d ",temp); if (gbl_int == 0) printf("INT:No \r"); else { printf("INT:Si \r"); gbl_int = 0; } } else if(recibido ==0x34) { eewEnabled(True); writeData(0x2c,0x15);//customer data temp=readData(0x2c); fputc(temp,COM1); } else if (recibido == 0x035) { temp = readData(BMA180_OFFSET_T); fputc(temp,COM1); } }Cuando se reciben datos desde la PC se activa esta interrupción. Dependiendo del byte de entrada, el microcontrolador devuelve algún tipo de información.
Cuando recibe un 1 (0x31) devuelve el ID del sensor.
Cuando recibe un 2 (0x32) devuelve los valores de aceleración, temperatura y el estado de la interrupción en formato simplificado: [2][xlsb][xmsb][ylsb][ymsb][zlsb][zmsb][temp][int][0x0a]
[2] Es el comando recibido.
[xlsb][ylsb][zlsb] El bit menos significativo del valor de la aceleración del eje correspondiente.
[xmsb][ymsb][zmsb] El bit mas significativo del valor de la aceleración del eje correspondiente.
[temp] La temperatura registrada por el sensor.
[int] Indica si ha ocurrido una condición de interrupción en el sensor.
[0x0a] Fin de línea.
Cuando recibe un 3 (0x33) devuelve los valores de aceleración, temperatura y el estado de la interrupción en formato largo. En este caso hago la conversión de los valores de aceleración en el microcontrolador, pues el tiempo de retardo no es importante.
Cuando se recibe un 4 (0x34) escribe 0x15 en el registro customer data, y lo devuelve. Esto es solo para probar la escritura y lectura del registro.
Cuando recibe un 5 (0x35) devuelve el offset_t que es el valor de calibración del sensor de temperatura.
void main()
void main() { setup_adc_ports(NO_ANALOGS|VSS_VDD); setup_adc(ADC_OFF); setup_psp(PSP_DISABLED); setup_spi(SPI_MASTER|SPI_H_TO_L|SPI_XMIT_L_TO_H|SPI_CLK_DIV_64); setup_wdt(WDT_OFF); setup_timer_0(RTCC_INTERNAL); setup_timer_1(T1_DISABLED); setup_timer_2(T2_DISABLED,0,1); setup_timer_3(T3_DISABLED|T3_DIV_BY_1); setup_comparator(NC_NC_NC_NC); setup_vref(FALSE); enable_interrupts(INT_EXT2_H2L); enable_interrupts(INT_RB); enable_interrupts(GLOBAL); setup_oscillator(OSC_8MHZ|OSC_INTRC|OSC_PLL_OFF); set_tris_a(0); output_low(pin_a1); // apagar led initBMA180(); }En esta parte se inicializa el microcontrolador. Se configuran las interrupciones y los registros. También se inicializa el sensor.
void initBMA180()
void initBMA180() { int ctrl_reg3; eewEnabled(TRUE); ctrl_reg3 = readData(BMA180_CTRL_REG3); //poner a uno high_int que habilita high_th_criteria para generar la interrupcion ctrl_reg3 |= 0x20; //activar new data interrupt //ctrl_reg3 |= 0x2; //writeData(BMA180_CTRL_REG3,ctrl_reg3); //escribir el valor minimo para ejecutar la interrupcion writeData(BMA180_HIGH_TH,HIGH_th); //escribir el valor de hysteresis de la interrupcion, como no me interesa este valor le pondre cero writeData(BMA180_HY,0x02); //activar las interrupciones y desactivar el filtro writeData(BMA180_HIGH_LOW_INFO,hli_h_x); //definir el tiempo que tiene que pasar para que se active la interrupcion con las condiciones //dadas anteriormente. Sin espera = 0. writeData(BMA180_HIGH_DUR, 0x00); //modificar el registro offset t, el valor de fabrica es 0x64 y para valores //proximos a 30 grados me da 16d writeData(BMA180_OFFSET_T,offset_t); }Inicializa el sensor con la siguiente configuración:
- Interrupción de detección de alta aceleración habilitada.
- Se establece el valor mínimo para la activación de la interrupción.
- Sin histéresis ni filtro.
- Sin tiempo de espera para la activación de la interrupción.
- Se calibra el termometro.
int readData(int direccion)
int readData(int direccion) { int data; direccion |= 0x80; output_low(CS); data = spi_read(direccion); data = spi_read(dummy_data); output_high(CS); return data; }Función que se utiliza para leer cualquier dato en la dirección especificada. Cuando se leen los datos, es necesario poner el primer bit a 1. Esto se hace haciendo una OR con el byte de la dirección. Primero se escribe la dirección y el dato leído es un dato sin importancia. En la segunda instrucción de lectura se escribe una dirección dummy y el dato leído es el que buscamos.
void writeData(int direccion, int data)
void writeData(int direccion, int data) { int dummy_read; output_low(CS); direccion &=0x7f;//0x7F pone el primer bit a 0 para escritura spi_write(direccion ); //apuntar hacia el registro direccion dummy_read = spi_read();//dummy read spi_write(data); //escribir datos dummy_read =spi_read(); //dummy read output_high(CS); }Función que se utiliza para escribir el byte data en la dirección especificada. Primero se escribe la dirección a la que se quiere escribir. Después se le un dato sin importancia. Luego se escribe el byte data de configuración y se hace una lectura sin importancia para completar el ciclo.
void eewEnabled(short Habilitar)
void eewEnabled(short Habilitar) { int ctrl_reg0; if (Habilitar== True) { ctrl_reg0 = readData(BMA180_CTRL_REG0); ctrl_reg0 |=0x10;// pone el bit 5 del ctrl_reg0 a uno (ee_w), deja los demas bits igual writeData(BMA180_CTRL_REG0,ctrl_reg0); } else { ctrl_reg0 = readData(BMA180_CTRL_REG0); ctrl_reg0 &=0xEF;// pone el bit 5 del ctrl_reg0 a cero (ee_w), deja los demas bits igual writeData(BMA180_CTRL_REG0,ctrl_reg0); }Habilita o deshabilita la posibilidad de escribir en la imagen.
Interfaz de usuario
La interfaz de usuario utilizada es la siguiente:Los tres vertical layer que se encuentran en el centro contendrán las gráficas de los tres ejes. En la parte superior derecha esta un QLabel dentro de un QFrame que muestra una imagen cuando se detecte una condición de interrupción. En el centro se dibujará un QwtThermo mostrando la temperatura del sensor y abajo se muestran los valores numéricos de las aceleraciones.
Código completo
TESTDIGI.PYWLa base de este programa se toma de los ejemplos anteriores con algunas modificaciones que se muestran a continuación.
Función crearTermo(self)
def crearTermo(self): termo = Qwt.QwtThermo(self) termo.setOrientation(QtCore.Qt.Vertical, Qwt.QwtThermo.RightScale) termo.setRange(0, 85) termo.setFillColor(QtCore.Qt.green) termo.setPipeWidth(100) termo.setAlarmColor(QtCore.Qt.red) termo.setAlarmLevel(40) termo.setAlarmEnabled(True) return termoEsta función devuelve un objeto QwtThermo con una orientación vertical (cambia de abajo hacia arriba) y con una escala en la parte derecha. El rango de temperatura es de 0° a 85°. La alarma es el cambio de color que sufre la barra después de cruzar un umbral predefinido. En este caso, después de que el valor de temperatura sube de 40 el color de la barra cambia a rojo.
Función timerEvent(self)
def timerEvent(self): self.ptoSerial.write("2") self.actualizarPlot() self.actualizarTermo(self.temp) #timerEventEn ejemplos anteriores el microcontrolador siempre estaba transmitiendo por el puerto serie la información del sensor. En este ejemplo es necesario mandar un byte que pide algun tipo de información al sensor. Para recibir los datos de aceleración en formato corto se escribe un 2, después se actualiza la gráfico con los datos leídos y también el termómetro.
Función decodeData(self, data)
def decodeData(self, data): #el formato esperado son 8 bytes ordenados de la siguiente forma xnynznCRLF #donde n es el byte que representa la lectura del A/D precedido del eje medido y el final son los caracteres de retorno de carro y nueva linea #necesarios para la instruccion readline #eje x convertido de tipo byte a su representacion en numeros decimales con la instruccion ord #el 330/255 es la constante que convierte el tipo byte 0-255 numeros al maximo de 330 milivolts donde cada numero tipo byte equivale a (330/255) milivolts if (len(data) <8): print "Error" return 0, 0, 0, 0, 0 xlsb = ord(data[1]) xmsb = ord(data[2]) ylsb = ord(data[3]) ymsb = ord(data[4]) zlsb= ord(data[5]) zmsb = ord(data[6]) temp = ord(data[7]) interrupcion = ord(data[8]) x = self.byte2Word(xmsb, xlsb) #convertir los dos bytes a tipo word x = x >>2 #eliminar los dos bits que no se utilizan #quitar complemento a 2 #si el bit 14 de x es 1 entonces es signo negativo if x & 0x2000: #poner el bit 15 y el bit 16 a unos, esto hace que el tipo word de 14 bits del acelerometro (bit de signo es el 14), se convierta a tipo word normal #(bit de signo es el 16) y un uno en el 15 pues va a cambiar de uno a cero x = x | 0xC000 #cambiar todos los ceros por uno #pero parece que no quita el uno del signo x = ~ x #quitar el uno del bit de signo (bit 16); x= x & 0x7fff #sumar 1 x = x + 1 #poner a negativo x = x * -1 y = self.byte2Word(ymsb, ylsb) y = y >>2 if y & 0x2000: y = y | 0xC000 y = ~ y y = y & 0x7fff y = y + 1 y = y * -1 z = self.byte2Word(zmsb, zlsb) z = z >>2 if z & 0x2000: z = z | 0xC000 z = ~ z z = z & 0x7fff z = z + 1 z = z * -1 return x, y, z, temp, interrupcion #decodeDataSe espera recibir 10 bytes, el primero y el ultimo son de control. Con la función byte2Word almacenamos los dos bytes recibidos por el puerto serie en una sola variable. Después hacemos un desplazamiento a la derecha para descartar los dos bytes menos significativos que corresponden a un 0 y a la bandera de nuevos datos disponibles respectivamente. Pero al hacer este desplazamiento también desplazamos el bit de signo, por eso tenemos que hacer la siguiente comparación:
if x & 0x2000:el numero hexadecimal 0x2000 equivale a 0010 0000 0000 0000, donde el único uno corresponde al bit que esta en la posición catorce. Como anteriormente se dijo, convertí el dos bytes a una sola variable tipo word (16 bits) y con el desplazamiento el bit mas significativo (bit 16) que corresponde al bit de signo paso a la posición 14. Si la condición anterior se cumple, quiere decir que el bit 14 es un uno, y por lo tanto el numero es un numero negativo en formato complemento a 2 con el siguiente código:
if y & 0x2000: y = y | 0xC000 y = ~ y y = y & 0x7fff y = y + 1 y = y * -1El numero 0xC000 es igual a 1100 0000 0000 0000. Con la operación OR cambiamos los dos MSB a 1. Después cambiamos todos los unos por ceros y viceversa con el operador ~ .
El numero 0x7FFF es igual a 0111 1111 1111 1111. Con la operación AND, ponemos a cero el MSB y los demás bits los dejamos igual.
Sumamos un uno y multiplicamos por menos 1 porqué ya establecimos que si se realizan todos estos pasos, es por que el numero es negativo.
Esto lo hacemos tres veces pues son tres ejes.
Función toggleMonitoreo(self)
def toggleMonitoreo(self): if self.ui.actionIniciar_monitoreo.text() == 'Iniciar monitoreo': #elimina el buffer del puerto para que no se grafiquen los datos almacenados porque aunque detenga el timer #y no se siga graficando, el puerto sigue abierto y recibiendo datos en el buffer de entrada self.ptoSerial.flushInput() self.ptoSerial.write(self.readID) #leer la id del acelerometro lectura = self.ptoSerial.readline() if lectura: id = ord(lectura[1]) if id == 3: self.ui.actionIniciar_monitoreo.setText("Detener monitoreo") self.timer.start(self.muestreo) else: QtGui.QMessageBox.warning(None, self.trUtf8("Error"), self.trUtf8("""El acelerometro no esta presente."""), QtGui.QMessageBox.StandardButtons(\ QtGui.QMessageBox.Ok)) else: QtGui.QMessageBox.warning(None, self.trUtf8("Error"), self.trUtf8("""El acelerometro no esta presente."""), QtGui.QMessageBox.StandardButtons(\ QtGui.QMessageBox.Ok)) else: self.ui.actionIniciar_monitoreo.setText("Iniciar monitoreo") self.timer.stop() #toggleMonitoreoEn este ejemplo antes de iniciar el monitoreo de los valores de aceleración, se envía el byte de control que devuelve el valor de identificación del sensor. Si el valor devuelto es el esperado (0x03), entonces se continua con el monitoreo, si no lo es, aparece un error.
Función actualizarTermo(self, Temp)
def actualizarTermo(self, temp): self.termo.setValue(temp) #actualizarTermoActualiza el control QwtThermo con el valor Temp.
Función interruptDetected(self, detectada)
def interruptDetected(self, detectada): if detectada == True: self.ui.lblInt.setPixmap(QtGui.QPixmap(":\img\mov.png")) self.ui.frmInt.setAutoFillBackground(True) self.timer.stop() self.timer.start(self.muestreo) else: self.ui.lblInt.setPixmap(QtGui.QPixmap("")) self.ui.frmInt.setAutoFillBackground(False) #interruptDetectedMuestra una imagen en el QLabel indicando que ha ocurrido una condición de interrupción.
Función byte2Word(self, msb, lsb)
def byte2Word(self, msb, lsb): word = msb <<8 word = word | lsb return word #byte2WordLos bytes (8 bits) msb y lsb los convierte en una sola palabra tipo word (16 bits).
0 comentarios:
Publicar un comentario