ARM 下的 ADC(模数转换器)
- ADC(Analog to Digital Converter,模数转换器)是嵌入式系统中非常重要的一个模块,负责将连续变化的模拟信号(如温度、电压、电流等)转换为数字信号,供 CPU 和程序进行处理。
- ADC 基本概念
- 模拟量(Analog Quantity):模拟量是连续变化的物理量,比如温度、湿度、光照强度、电压等。它们的变化是连续的,无法直接被数字处理器理解。
- 数字量(Digital Quantity):数字量是离散的、不连续的。数字处理器只能处理数字量,比如 1 和 0 的组合。
- 转换:ADC 模块负责将模拟信号(比如电压信号)转换为相应的数字量,使处理器能够读取和处理。
- 工作原理
- 在 ARM 系统中的 ADC 模块通常是一个基于电压的转换器,它通过对输入电压信号的采样,将其转换为对应的数字值。
- 例如,在 12 位 ADC 中,输入电压的范围通常为 0 - Vref,其中 Vref 是参考电压。输入电压 Vx 可以通过以下公式转换为数字值
B L = V x V r e f × 4095 BL = \frac{V_x}{V_{ref}} \times 4095 BL=VrefVx×4095
- BL 是 ADC 转换得到的数字值,12 位 ADC 的最大值是 4095(即 2^12 - 1)
- Vx 是输入的电压值
- Vref 是参考电压,通常为 3.3V 或 5V
- ARM下的ADC特点
- 分辨率:ADC 的分辨率决定了它能将模拟量细分为多少个数字值。例如,12 位分辨率的 ADC 能将输入电压分成 4096 个离散的数字量(即 0 - 4095)
- 采样速度:这是 ADC 每秒能够执行的转换次数,通常以 Samples Per Second(SPS,样本/秒)为单位
- 参考电压(Vref):这是 ADC 转换时的参考电压,通常由外部或者内部的电压源提
- ADC编程步骤
- 配置 GPIO 引脚为模拟输入模式: 在一些 ARM Cortex-M 系列的 MCU 中,ADC 通道与特定的 GPIO 引脚绑定,在使用 ADC 之前需要将 GPIO 配置为模拟模式。
- 配置 ADC 模块:
- 配置分辨率和采样速度(如 12 位,1 MSPS)。
- 设置参考电压(Vref)。
- 启动转换: 启动 ADC 转换,等待 ADC 模块完成采样并将结果存储在数据寄存器中。
- 读取 ADC 数据寄存器: 一旦转换完成,ADC 数据寄存器将存储转换后的数字值,可以通过访问寄存器读取这个值。
- ARM 典型寄存器配置(伪代码)
c
复制代码
// 伪代码 - ARM ADC 配置与读取
//基于 ARM Cortex-M 系列的 ADC 配置步骤
void ADC_Init(void)
{
// 1. 配置 GPIO 引脚为模拟输入模式
GPIO_PinConfig(GPIOA, PIN1, GPIO_MODE_ANALOG);
// 2. 配置 ADC 模块
ADC1->CR1 = 0; // 配置为12位模式
ADC1->SQR1 = 0; // 设置规则序列长度为 1
ADC1->SMPR2 |= ADC_SMPR2_SMP10; // 设置采样时间
// 3. 启动 ADC
ADC1->CR2 |= ADC_CR2_ADON; // 启用 ADC 模块
}
uint16_t ADC_Read(void)
{
// 4. 启动转换
ADC1->CR2 |= ADC_CR2_SWSTART; // 启动转换
// 5. 等待转换完成
while(!(ADC1->SR & ADC_SR_EOC)); // 等待转换结束
// 6. 读取 ADC 数据寄存器
return ADC1->DR; // 返回转换后的值
}
- ARM 下的 ADC 实际应用
- 温度测量:读取热敏电阻输出的模拟电压值,将其转换为数字值,再根据转换结果计算温度。
- 电压监测:通过电压分压电路,监测电池或电源的电压值,进而判断电源状态。
- 光强度检测:读取光电二极管或光敏电阻的模拟信号,转换为数字量后判断环境光照强度。
ARM的ADC编程实例(main.c)
- 第一步:UART初始化
目的:初始化串口通信模块,使 CPU 可以通过 UART 向外部设备发送字符数据(例如,输出 ADC 转换得到的电压值
c
复制代码
#define GPA1CON *(volatile long*)0x11400020 // GPIO A1 控制寄存器地址
#define ULCON2 *(volatile long*)0x13820000 // UART 控制寄存器 ULCON2,设置 UART 数据格式
#define UCON2 *(volatile long*)0x13820004 // UART 控制寄存器 UCON2,设置 UART 的控制模式(轮询模式)
#define UTRSTAT2 *(volatile long*)0x13820010 // UART 状态寄存器,用于查询 UART 是否就绪
#define UTXH2 *(volatile long*)0x13820020 // UART 发送寄存器,用于向 UART 发送数据
#define UBRDIV2 *(volatile long*)0x13820028 // UART 波特率寄存器,设置波特率
#define UFRACVAL2 *(volatile long*)0x1382002C // UART 波特率分数寄存器,用于波特率微调
void uart_init(void)
{
GPA1CON = GPA1CON & ~0xF; // 清除 GPA1_0 的配置位,确保后续操作正确
GPA1CON = GPA1CON | (1<<1); // 设置 GPA1_0 为 UART 发送功能
GPA1CON = GPA1CON & ~(0xF<<4); // 清除 GPA1_1 的配置位
GPA1CON = GPA1CON | (1<<5); // 设置 GPA1_1 为 UART 接收功能
ULCON2 = ULCON2 | 0x3; // 设置 UART 数据格式为 8 位数据,无校验位,1 位停止位
ULCON2 = ULCON2 & ~(1<<2); // 确保校验位为无校验
UCON2 &= ~(3); // 清除 UART 控制模式位
UCON2 |= 1<<0; // 设置 UART 为轮询接收模式
UCON2 &= ~(3<<2); // 清除 UART 控制模式位
UCON2 |= 1<<2; // 设置 UART 为轮询发送模式
UBRDIV2 = 53; // 设置 UART 波特率分频值
UFRACVAL2 = 4; // 设置 UART 波特率的分数部分
}
- 第二步:ADC初始化
目的:初始化 ADC,使 CPU 能够从指定的 ADC 通道读取模拟电压值,并转换为数字信号
c
复制代码
#define ADCCON *(volatile long*)0x126C0000 // ADC 控制寄存器地址
#define ADCDAT *(volatile long*)0x126C000C // ADC 数据寄存器地址
#define ADCMUX *(volatile long*)0x126C001C // ADC 多路复用器寄存器地址,选择 ADC 输入通道
void adc_init(void)
{
ADCCON |= 1<<16; // 启用 12 位分辨率
ADCCON |= 1<<14; // 使能分频器
ADCCON &= ~(0xFF<<6); // 清除原有的分频器设置
ADCCON |= 19<<6; // 设置分频器值为 19
ADCCON &= ~(1<<2); // 禁用空闲模式
ADCCON |= 1<<1; // 启用自动重新转换
ADCCON |= 1<<0; // 启动 ADC 转换
ADCMUX = 3; // 选择 ADC 输入通道 3
int reg = ADCDAT; // 读取一次 ADC 数据寄存器,准备初始化
}
- 第三步:获取 ADC 数据并将其转换为电压值
目的:通过 ADC 读取输入电压并转换为数字值(mV),用于后续的显示。
c
复制代码
int adc_get_mv(void)
{
int reg = ADCDAT & 0xFFF; // 获取 ADC 数据寄存器中的 12 位有效数据
int mv = reg * 1800 / 4095; // 将 ADC 数据转换为毫伏(1800mV 为参考电压)
return mv; // 返回计算出的电压值
}
- 第四步:通过 UART 输出电压值
目的:将 ADC 读取的电压值以字符形式通过 UART 发送,显示到外部设备。
c
复制代码
void main(void)
{
int mv;
int qian, bai, shi, ge;
uart_init(); // 初始化 UART,配置波特率和 GPIO
adc_init(); // 初始化 ADC,设置分频器和输入通道
while(1){
mv = adc_get_mv(); // 获取 ADC 转换得到的电压值(以 mV 为单位)
// 分离电压值的每一位数字(千、百、十、个位)
qian = mv / 1000; // 计算千位
putc(qian + '0'); // 发送千位数字
bai = mv / 100 % 10; // 计算百位
putc(bai + '0'); // 发送百位数字
shi = mv / 10 % 10; // 计算十位
putc(shi + '0'); // 发送十位数字
ge = mv % 10; // 计算个位
putc(ge + '0'); // 发送个位数字
// 发送 'mv' 后缀,表示毫伏
putc('m');
putc('v');
putc('\r'); // 发送回车符
putc('\n'); // 发送换行符
mysleep(200); // 延时 200ms
}
}
- 第五步:将电压值拆分成千、百、十、个位并输出
目的:将电压值(以毫伏为单位)拆分成千位、百位、十位和个位,然后逐位通过 UART 输出字符形式的数值,最后附加上 "mv" 单位。
c
复制代码
void main(void)
{
int mv;
int qian, bai, shi, ge;
uart_init(); // 初始化 UART,配置波特率和 GPIO
adc_init(); // 初始化 ADC,设置分频器和输入通道
while(1){
mv = adc_get_mv(); // 获取 ADC 转换得到的电压值(以 mV 为单位)
// 分离电压值的每一位数字(千、百、十、个位)
qian = mv / 1000; // 计算千位
putc(qian + '0'); // 发送千位数字
bai = mv / 100 % 10; // 计算百位
putc(bai + '0'); // 发送百位数字
shi = mv / 10 % 10; // 计算十位
putc(shi + '0'); // 发送十位数字
ge = mv % 10; // 计算个位
putc(ge + '0'); // 发送个位数字
// 发送 'mv' 后缀,表示毫伏
putc('m'); // 发送字符 'm'
putc('v'); // 发送字符 'v'
putc('\r'); // 发送回车符 '\r'
putc('\n'); // 发送换行符 '\n'
mysleep(200); // 延时 200 毫秒,避免过快刷新输出
}
}
- 第六步:延时函数 mysleep
目的:通过简单的循环实现延时功能,避免程序过快地进行下一轮 ADC 读取与数据输出
cpp
复制代码
void mysleep(int ms)
{
while(ms--){ // 每次减少 1 毫秒
int num = 0x1FFF/2; // 初始化计数器,数值决定延时时间
while(num--); // 空循环,达到延时效果
}
}
- 综合示例
cpp
复制代码
//使用 UART 和 ADC 来获取模拟电压值并通过 UART 输出电压读数。它包括 UART 初始化、ADC 初始化、获取 ADC 数据和通过串口输出电压值的过程。
#define GPA1CON *(volatile long*)0x11400020 // GPIO A1 控制寄存器
#define ULCON2 *(volatile long*)0x13820000 // UART2 控制寄存器,设置数据位、停止位、校验等
#define UCON2 *(volatile long*)0x13820004 // UART2 控制寄存器,设置轮询、发送和接收模式
#define UTRSTAT2 *(volatile long*)0x13820010 // UART2 状态寄存器,检查发送和接收状态
#define UTXH2 *(volatile long*)0x13820020 // UART2 发送缓冲寄存器,发送数据
#define URXH2 *(volatile long*)0x13820024 // UART2 接收缓冲寄存器,接收数据
#define UBRDIV2 *(volatile long*)0x13820028 // UART2 波特率分频寄存器,设置波特率
#define UFRACVAL2 *(volatile long*)0x1382002C // UART2 波特率小数部分寄存器
// 初始化 UART
void uart_init(void)
{
GPA1CON = GPA1CON & ~0xF; // 清除 GPA1_0 的原有配置
GPA1CON = GPA1CON | (1<<1); // 设置 GPA1_0 为 UART TX (发送)
GPA1CON = GPA1CON & ~(0xF<<4); // 清除 GPA1_1 的原有配置
GPA1CON = GPA1CON | (1<<5); // 设置 GPA1_1 为 UART RX (接收)
ULCON2 = ULCON2 | 0x3; // 配置 UART2:8 位数据,无校验,1 位停止位
ULCON2 = ULCON2 & ~(1<<2); // 设置 1 位停止位
ULCON2 = ULCON2 & ~(7<<3); // 禁用校验位
ULCON2 = ULCON2 & ~(1<<6); // 禁用红外模式
UCON2 &= ~(3); // 清除发送接收的控制配置
UCON2 |= 1<<0; // 轮询模式接收
UCON2 &= ~(3<<2); // 清除发送配置
UCON2 |= 1<<2; // 轮询模式发送
UBRDIV2 = 53; // 设置波特率的整数部分
UFRACVAL2 = 4; // 设置波特率的小数部分
}
// 发送单个字符
void putc(char ch)
{
while( (UTRSTAT2 & (1<<1)) == 0 ) ; // 等待直到发送缓冲区为空
UTXH2 = ch; // 向 UART2 发送寄存器写入字符
}
// 发送字符串
void puts(char *s)
{
int i = 0;
while (s[i]) { // 遍历字符串
putc(s[i]); // 逐字符发送
i++;
}
}
#define ADCCON *(volatile long*)0x126C0000 // ADC 控制寄存器
#define ADCDAT *(volatile long*)0x126C000C // ADC 数据寄存器,存放采样数据
#define ADCMUX *(volatile long*)0x126C001C // ADC 多路复用器,选择输入通道
// 简单的毫秒级延时函数
void mysleep(int ms)
{
while (ms--) {
int num = 0x1FFF / 2; // 简单的循环延时
while (num--);
}
}
// 初始化 ADC
void adc_init(void)
{
int reg;
ADCCON |= 1<<16; // 启用 12 比特分辨率
ADCCON |= 1<<14; // 启用分频值
ADCCON &= ~(0xFF<<6); // 清除分频值
ADCCON |= 19<<6; // 设置分频值为 19
ADCCON &= ~(1<<2); // 禁用空闲模式
ADCCON |= 1<<1; // 重新转换
ADCCON |= 1<<0; // 启动 ADC 转换
ADCMUX = 3; // 设置 ADC 输入通道为 3
reg = ADCDAT; // 读取 ADC 数据寄存器,清除第一次转换的值
}
// 获取 ADC 值并转换为毫伏值
int adc_get_mv(void)
{
int reg = ADCDAT & 0xFFF; // 获取 12 位有效的 ADC 数据
int mv = reg * 1800 / 4095; // 将 ADC 值转换为电压(1800 毫伏参考电压)
return mv;
}
// 主函数
void main(void)
{
int mv;
int qian, bai, shi, ge; // 用于存储电压的千位、百位、十位和个位
uart_init(); // 初始化 UART
adc_init(); // 初始化 ADC
while (1) {
mv = adc_get_mv(); // 获取电压值(例如 1658 mV)
qian = mv / 1000; // 获取千位
putc(qian + '0'); // 通过 UART 发送千位数字
bai = mv / 100 % 10; // 获取百位
putc(bai + '0'); // 通过 UART 发送百位数字
shi = mv / 10 % 10; // 获取十位
putc(shi + '0'); // 通过 UART 发送十位数字
ge = mv % 10; // 获取个位
putc(ge + '0'); // 通过 UART 发送个位数字
putc('m'); // 发送字符 'm'
putc('v'); // 发送字符 'v'
putc('\r'); // 发送回车符
putc('\n'); // 发送换行符
mysleep(200); // 延时 200 毫秒
}
return;
}
其余相关代码
- start.S
c
复制代码
.global delay1s @ 声明全局符号 delay1s,用于定义延时函数
.text @ 代码段的开始
.global _start @ 声明全局符号 _start,程序的入口点
_start:
b reset @ 程序启动时跳转到 reset,执行复位后的初始化操作
ldr pc,_undefined_instruction @ 设置未定义指令异常的处理地址
ldr pc,_software_interrupt @ 设置软件中断异常的处理地址
ldr pc,_prefetch_abort @ 设置预取指令异常的处理地址
ldr pc,_data_abort @ 设置数据访问异常的处理地址
ldr pc,_not_used @ 没有使用的异常,保留
ldr pc,_irq @ 设置普通中断 (IRQ) 的处理地址
ldr pc,_fiq @ 设置快速中断 (FIQ) 的处理地址
_undefined_instruction: .word _undefined_instruction @ 定义未定义指令异常处理地址
_software_interrupt: .word _software_interrupt @ 定义软件中断异常处理地址
_prefetch_abort: .word _prefetch_abort @ 定义预取异常处理地址
_data_abort: .word _data_abort @ 定义数据异常处理地址
_not_used: .word _not_used @ 保留,未使用的异常
_irq: .word _irq @ 定义 IRQ 处理地址
_fiq: .word _fiq @ 定义 FIQ 处理地址
reset:
ldr r0,=0x40008000 @ 将异常向量表的起始地址设置为 0x40008000
mcr p15,0,r0,c12,c0,0 @ 将向量基地址 (VBAR) 设置为 r0(即 0x40008000)
init_stack:
ldr r0,=stacktop @ 获取栈顶指针的值到 r0 中
/******** SVC 模式的栈 ********/
mov sp,r0 @ 将栈顶指针设置为当前栈指针 (sp)
sub r0,#128*4 @ 为 IRQ 模式预留 512 字节的栈空间
/**** IRQ 模式的栈 ****/
msr cpsr,#0xd2 @ 切换到 IRQ 模式
mov sp,r0 @ 设置 IRQ 模式的栈指针
sub r0,#128*4 @ 为 FIQ 模式预留 512 字节的栈空间
/*** FIQ 模式的栈 ***/
msr cpsr,#0xd1 @ 切换到 FIQ 模式
mov sp,r0 @ 设置 FIQ 模式的栈指针
sub r0,#0 @ FIQ 模式不需要更多的栈空间
/*** Abort 模式的栈 ***/
msr cpsr,#0xd7 @ 切换到 Abort 模式
mov sp,r0 @ 设置 Abort 模式的栈指针
sub r0,#0 @ Abort 模式不需要更多的栈空间
/*** Undefined 模式的栈 ***/
msr cpsr,#0xdb @ 切换到 Undefined 模式
mov sp,r0 @ 设置 Undefined 模式的栈指针
sub r0,#0 @ Undefined 模式不需要更多的栈空间
/*** System 和 User 模式的栈 ***/
msr cpsr,#0x10 @ 切换到 System/User 模式
mov sp,r0 @ 设置 System/User 模式的栈指针,预留 1024 字节空间
b main @ 跳转到 main 函数,开始执行主程序
delay1s:
ldr r4,=0x1ffffff @ 将延时循环计数器的值装入 r4
delay1s_loop:
sub r4,r4,#1 @ 计数器减 1
cmp r4,#0 @ 比较计数器是否归零
bne delay1s_loop @ 如果没有归零,继续循环
mov pc,lr @ 如果计数结束,返回调用处
.align 4 @ 对齐到 4 字节边界,优化存储器访问
/**** SWI 中断处理程序 ****/
.data @ 数据段的开始
stack:
.space 4*512 @ 为所有模式分配 512 字节栈空间
stacktop: @ 栈顶符号
.end @ 汇编文件的结束
- Makefile
c
复制代码
all:
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o start.o start.S
arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o main.o main.c
arm-none-linux-gnueabi-ld start.o main.o -Tmap.lds -o main.elf
arm-none-linux-gnueabi-objcopy -O binary main.elf main.bin
arm-none-linux-gnueabi-objdump -D main.elf > main.dis
clean:
rm -rf *.bak start.o main.o main.elf main.bin main.dis
- map.lds