This video presentation is the eleventh installment of the Essentials of MQX RTOS Application Development training course. In this session, you will be introduced to I2C Driver.
This training was created by Embedded Access Inc., a Freescale sponsored training provider and proven partner.
|Session 11 Course Line||Lab Outline|
First, watch the video for Session 11: I2C Driver.
Then, follow through with the interactive lab assignment below.
SESSION 11: LAB ASSIGNMENT
The purpose of this lab is to add an I2C interface to the Accel task so that it can read data from the accelerometer. The data read will be sent to the Health Task and Theft Task using Message Passing, which was covered in session 4. The Theft task will use this data to determine if there has been any motion and it will act accordingly. This is one of the longer labs, however there are many options to 'cheat' and see what is intended. If you are stuck on a step, open the cheat pop at the end of that step, or the next one in the instructions to see what you need to do. It is worth the investment in time to work through this lab in order to have a better understanding of the I2C code and how the application works.
The objective of this lab is to learn about the I2C by using it to read from the accelerometer. You will also cover aspects of Message Passing that were not handled in Lab 4. This will be accomplished by:
- Creating a Broadcast Message
- Opening and initializing the I2C channel
- Reading data from the accelerometer
- Extracting position data from the accelerometer transmission
- Processing the data to determine if there has been any motion
UPDATING THE MESSAGE PASSING SCHEME TO SEND THE ACCELEROMETER DATA AS A BROADCAST
- We'll start with setting up the environment for the Accel Task to send messages to the Health Task and the Theft Task. This could be done by creating a message and then copying that message so both copies can be sent independently, but instead we'll use the broadcast capability that Message Passing supports. To do this we need to declare an array of queue IDs in lieu of the declaration for the Health Queue ID so you can update the declaration for this at the top of the Accel Task, and then populate the array with the appropriate queue IDs using the _msgq_get_id() function. Keep in mind that a list of queue IDs needs to be terminated with a null entry so the array should have 3 elements. Finally the existing line to set the 'health_qid' can be removed, along with the task_block() function.
- Replace the msgq_send() function already in the Accel Task with the msgq_send_broadcast() function. You can use the NULL Pool ID define (MSGPOOL_NULL_POOL_ID) which means that the system pool will be used for storing the messages, as opposed to a separate pool. Also, since this is a broadcasted message to specified tasks, you can remove the existing line that specifies that the target queue ID is the Health Task.
- The x, y, and z values received from the accelerometer need to be included with the broadcasted message since the Theft Task will require this info to determine if there has been any motion. To do this the message structure in main.h needs to be updated to include a uint16_t value for each of the x, y, and z coordinates since this data coming back from the accelerometer is 16 bits.
- The message being sent out in the Accel Task needs to be updated to include this data, but since the code that reads these values over I2C hasn't been written yet you can simply assign each value to 0 and we'll update it later.
ADDING THE I2C DRIVER
- The first thing that you'll need before you can access I2C is a file handle of type 'MQX_File *'
- After the Accel Task starts and before the while (1) loop, the I2C device needs to be opened which can be done using the fopen() function. The Accelerometer is connected to I2C device 0, so in the fopen() function use 'i2c0:' as the device that is to be opened, and no parameters are required. If the I2C device did not open for some reason a NULL value will be returned, and so it would be good practice to check for a NULL value returned to the file handle and block the task if that is what is happened.
- The I2C controller needs to be put into Master mode which can be done using an ioctl function with the command IO_IOCTL_I2C_SET_MASTER_MODE. No additional parameters are needed. It can be a good idea to check if there is a failure of this command.
- The destination address that the target I2C device uses needs to be set and that can also be done using an ioctl. A parameter is required so first create a 32bit uint called 'param'. After the I2C is put into Master Mode set the parameter to the internal address of the accelerometer which is 0x1D and then make an ioctl call with the IO_IOCTL_I2C_SET_DESTINATION_ADDRESS command and pass in the address parameter.
COMMUNICATING WITH THE ACCELEROMETER
- To communicate with the accelerometer we need an array of uint8_t variables where the data can be stored, so declare this array at the top of the function. Before it will start measuring data the accelerometer must first be turned on which is done by writing to the system control register at address 0x2a and setting bit 0 to logic 1. So our first message to the accelerometer will only be two bytes long and looks like this [0x2a, 0x01], which writes 0x01 to address 0x2a. Set the first two parameters of the data array to these values accordingly.
- Use the _io_write() function to send these two bytes over the I2C bus.
- Afterwards the handle should be flushed using the fflush() function. And since we are done writing to the accelerometer a stop condition needs to be asserted which is done with the ioctl() function using the IO_IOCTL_I2C_STOP command.
- The 3-axis acceleration data is required periodically so the reading of this data should be done in the while(1) loop. First though, it is a good idea in general to reset the destination address in case another task has used the same I2C bus to communicate with a different device since the Accel Task last used the I2C bus. This code can be copied from when it was done in step 8.
- As was done before, you need to specify which registers you want to read, which in this case are the x, y, and z registers that start at address 0x00. Set the first byte of the data array to 0x00 and then write the data out using _io_write() function as was done before, except that only a single byte is to be written.
- In order to clear the output buffer you need to do a flush, but instead of doing an fflush() this needs to be done with an ioctl() call using the IO_IOCTL_I2C_FLUSH_OUTPUT command because we need to know if the ACK was received or not after our byte was transmitted. Therefore, add the 'param' parameter to the call which will contain an indication if the ACK bit was received or not to indicate a Stop condition.
- If the 'param' retuned from the previous IOCTL call is set, the code should NOT continue with this reading of the accelerometer and should just return to the time delay at the top of the while(1) loop (ie check the param value with an 'if' statement and only continue if it is not set).
- A repeated start needs to be generated to indicate that another message is coming from the CPU. This is done using an ioctl call with an IO_IOCTL_I2C_REPEATED_START command which has no parameters.
- The next message then is to indicate that data is to be read, which is done using an ioctl function with an IO_IOCTL_I2C_SET_RX_REQUEST command and a parameter of 6 to indicate the number of bytes to read (3 uint16 values).
- Now we can read the data from the accelerometer that we'll do with a polled loop that runs until all bytes have been read. Initialize a byte counter to zero and then start a Do loop. Inside the loop use the _io_read() function indicating the I2C bus to read from, the data array to write to, and the number of remaining bytes to read. The _io_read() function will return a int32_t (not a uint32_t) value to indicate the number of bytes read, and so the returned value should be saved in a 'result' variable that can be checked for an error.
- If the result is greater than 0 the read was successful and the number of bytes read can be incremented. The Do loop should continue while there are more bytes to read and that an error was not received when writing.
- Use the fflush() function to clear the handle and then send a stop command using the ioctl() function as was done before.
EXTRACTING THE DATA
- The x, y, and z acceleration values can now be extracted from the received data and put into the message that will be broadcast. Move the code that is used to allocate, fill in, and broadcast the message so that it is immediately below the flush and asserting the stop condition you did in step 20.
- The x, y, and z data values were previously set to 0, but this can be updated now to be:
msg->X = (data<<8) | data;Put in a temporary printf statement that prints to the console the X, Y, and Z data that is being broadcast. Note that to clean up the print out you see on the console, it would help to temporarily comment out the printf statements in the Display , Theft, and Health Tasks.
msg->Y = (data<<8) | data;
msg->Z = (data<<8) | data;
- Compile and run the code. While the code is running, pick up the board and rotate it around to generate movement in the x, y, and z axis. Ensure that the print statements show the change in position.
UPDATING THE THEFT TASK
- The Theft Task will read the motion information that it receives in the messages sent from the Accel Task, and if motion above a set threshold is detected a message will be sent to the Display Task. To start then we will need to set up the access to the queue for the Display Task. This requires a queue_id that is initialized using the _msgq_get_id() function that can be added just after you use the msgq_open() function to get the Theft Task queue id.
- In order to detect motion we will need to know the x, y, and z positions reported in the previous message, and we'll need variables that can store the change in position for each of the 3 axis. These should all be uint16_t variables. We will also need to know if we are processing the initial message from the Accel task or not in order to fill in the change in position values correctly, so an appropriate variable should be declared for that. This can be a simple Boolean flag called something like 'first_message' that is initialized to TRUE.
- In the while(1) loop, we no longer need the printf line so that can be removed if you didn't do it previously.
- After the message is received with the _msgq_receive() function the message type should be confirmed to be of ACCEL_MESSAGE type to make sure we are processing the correct message. If this is not the first message from the Accel Task then we should calculate the change in position for each axis by subtracting the previously reported position from the position reported in the current message. These are unsigned values and so if you get a negative value (ie greater than 0x8000) then the change should corrected into an absolute value.
- Regardless if this is the first message since boot up from the Accel Task or not, the passed in position values should be saved into the variables declared in step 26 as the values reported in the previous message since you'll need them when processing the next message.
- We now need to determine if any motion above a set threshold in any of the axis has occurred. Use a change of 512 in any axis as the threshold for motion detection.
- If motion is detected, we want to send a message to the Theft Task. This will require a second message called the 'theft_msg' to be declared (similar to how the message called 'msg' was declared) and then we need to allocate a message with the msg_alloc_system() function. As was done before remember to check if this message returns a NULL.
- If a valid message was allocated, fill in the target queue ID (ie the Display Queue) and the Message Type (ie THEFT_MESSAGE) for our message. Note that THEFT_MESSAGE will have to be added to the list of valid message types in main.h since we haven't used this type before and local 'APPLICATION_MESSAGE' called 'theft_msg' will have to be declared.
- Use the _msgq_send() function to initiate the transmission of this message.
UPDATING THE DISPLAY TASK TO INDICATE WHEN MOTION IS DETECTED
- The Display Task will be receiving messages from the Theft Task when motion was detected, and so this case needs to be added to the switch statement. Add a case for receiving a message of type THEFT_MESSAGE and when this happens set LED 1 to LWGPIO_VALUE_LOW in order to turn that LED on.
- You can remove the printf added to the Accel Task in step 22.
- Compile and run the code. LED 1 (the orange LED) should come on when you pick up the tower kit. LED 1 can be turned off by pressing sw2 on the K70 Tower Board. You can play with different thresholds for the motion detection to make it more, or less sensitive.
Need more help? The full source code for this lab can be found in the 'Lab Source Code' folder here.