This is the third part in a series of articles in which I’m ‘highlighting’ what I consider to be lousy code provided by IAR in their NXP LPC1764 development kit. This example is taken from the Accelerometer project. The accelerometer is accessed via an I2C bus, and so the main purpose of this example code is to demonstrate how to use the I2C interface. If you have ever had to write an I2C driver you will know that they are deceptively hard. In my case the project I’m working on does indeed need an I2C driver – and so I was expecting to be able to borrow heavily from the IAR code. Alas I found the code to be almost completely useless. To see why, read on …
Upon examination of the I2C driver the first thing I found were a number of ‘helper functions’. For example:
/*************************************************************************
* Function Name: I2C_EnableI2C
* Parameters: void
* Return: void
* Description: Enable I2C device.
*************************************************************************/
#pragma inline
void I2C_EnableI2C(void)
{
I2C2CONSET |= I2CON_I2EN;
}
While in general I’m all in favor of these types of helper functions, I had three problems with the way they were implemented.
- Clearly if anyone other than the I2C driver was to invoke these functions then really bad things would happen. Despite this, the author has not seen fit to declare the function static, thus opening them up for the world to use.
- These helper functions are located at the top of the file. There has been an interesting thread of discussion on this blog regarding whether this is good, bad or indifferent practice. Personally I strongly dislike it as it puts an implementation minutiae in my face – when want I want to know is ‘what’s the big picture’. Thus IMHO these types of functions should be at the end of the file – but I recognize that there are those who disagree!
- The next thing that puzzled me was that I couldn’t find the definition of ‘I2CON_I2EN’ anywhere in the file. Clearly it had to exist somewhere and so a project wide search showed that it was defined in the the file i2c_drv.h. Now the only module that could possibly have any interest in the value of ‘I2CON_I2EN’ is the driver code i2c_drv.c. Despite this, the author has seen fit to put the define in the header file rather than the C file. I have to admit that I have seen this done by a lot of other people – and for the life of me I cannot understand the logic. Why would you go out of your way to make public something that inherently should remain private? When combined with the fact that the helper functions are also public the only possible explanation is that the author expected code outside the driver to interact with the I2C hardware!
I also noticed that these functions were being declared without a prototype – and yet the code was compiling without a warning. I thus examined the compilation options – and sure enough the author had the ‘Require prototypes’ box unchecked.
Now to be fair to the author, and for reasons only known to IAR, this is the default setting. Notwithstanding this, building code without requiring prototypes is just inexcusable.
Anyway, next up in the driver was an initialization function and then this function:
/*************************************************************************
* Function Name: I2C_MasterWrite
* Parameters: unsigned char addr -- the slave address which you send message to
* unsigned char *pMsg -- the point to the message
* unsigned long numMsg -- the byte number of the message
* Return: int
* 0: success
* non-zero: error number
*
* Description: Transmit messages
*
*************************************************************************/
int I2C_MasterWrite (unsigned char addr, unsigned char *pMsg , unsigned long numMsg)
{
return I2C_Transfer (addr, pMsg , numMsg, WRITE, 0);
}
I looked at the header and was immediately disappointed as I noticed the following:
- The author had suddenly and mysteriously switched to using the built in data types (char, int, long), despite the rest of the code in the project using defined data types Int32u etc. Whenever I see lack of data type discipline I know I’m in for a bad experience.
- The first parameter was described as “the slave address which you send message to”. Well anyone that has done any work with the I2C bus knows that the concept of an address is poorly defined. In the I2C standard, addresses are 7 bits. However, what gets sent to an I2C slave is the address left shifted one place with a R/W bit placed in the LSB position. As a result, many people consider the slave address to be the byte that is transmitted with the LSB masked out. While I’m not particularly interested in arguing about which camp is correct, the point is that tremendous ambiguity exists in this area. As a result a well written I2C driver should be unambiguous about what it expects for a slave address. In this case I’m clueless as to what to pass to the driver.
- The second parameter is a pointer to the message to be transmitted. However, the pointer is not defined as pointing to const – which it should be. Again this is indicative of someone who is sloppy.
- The third parameter is described as “the byte number of the message”. I’m sure the author meant “the number of bytes in the message”. I’m not going to complain about this – on the assumption that the author’s native language is not English. Notwithstanding this, I do still have a problem with the third parameter – and that’s the choice of the data type. I have written a lot of I2C code in my life – and I have never been in a situation where I needed to send so many bytes to a peripheral that the length byte needed to be an unsigned long. I would have thought a 16 bit variable would have been quite enough.
- The return type was defined as ‘int’ with zero being success and non-zero being an error number. However, it was completely unclear what were the possible list of error numbers. IMHO if you are going to return an error code from a function then the return type should be an enumeration – which presumably via the suitable choice of enumeration names would describe the particular error. Notwithstanding this, I hunted around and in the header file for the driver I found the following:
/* Status Errors */ #define I2C_OK 0 // transfer ended No Errors #define I2C_IDLE 1 // bus idle #define I2C_BUSY 2 // transfer busy #define I2C_ERROR 3 // err: general error #define I2C_NO_DATA 4 // err: No data in block #define I2C_NACK_ON_DATA 5 // err: No ack on data #define I2C_NACK_ON_ADDRESS 6 // err: No ack on address #define I2C_DEVICE_NOT_PRESENT 7 // err: Device not present #define I2C_ARBITRATION_LOST 8 // err: Arbitration lost #define I2C_TIME_OUT 9 // err: Time out occurred #define I2C_SLAVE_ERROR 10 // err: Slave mode error #define I2C_INIT_ERROR 11 // err: Initialization (not done) #define I2C_RETRIES 12 // err: Initialization (not done)
These appeared to fit the bill – or so I thought. However, looking at the body of the above code, it was apparent that all the real work got done in the I2C_Transfer() function and so I turned my attention to it. The first part of the code is below:
/*************************************************************************
* Function Name: I2C_Transfer
* Parameters: unsigned char addr -- the slave address which you send message to
* unsigned char *pMsg -- the point to the message
* unsigned long numMsg -- the byte number of the message
* LPC_I2C_TransMode_t transMode -- Read, Write, Write then read
* unsigned long numWrite -- this is only for "Write then read" mode
*
* Return: int
* 0: success
* non-zero: error number
*
* Description: Transfer messages
*
*************************************************************************/
int I2C_Transfer (unsigned char addr, unsigned char *pMsg , unsigned long numMsg,
LPC_I2C_TransMode_t transMode, unsigned long numWrite)
{
unsigned int timeout = DLY_I2C_TIME_OUT;
if (transMode == WRITETHENREAD)
{
if (numWrite >= numMsg)
return 1;
else
I2CMsg.nrWriteBytes = numWrite;
}
else
I2CMsg.nrWriteBytes = 0;
...
}
In looking at this code I noticed that it would immediately return the value of 1 if some criterion was met. However looking at my list of error codes, a 1 is ‘I2C_IDLE’. That clearly didn’t make sense. Despite this, on examining the rest of the function I found that it did indeed return the error codes listed above. In other words the author was just too lazy to define another error code and so he simply reused an existing error code – even though it had nothing to do with the issue at hand. By this point I was utterly convinced that the code was a complete train wreck.
While I also found a lot more to dislike about the implementation, my final complaint is actually a lot more subtle – but in many ways the most important of all. Having reviewed the entire driver, I realized that it didn’t contain an interrupt handler. The implication (which was borne out by closer code inspection) was that the I2C driver worked using polling. Now if you have done any work with I2C you will know that it is quite legal for an addressed I2C slave to stretch the clock and to in general take its sweet time responding to read requests. As a result it is quite usual for an I2C transaction to take a millisecond to complete. Throw in the cases where bus contention occurs and the worst case I2C transaction time can rapidly hit multiple milliseconds. Maybe it’s just me, but I don’t invest in a 100 MHz Cortex processor to have it spinning its wheels polling an I2C bus. Now while I can accept that in this case the author’s intention may have been to just demonstrate some basic I2C functionality, at no point in the source code comments or in a readme file did the author state “WARNING: This driver uses polling and may block for up to N ms”. IMHO any code that is likely to block must come with the appropriate warnings. The fact that this one doesn’t tells me a lot about the author’s attitude to real time response.
I think that will do it for the I2C driver. Next up – how to make code maintenance hard.