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_VREFHprovides 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:
c
#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:
c
/**
* @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:
c
#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:
c
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:
c
/**
* @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:
armasm
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.