Bare metal IIC driver (Linux Driver Development)

Posted by svgmx5 on Sat, 12 Feb 2022 17:33:12 +0100

1. Bare metal drive

  • Single chip microcomputer A and sensor B
  • It is written without the help of the API of linux kernel, that is, bare metal (single chip microcomputer)
  • Principle: to communicate between A and B, we need to write A driver protocol for A and B to communicate. The writing driver is divided into host driver and device driver. In fact, the two can be combined into one document, but they are separated in order to meet the idea of driven separation and layering. For the benefits of this idea, please see another article I wrote Separation and layering of Linux drivers.
  • Well, we already know the general steps are:
    First, write the host driver (that is, use various registers of MCU A). Then write A driver file for the sensor B we use, called device driver (with the help of the API interface written by the host driver), fill in the corresponding sensing information, and then we can communicate normally.

1.1 host driver (MCU A)

iic host driver, (once written, it does not need to be modified) other I2C devices can directly call the API functions provided by the host driver to complete the read-write operation


It can be seen from the above that there are enough pits left in the host driver (the host can connect different devices)

bsp_i2c.h file

#ifndef _BSP_I2C_H
#define _BSP_I2C_H

#include "imx6ul.h"

/* Related macro definitions */
#define I2C_STATUS_OK				(0)
#define I2C_STATUS_BUSY				(1)
#define I2C_STATUS_IDLE				(2)
#define I2C_STATUS_NAK				(3)
#define I2C_STATUS_ARBITRATIONLOST	(4)
#define I2C_STATUS_TIMEOUT			(5)
#define I2C_STATUS_ADDRNAK			(6)

/*
 * I2C Direction enumeration type
 */
enum i2c_direction
{
    kI2C_Write = 0x0, 		/* The master writes data to the slave */
    kI2C_Read = 0x1,  		/* The host reads data from the slave */
} ;

/*
 * Host transmission structure
 */
struct i2c_transfer
{
    unsigned char slaveAddress;      	/* 7 Bit slave address 			*/
    enum i2c_direction direction; 		/* Transmission direction 			*/
    unsigned int subaddress;       		/* Register address			*/
    unsigned char subaddressSize;    	/* Register address length 			*/
    unsigned char *volatile data;    	/* Data buffer 			*/
    volatile unsigned int dataSize;  	/* Data buffer length 			*/
};


/*
 *Function declaration
 */
void i2c_init(I2C_Type *base);
unsigned char i2c_master_start(I2C_Type *base, unsigned char address, enum i2c_direction direction);
unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address,  enum i2c_direction direction);
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status);
unsigned char i2c_master_stop(I2C_Type *base);
void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size);
void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size);
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer);
#endif

bsp_i2c.c

  • At the beginning, any two pins can do SCL and SDA (analog IIC). 51 single chip microcomputer will often simulate II2C.
  • This routine uses the IIC controller of the single chip microcomputer. As long as different register pins are configured, the function can be realized. So you may not strictly see the shadow of the sequence diagram
  • It doesn't say that using IICx (can be 1, 2, 3) to see how many channels your MCU supports. Here is the interface

bsp_i2c.c Documents

#include "bsp_i2c.h"
#include "bsp_delay.h"
#include "stdio.h"

/*
 * @description		: Initialize I2C, baud rate 100KHZ
 * @param - base 	: IIC settings to initialize
 * @return 			: nothing
 */
void i2c_init(I2C_Type *base)
{
	/* 1,Configure I2C */
	base->I2CR &= ~(1 << 7); /* To access I2C registers, you first need to turn off I2C */

    /* Set the baud rate to 100K
     * I2C The clock source of is from IPG_CLK_ROOT=66Mhz
 	 * IC2 Clock = perclk_ Root / divison (ifdr register)
	 * Set register ifdr. Refer to page P1260 of IMX6UL reference manual, table 29-3 for ifdr register,
	 * According to the values in table 29-3, select a frequency division number that is still, for example, in this routine, we
	 * Set the baud rate of I2C to 100K, so when the frequency division value = 66000000 / 100000 = 660
	 * In table 29-3, there is no value of 660, but there is 640, so 640 is used,
	 * That is, the IC bit of register IFDR is set to 0X15
	 */
	base->IFDR = 0X15 << 0;

	/*
     * Set register I2CR and turn on I2C
     * bit[7] : 1 Before enabling other bits of I2C and i2cr registers, this bit must be set to 1 first
	 */
	base->I2CR |= (1<<7);
}

/*
 * @description			: Send restart signal
 * @param - base 		: IIC to use
 * @param - addrss		: Device address
 * @param - direction	: direction
 * @return 				: 0 Normal other values error
 */
unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address,  enum i2c_direction direction)
{
	/* I2C Busy and working from mode, jump out */
	if(base->I2SR & (1 << 5) && (((base->I2CR) & (1 << 5)) == 0))		
		return 1;

	/*
     * Set register I2CR
     * bit[4]: 1 send out
     * bit[2]: 1 Generate restart signal
	 */
	base->I2CR |=  (1 << 4) | (1 << 2);

	/*
     * Set register I2DR
     * bit[7:0] : For the data to be sent, write the slave device address here
     *            Reference: IMX6UL reference manual P1249
	 */ 
	base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);
	
	return 0;
}

/*
 * @description			: Send start signal
 * @param - base 		: IIC to use
 * @param - addrss		: Device address
 * @param - direction	: direction
 * @return 				: 0 Normal other values error
 */
unsigned char i2c_master_start(I2C_Type *base, unsigned char address,  enum i2c_direction direction)
{
	if(base->I2SR & (1 << 5))			/* I2C busy */
		return 1;

	/*
     * Set register I2CR
     * bit[5]: 1 Main mode
     * bit[4]: 1 send out
	 */
	base->I2CR |=  (1 << 5) | (1 << 4);

	/*
     * Set register I2DR
     * bit[7:0] : For the data to be sent, write the slave device address here
     *            Reference: IMX6UL reference manual P1249
	 */ 
	base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);
	return 0;
}

/*
 * @description		: Check and clear errors
 * @param - base 	: IIC to use
 * @param - status	: state
 * @return 			: Status results
 */
unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status)
{
	/* Check for missing arbitrations */
	if(status & (1<<4))
	{
		base->I2SR &= ~(1<<4);		/* Clear arbitration missing error bits 			*/

		base->I2CR &= ~(1 << 7);	/* Turn off I2C first 				*/
		base->I2CR |= (1 << 7);		/* Reopen I2C 				*/
		return I2C_STATUS_ARBITRATIONLOST;
	} 
	else if(status & (1 << 0))     	/* The response signal from the slave is not received */
	{
		return I2C_STATUS_NAK;		/* Return NAK(No acknowledge) */
	}
	return I2C_STATUS_OK;
}

/*
 * @description		: Stop signal
 * @param - base	: IIC to use
 * @param			: nothing
 * @return 			: Status results
 */
unsigned char i2c_master_stop(I2C_Type *base)
{
	unsigned short timeout = 0xffff;

	/*
	 * Clear the three bits of bit[5:3] of I2CR
	 */
	base->I2CR &= ~((1 << 5) | (1 << 4) | (1 << 3));

	/* Wait for busy end */
	while((base->I2SR & (1 << 5)))
	{
		timeout--;
		if(timeout == 0)	/* Timeout jump */
			return I2C_STATUS_TIMEOUT;
	}
	return I2C_STATUS_OK;
}

/*
 * @description		: send data
 * @param - base 	: IIC to use
 * @param - buf		: Data to send
 * @param - size	: Size of data to send
 * @param - flags	: sign
 * @return 			: nothing
 */
void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size)
{
	/* Wait for the transfer to complete */
	while(!(base->I2SR & (1 << 7))); 
	
	base->I2SR &= ~(1 << 1); 	/* Clear flag bit */
	base->I2CR |= 1 << 4;		/* send data */
	
	while(size--)
	{
		base->I2DR = *buf++; 	/* Write the data in buf to I2DR register */
		
		while(!(base->I2SR & (1 << 1))); 	/* Wait for the transfer to complete */	
		base->I2SR &= ~(1 << 1);			/* Clear flag bit */

		/* Check ACK */
		if(i2c_check_and_clear_error(base, base->I2SR))
			break;
	}
	
	base->I2SR &= ~(1 << 1);
	i2c_master_stop(base); 	/* Send stop signal */
}

/*
 * @description		: Read data
 * @param - base 	: IIC to use
 * @param - buf		: Read data
 * @param - size	: Size of data to read
 * @return 			: nothing
 */
void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size)
{
	volatile uint8_t dummy = 0;

	dummy++; 	/* Prevent compilation errors */
	
	/* Wait for the transfer to complete */
	while(!(base->I2SR & (1 << 7))); 
	
	base->I2SR &= ~(1 << 1); 				/* Clear interrupt hold bit */
	base->I2CR &= ~((1 << 4) | (1 << 3));	/* receive data  */
	
	/* If only one byte of data is received, NACK signal is sent */
	if(size == 1)
        base->I2CR |= (1 << 3);

	dummy = base->I2DR; /* False reading */
	
	while(size--)
	{
		while(!(base->I2SR & (1 << 1))); 	/* Wait for the transfer to complete */	
		base->I2SR &= ~(1 << 1);			/* Clear flag bit */

	 	if(size == 0)
        {
        	i2c_master_stop(base); 			/* Send stop signal */
        }

        if(size == 1)
        {
            base->I2CR |= (1 << 3);
        }
		*buf++ = base->I2DR;
	}
}

/*
 * @description	: I2C Data transmission, including read and write
 * @param - base: IIC to be used
 * @param - xfer: Transmission structure
 * @return 		: Transmission result: 0 succeeded, other values failed;
 */
unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer)
{
	unsigned char ret = 0;
	 enum i2c_direction direction = xfer->direction;	

	base->I2SR &= ~((1 << 1) | (1 << 4));			/* Clear flag bit */

	/* Wait for the transfer to complete */
	while(!((base->I2SR >> 7) & 0X1)){}; 

	/* If it is read, the register address should be sent first, so the direction should be changed to write first */
    if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read))
    {
        direction = kI2C_Write;
    }

	ret = i2c_master_start(base, xfer->slaveAddress, direction); /* Send start signal */
    if(ret)
    {	
		return ret;
	}

	while(!(base->I2SR & (1 << 1))){};			/* Wait for the transfer to complete */

    ret = i2c_check_and_clear_error(base, base->I2SR);	/* Check for transmission errors */
    if(ret)
    {
      	i2c_master_stop(base); 						/* Sending error, sending stop signal */
        return ret;
    }
	
    /* Send register address */
    if(xfer->subaddressSize)
    {
        do
        {
			base->I2SR &= ~(1 << 1);			/* Clear flag bit */
            xfer->subaddressSize--;				/* Address length minus one */
			
            base->I2DR =  ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //Write sub address to I2DR register
  
			while(!(base->I2SR & (1 << 1)));  	/* Wait for the transfer to complete */

            /* Check for errors */
            ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
             	i2c_master_stop(base); 				/* Send stop signal */
             	return ret;
            }  
        } while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));

        if(xfer->direction == kI2C_Read) 		/* Read data */
        {
            base->I2SR &= ~(1 << 1);			/* Clear interrupt hold bit */
            i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* Send repeat start signal and slave address */
    		while(!(base->I2SR & (1 << 1))){};/* Wait for the transfer to complete */

            /* Check for errors */
			ret = i2c_check_and_clear_error(base, base->I2SR);
            if(ret)
            {
             	ret = I2C_STATUS_ADDRNAK;
                i2c_master_stop(base); 		/* Send stop signal */
                return ret;  
            }
           	          
        }
    }	

    /* send data */
    if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0))
    {
    	i2c_master_write(base, xfer->data, xfer->dataSize);
	}

    /* Read data */
    if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0))
    {
       	i2c_master_read(base, xfer->data, xfer->dataSize);
	}
	return 0;	
}



1.2 device drive (sensor B)

  • With the above host driver foundation, the next step is to write for the specific device of sensor B and some registers.
  • The following is the information of sensor B, device address and supporting register information (since linux system is not introduced, the address here is often referred to as physical address)

    Sensor B.h file
#ifndef _BSP_AP3216C_H
#define _BSP_AP3216C_H

#include "imx6ul.h"

#define AP3216C_ADDR     	 0X1E 	/*  Ap3216c device address*/

/* AP3316C register */
#define AP3216C_SYSTEMCONG 	 0x00 	/*  Configuration register 			*/
#define AP3216C_INTSTATUS 	 0X01 	/*  Interrupt status register 			*/
#define AP3216C_INTCLEAR 	 0X02 	/*  Interrupt clear register 			*/
#define AP3216C_IRDATALOW 	 0x0A 	/*  IR data low byte 			*/
#define AP3216C_IRDATAHIGH 	 0x0B 	/*  IR data high byte 			*/
#define AP3216C_ALSDATALOW 	 0x0C 	/*  ALS data low byte 		*/
#define AP3216C_ALSDATAHIGH 	 0X0D 	/*  ALS data high byte 			*/
#define AP3216C_PSDATALOW 	 0X0E 	/*  PS data low byte 			*/
#define AP3216C_PSDATAHIGH 	 0X0F 	/*  PS data high byte 			*/

/* Function declaration */
unsigned char ap3216c_init(void);
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg);
unsigned char ap3216c_writeonebyte(unsigned char addr,unsigned char reg, unsigned char data);
void ap3216c_readdata(unsigned short *ir, unsigned short *ps, unsigned short *als);

#endif
  • I2C1 controller using MCU A
  • Using the API functions written in the host driver
    Sensor B.c file
#include "bsp_ap3216c.h"
#include "bsp_i2c.h"
#include "bsp_delay.h"
#include "cc.h"
#include "stdio.h"

/*
 * @description	: Initialize AP3216C
 * @param		: nothing
 * @return 		: 0 Success, other value error code
 */
unsigned char ap3216c_init(void)
{
	unsigned char data = 0;

	/* 1,IO Initialize and configure I2C IO attribute	
     * I2C1_SCL -> UART4_TXD
     * I2C1_SDA -> UART4_RXD
     */
	IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1);
	IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1);

	/* 
	 *bit 16:0 HYS close
	 *bit [15:14]: 1 Default 47K pull-up
	 *bit [13]: 1 pull function
	 *bit [12]: 1 pull/keeper Enable 
	 *bit [11]: 0 Turn off the open circuit output
	 *bit [7:6]: 10 Speed 100Mhz
	 *bit [5:3]: 110 The driving capacity is R0/6
	 *bit [0]: 1 High conversion rate
	 */
	IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x70B0);
	IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0X70B0);

	i2c_init(I2C1);		/* Initialize I2C1 */

	/* 2,Initialize AP3216C */
	ap3216c_writeonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X04);	/* Reset AP3216C 			*/
	delayms(50);													/* AP33216C Reset for at least 10ms */
	ap3216c_writeonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG, 0X03);	/* Turn on ALS, PS+IR 		   	*/
	data = ap3216c_readonebyte(AP3216C_ADDR, AP3216C_SYSTEMCONG);	/* Read 0X03 just written in */
	if(data == 0X03)
		return 0;	/* AP3216C normal 	*/
	else 
		return 1;	/* AP3216C fail 	*/
}

/*
 * @description	: Write data to AP3216C
 * @param - addr: Device address
 * @param - reg : Register to write
 * @param - data: Data to write
 * @return 		: Operation results
 */
unsigned char ap3216c_writeonebyte(unsigned char addr,unsigned char reg, unsigned char data)
{
    unsigned char status=0;
    unsigned char writedata=data;
    struct i2c_transfer masterXfer;
	
    /* Configure I2C xfer structure */
   	masterXfer.slaveAddress = addr; 			/* Device address 				*/
    masterXfer.direction = kI2C_Write;			/* Write data 				*/
    masterXfer.subaddress = reg;				/* Register address to write 			*/
    masterXfer.subaddressSize = 1;				/* The address length is one byte 			*/
    masterXfer.data = &writedata;				/* Data to write 				*/
    masterXfer.dataSize = 1;  					/* Write data length 1 byte			*/

    if(i2c_master_transfer(I2C1, &masterXfer))
        status=1;
        
    return status;
}

/*
 * @description	: Read one byte of data from AP3216C
 * @param - addr: Device address
 * @param - reg : Register to read
 * @return 		: Read data.
 */
unsigned char ap3216c_readonebyte(unsigned char addr,unsigned char reg)
{
	unsigned char val=0;
	
	struct i2c_transfer masterXfer;	
	masterXfer.slaveAddress = addr;				/* Device address 				*/
    masterXfer.direction = kI2C_Read;			/* Read data 				*/
    masterXfer.subaddress = reg;				/* Register address to read 			*/
    masterXfer.subaddressSize = 1;				/* The address length is one byte 			*/
    masterXfer.data = &val;						/* Receive data buffer 				*/
    masterXfer.dataSize = 1;					/* Read data length 1 byte			*/
	i2c_master_transfer(I2C1, &masterXfer);

	return val;
}

/*
 * @description	: Read the data of AP3216C and read the original data, including ALS,PS and IR. Attention!
 *				: If ALS and IR + PS are turned on at the same time, the time interval between two data reads should be greater than 112.5ms
 * @param - ir	: ir data
 * @param - ps 	: ps data
 * @param - ps 	: als data 
 * @return 		: None.
 */
void ap3216c_readdata(unsigned short *ir, unsigned short *ps, unsigned short *als)
{
    unsigned char buf[6];
    unsigned char i;

	/* Cycle through all sensor data */
    for(i = 0; i < 6; i++)	
    {
        buf[i] = ap3216c_readonebyte(AP3216C_ADDR, AP3216C_IRDATALOW + i);	
    }
	
    if(buf[0] & 0X80) 	/* IR_OF If the bit is 1, the data is invalid */
		*ir = 0;					
	else 				/* Read data from IR sensor   		*/
		*ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03); 			
	
	*als = ((unsigned short)buf[3] << 8) | buf[2];	/* Read data from ALS sensor 			 */  
	
    if(buf[4] & 0x40)	/* IR_OF If the bit is 1, the data is invalid 			*/
		*ps = 0;    													
	else 				/* Read the data of PS sensor    */
		*ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 	
}

Topics: Single-Chip Microcomputer