IMX6ULL ADC Driver Development

1. Sorting Out Core Concepts of ADC

1.1 What is ADC?

ADC (Analog-to-Digital Converter) is a core module that converts continuously varying analog voltage signals into discrete digital signals, serving as a bridge for digital systems (MCU/processor) to interact with analog signals from the physical world.

1.2 Core Terminology

Terminology Core Explanation
Analog Signal A continuously varying physical quantity (e.g., voltage, temperature) in the physical world, acting as the input source of ADC
Digital Signal A discrete, discontinuous signal that can be directly parsed by MCU (e.g., values from 0 to 4095)
Sensor A device that converts physical quantities (light, temperature, pressure, etc.) into analog electrical signals (voltage/current)

2. Hardware Analysis of IMX6ULL ADC

2.1 Schematic Diagram Analysis

The ADC module of IMX6ULL is integrated inside the SOC, with the following core hardware associations:

  • Core Board (IMX6ULL_CORE_V2.0): The ADC reference voltage pin ADC_VREFH provides a voltage reference for conversion;
  • Bottom Board (IMX6ULL_MINI_V2.2): GPIO_1 (pin 7) of the P4 module is associated with the ADC channel, acting as the hardware carrier for actual signal acquisition.

2.2 Key Information from Reference Manual

(1) ADC External Signal Mapping

ADC1 of IMX6ULL includes 10 input channels, with the core channel-pin mapping as follows (only key ones listed):

Signal Description Corresponding Pin Direction
ADC_VREFH ADC high reference voltage ADC_VREFH Input
ADC1_IN0 ADC1 channel 0 input GPIO1_IO00 Input
ADC1_IN1 ADC1 channel 1 input GPIO1_IO01 Input
ADC1_IN7 ADC1 channel 7 input GPIO1_IO07 Input
(2) Clock Configuration

The ADC input clock (ADICLK) supports 3 options, which determine the conversion rate:

ADICLK Value Clock Source
00 IPG clock
01 IPG clock divided by 2
11 Asynchronous clock (ADACK)
(3) Core Registers (Key for Development)
Register Core Bit Fields Function Description
ADCx_HC0 ADCH (b0-b4) Selects the acquisition channel; switching the channel triggers one conversion
AIEN (b7) Conversion complete interrupt enable (disabled by default)
ADCx_HS COCO0 (b0) Conversion complete flag (1 = completed, 0 = not completed)
ADCx_R0 CDATA (b0-b11) Stores the conversion result (lower 12 bits valid, range 0~4095)
ADCx_GC CAL (b7) Starts automatic calibration (write 1 to start, read 0 when completed)
ADCx_GS CALF (b1) Calibration failure flag (0 = success, 1 = failure; needs to be cleared by writing 1)

3. Practical ADC Driver Development

3.1 ADC Calibration (Mandatory Step)

The ADC of IMX6ULL must undergo automatic calibration before use to eliminate hardware offset errors. The core code is as follows:

#include "imx6ull.h"

/**
 * @brief ADC automatic calibration
 * @param adc_base: ADC base address (e.g., ADC1_BASE)
 * @return 0: Calibration successful, -1: Calibration failed
 */
int adc_calibrate(ADC_Type *adc_base)
{
    // 1. Start calibration: Write 1 to the CAL bit
    adc_base->GC |= (1 << 7);
    
    // 2. Wait for calibration completion: CAL bit reads 0 when completed
    while((adc_base->GC & (1 << 7)) != 0);
    
    // 3. Check calibration failure flag
    if(adc_base->GS & (1 << 1))
    {
        adc_base->GS |= (1 << 1); // Clear the failure flag
        return -1;
    }
    return 0;
}

Principle Explanation: Calibration is a core step for ADC to ensure conversion accuracy, and the hardware automatically completes offset compensation. It is necessary to wait for the CAL bit to return to 0. If CALF is set to 1, it indicates calibration failure, and the flag needs to be cleared and calibration re-performed.

3.2 ADC Single Sampling

After calibration, perform single sampling on the specified channel. The core code is as follows:

/**
 * @brief ADC single sampling (12-bit resolution)
 * @param adc_base: ADC base address
 * @param channel: Sampling channel (0-9)
 * @return Sampled value (0~4095, corresponding to 0~ADC_VREFH voltage)
 */
unsigned short adc_single_sample(ADC_Type *adc_base, unsigned char channel)
{
    unsigned short adc_value = 0;
    
    // 1. Configure the sampling channel: Disable interrupt, set target channel
    adc_base->HC0 = (0 << 7) | (channel & 0x1F);
    
    // 2. Wait for conversion completion: Poll the COCO0 bit
    while((adc_base->HS & 0x01) == 0);
    
    // 3. Read the conversion result: Retain only valid lower 12 bits of data
    adc_value = adc_base->R0 & 0xFFF;
    
    return adc_value;
}

Principle Explanation: Writing the channel number to ADCx_HC0 triggers one conversion automatically by the ADC. Poll the COCO0 bit until it is set to 1, indicating conversion completion. The lower 12 bits of ADCx_R0 are the final digital quantity, corresponding to the proportional value of the analog voltage.

3.3 Mean Filter Optimization (Dedicated to Photosensitive Sensors)

The signal collected by the photosensitive sensor is susceptible to environmental interference, and mean filtering is used to reduce noise. The core code is as follows:

#define SAMPLE_COUNT 10  // Number of sampling times, adjustable as needed

/**
 * @brief ADC mean filter sampling (improves photosensitive signal stability)
 * @param adc_base: ADC base address
 * @param channel: Sampling channel
 * @return Filtered sampled value
 */
unsigned short adc_average_sample(ADC_Type *adc_base, unsigned char channel)
{
    unsigned int sum = 0;
    unsigned char i = 0;
    
    // 1. Accumulate multiple sampling values
    for(i = 0; i < SAMPLE_COUNT; i++)
    {
        sum += adc_single_sample(adc_base, channel);
        // Short delay to avoid excessively high sampling frequency
        for(volatile int j=0; j<1000; j++);
    }
    
    // 2. Calculate the average value to filter out random noise
    return (unsigned short)(sum / SAMPLE_COUNT);
}

Principle Explanation: Collect N samples (10 in the example) continuously from the same channel, accumulate them and take the average value, which can effectively offset random interference in a single sampling and improve the stability of the photosensitive signal.

4. Extended Practice: I2C Temperature Reading and Code Optimization

4.1 LM75 Temperature Sensor Reading (I2C Method)

LM75 is a temperature sensor with an I2C interface. The core code for reading temperature is as follows:

unsigned char rcv_buffer[2] = {0}; // I2C receive buffer

/**
 * @brief Read LM75 temperature value (0.5℃ precision)
 * @return Temperature value (unit: ℃)
 */
float get_temp_value(void)
{
    unsigned short t = 0;
    struct I2C_Msg msg = {
        .dev_addr = 0x48,  // LM75 device address
        .reg_addr = 0x00,  // Temperature register address
        .reg_len = 1,      // Register address length (1 byte)
        .data = rcv_buffer,// Receive data buffer
        .len = 2,          // Number of bytes to read (temperature register is 16-bit)
        .dir = I2C_read    // I2C read direction
    };
    
    // Execute I2C data transfer
    i2c_transfer(I2C1, &msg);
    
    // Splice data and convert to actual temperature
    t |= (rcv_buffer[0] << 8) | (rcv_buffer[1] << 0);
    t = t >> 7;  // Retain valid temperature bits (upper 9 bits)
    return t * 0.5;  // 0.5℃ precision conversion
}

Principle Explanation: The temperature register of LM75 is 16-bit, with the upper 9 bits as valid temperature bits (including sign). After shifting right by 7 bits and multiplying by 0.5, the digital quantity can be converted into the actual temperature value (e.g., a value of 10 corresponds to 5℃).

4.2 I2C Multi-Byte Register Address Compatibility Optimization

The register address of an I2C device may be multi-byte (e.g., 16-bit/32-bit), so it is necessary to be compatible with the transmission of addresses of different lengths. The optimized code is as follows:

/**
 * @brief Send multi-byte register address (compatible with different I2C devices)
 * @param base: I2C base address
 * @param reg_addr: Register address
 * @param reg_len: Address length (number of bytes)
 * @return 0: Success, non-0: Failure
 */
int i2c_send_reg_addr(I2C_Type *base, unsigned int reg_addr, unsigned int reg_len)
{
    int status = 0;
    int i = reg_len - 1;
    
    // Send address byte by byte from high to low
    for (; i >= 0; i--)
    {
        base->I2DR = (reg_addr >> (8 * i)) & 0XFF; // Extract current byte
        status = i2c_wait_iif(base); // Wait for transmission completion
        if (status != 0) goto stop;  // Jump to stop process if failed
    }
    return 0;
    
stop:
    base->I2CR &= ~(1 << 5); // Stop I2C transmission
    return status;
}

Principle Explanation: According to the register address length (reg_len), extract the address from high to low byte by byte through shift and AND operations, and write them into the I2C data register (I2DR) one by one to ensure compatibility with register addresses of different byte lengths.

4.3 FPU Enable (Improve Floating-Point Operation Efficiency)

Temperature calculation involves floating-point operations (e.g., *0.5), so it is necessary to enable the FPU (Floating-Point Unit) of IMX6ULL. The core assembly code is as follows:

enable_fpu:
    // 1. Read CPACR register, enable CP10/CP11 (FPU access permission)
    mrc     p15, 0, r0, c1, c0, 2   
    orr     r0, r0, #(0xF << 20)    
    mcr     p15, 0, r0, c1, c0, 2   
    
    // 2. Set the EN bit of FPEXC to enable FPU
    mov     r0, #0x40000000         
    vmsr    fpexc, r0               
    
    // 3. Clear FPSCR flags to initialize floating-point status
    mov     r0, #0x00000000         
    vmsr    fpscr, r0               
    
    bx      lr                      // Function return

Principle Explanation: The CP10/CP11 bits of the CPACR register control FPU access permission; setting them to 1 allows floating-point operations. The EN bit of FPEXC is the main FPU enable bit, and enabling it improves floating-point operation efficiency by several times.

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐