019.ADC转换和子中断|千篇笔记实现嵌入式全栈/裸机篇

⚠️裸机仓库:https://gitee.com/simonchina_carel_li/mini2440-bare-metal.git

⚠️Tag: 19-adc

1. 这次要做什么?

板上有一个可调电阻器,连接在SOC的AIN0通道,

我们要实现采样值的打印,并且要以中断的方式实现

2. 方案分析

2.1 片上外设

  • SOC集成一个10bits ADC

  • 8通道多路复用,部分通道是支持线性触摸信号处理的

  • 最高工作在 2.5MHz 时钟下,此时转换率可以达到 500 KSPS
    转换时间 = 1 / ( x M H z / 5 周期 ) 转换时间 = 1/(xMHz / 5周期) 转换时间=1/(xMHz/5周期)

2.2 核心寄存器

REG 说明
ADCCON 设置预分频器、选择输入通道、启动转换以及查询转换完成状态
ADCDAT0 转换完成后的 10位结果会保存在该寄存器的低 10位中

2.3 操作思路

  • 配置分频相关

    • 选择通道

    • 启动转换

    • 等待转换完成(轮询ADCON[15]或者中断处理函数设置的标志位)

    • 打印

    • 循环♻️

2.4 子中断

ADC转换对应的中断源为INT_ADC,内部包含两个子中断INST_ADC_SINT_TC,

前者就是我们要关心的ADC中断,

我们实现ADC中断处理函数,还要在其内部实现子中断源的判断分发,并清除子中断挂起

3. 代码实现

新建common/adc.c,实现中断服务函数、初始化、轮询方式接口、中断方式接口,

具体细节、原理见注释,

C 复制代码
#include "s3c2440a.h"

// 全局变量,用于保存中断中读取的 ADC 结果
volatile int g_adc_val = 0;
volatile int g_adc_done = 0;   // 转换完成标志

/**
 * @brief ADC 中断处理函数
 */
void irq_int_adc_handler(void)
{
    // ADC转换完成
    if (SUBSRCPND & (1 << IRQ_SUB_INT_ADC_S)) {
        SUBSRCPND = (1 << IRQ_SUB_INT_ADC_S);
        // 读取 ADC 转换结果 (保留低 10 位)
        g_adc_val = ADCDAT0 & 0x3FF;
        // 设置转换完成标志,通知主程序
        g_adc_done = 1;
    }
    // 触摸
    else if (SUBSRCPND & (1 << IRQ_SUB_INT_TC)) {
        SUBSRCPND = (1 << IRQ_SUB_INT_TC);
        // TODO
    }
}

/**
 * @brief ADC 初始化函数
 */
void adc_init(bool irqEn) 
{
    // 配置 ADCCON 寄存器
    // [14]   PRSCEN = 1: 使能预分频器
    // [13:6] PRSCVL = 49: 预分频值设为 49 (假设 PCLK = 50.625MHz, 则 ADC clk = 50.625MHz / (49 + 1) = 1.0125MHz)
    // [5:3]  SEL_MUX = 0: 默认选择通道 AIN0
    // [2]    STDBM = 0: 正常工作模式
    // [1]    READ_START = 0: 禁用读启动模式
    // [0]    ENABLE_START = 0: 不启动转换
    ADCCON = (1 << 14) | (49 << 6) | (0 << 3);

    if (!irqEn)
        return ;

    irq_src_enable(IRQ_INT_ADC, IRQ_SUB_INT_ADC_S, true);
}

/**
 * @brief 读取指定通道的 ADC 转换值
 * @param channel 要读取的通道号 (0 ~ 7)
 * @return 10位的 ADC 转换结果
 */
int adc_read(int channel) 
{
    // 选择转换通道 (先清除原本的通道设置,再设置新通道)
    ADCCON = (ADCCON & ~(7 << 3)) | (channel << 3);
    // 启动 ADC 转换
    ADCCON |= (1 << 0);
    // 等待 ADC 转换完成 (ADCCON 的第15位为1时表示完成)
    while (!(ADCCON & (1 << 15)));
    // 读取并返回ADC转换数据 (ADCDAT0的低10位有效)
    return (ADCDAT0 & 0x3FF);
}

/**
 * @brief 启动一次 ADC 转换(中断方式)
 */
void adc_start(int channel) 
{
    // 标志位清零
    g_adc_done = 0;      
    // 选择转换通道 (先清除原本的通道设置,再设置新通道)
    ADCCON = (ADCCON & ~(7 << 3)) | (channel << 3);
    // 启动 ADC 转换
    ADCCON |= (1 << 0);  
}

/**
 * @brief 检查 ADC 转换是否完成并获取结果
 * @param out 指向存储 ADC 转换结果的变量
 * @return true 如果有新的 ADC 转换结果
 * @return false 如果没有新的 ADC 转换结果
 */
bool adc_poll(int *out)
{
    if (g_adc_done)
    {
        *out = g_adc_val;
        g_adc_done = 0;
        return true;
    }
    return false;
}

可以看到,中断使能函数变成了

C 复制代码
void irq_src_enable(irq_src_t src, irq_subsrc_t subsrc, bool enable)

增加了子中断的参数项,

对应的实现修改,

C 复制代码
/// @brief 中断源使能控制
void irq_src_enable(irq_src_t src, irq_subsrc_t subsrc, bool enable)
{
    unsigned long _save = disable_irq_save();
    if (subsrc != IRQ_SUB_INT_NONE)
        SUBSRCPND = (1 << subsrc);
    SRCPND = (1 << src);
    INTPND = (1 << src);
    if (enable) {
        INTMSK &= ~(1 << src);
        if (subsrc != IRQ_SUB_INT_NONE)
            INTSUBMSK &= ~(1 << subsrc);
    } else {
        INTMSK |= (1 << src);
        if (subsrc != IRQ_SUB_INT_NONE)
            INTSUBMSK |= (1 << subsrc);
    }
    restore_irq_mask(_save);
}

宏定义也增加IRQ_SUB_INT_NONE枚举,以兼容没有子中断的中断源,

C 复制代码
///@enum 中断子源寄存器位定义 (SUBSRCPND)
typedef enum {
    IRQ_SUB_INT_NONE = 0xFF,
    IRQ_SUB_INT_RXD0 = 0,   // UART0 接收中断
    IRQ_SUB_INT_TXD0 = 1,   // UART0 发送中断
    IRQ_SUB_INT_ERR0 = 2,   // UART0 错误中断

所以,还要将之前诸如,

C 复制代码
rq_src_enable(IRQ_INT_TIMER4, true);

改成,

C 复制代码
rq_src_enable(IRQ_INT_TIMER4, IRQ_SUB_INT_NONE, true);

另外实现测试代码,

新建adc/main.c,

C 复制代码
#include "s3c2440a.h"
#include <stdio.h>

int main(void) 
{
    int adc_val;
    // --- 轮询方式 ---
#if 0 
    // 初始化 ADC 外设
    adc_init(false);
    
    while (1) {
        easy_delay_ms(100);

        // 循环读取通道0的ADC值
        adc_val = adc_read(0);
        
        // 将 adc_val 通过UART串口打印输出
        printf("ADC Channel 0 Value: %d\r\n", adc_val);
    }
#else // --- 中断方式 ---
    adc_init(true);
    while (1) {
        adc_start(0);
        if (adc_poll(&adc_val)) {
            printf("ADC Channel 0 Value: %d\r\n", adc_val);
        }
    }
#endif
    
    return 0;
}

我们支持两种方式使用ADC,软件轮询和中断循环

4. 运行

make adc, 烧录、运行,

扭动变阻器,可以看到采样值在0 ~ 1023变化,

相关推荐
xuanwenchao1 小时前
ROS2学习笔记 - 2、类的继承及使用
服务器·笔记·学习
lingzhilab2 小时前
零知派——STM32驱动INA219电流功率监测计实现高精度电源管理
stm32·单片机·嵌入式硬件
GottdesKrieges2 小时前
OceanBase租户级物理恢复
linux·oceanbase
2601_949817722 小时前
基础篇:Linux安装redis教程(详细)
linux·运维·redis
CQU_JIAKE2 小时前
4.17[Q]
java·linux·服务器
aq55356003 小时前
CentOS vs Debian:如何选择最适合的Linux发行版
linux·centos·debian
LXY_BUAA3 小时前
《ubuntu22.04》_新系统的配置_20260418
linux·运维·服务器
楼田莉子3 小时前
同步/异步日志系统:日志落地模块\日志器模块\异步日志模块
linux·服务器·c++·学习·设计模式
洛水水3 小时前
图解式讲解内存池:告别内存碎片与随机coredump
linux·内存池