5.11 ADC设备
5.11 ADC设备
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、MCU ADC 基础知识、5.1 设备驱动框架
🎯 学习目标
- 理解 OneOS ADC 设备驱动框架的分层设计(框架层
adc.c/ HAL 层drv_adc.c) - 掌握
os_adc_device_t结构体(parent继承机制、mult/shift系数)与os_adc_ops操作接口 - 掌握 ADC 设备的使能与禁用操作
- 掌握单通道和多通道 ADC 读取方法(
os_adc_read与标准os_device_read的区别) - 了解 ADC 中断通知机制
- 理解
calc_mult_shift电压换算系数与参考电压/分辨率的换算关系 - 掌握更换 MCU 时 ADC 的移植/适配流程
一、概述
ADC(Analog-to-Digital Converter)是 MCU 将模拟信号转换为数字信号的外设。OneOS 将 ADC 抽象为设备,提供统一的使能、读取、控制接口。ADC 设备驱动位于 drivers/misc/ 目录下,核心头文件为 adc.h,实现文件为 adc.c。
ADC 设备框架的特点:
- 统一抽象:将不同 MCU 平台的 ADC 统一封装
- 多通道支持:通过
channel参数指定采样通道 - 中断通知:支持
OS_ADC_EVENT_CONVERT_DONE事件通知 - 参考电压换算:通过
mult/shift系数将 ADC 值换算为电压值(mV)
1.1 一句话核心
ADC 设备适用于把连续变化的模拟电压(如电池电压、电位器、传感器输出)转换成数字量,并通过统一接口读取的场景。它让你"指定通道,读到一个 0~max_value 的整数,再换成 mV 电压"。
1.2 典型应用场景
场景一:电池/电源电压监测——用分压电阻把电池电压降到 ADC 可测范围,读通道值换算成实际电压,做低电量报警:
/* 读通道 1(电池电压分压) */
os_adc_read((os_adc_device_t *)adc_dev, 1, &adc_value);
int32_t mv = (int32_t)(((uint64_t)adc_value * adc->mult) >> adc->shift);
/* 若分压比 2:1,实际电池电压 = mv * 2 */场景二:传感器模拟量采样——很多传感器(NTC 温度、光敏、电位器)输出的是电压,配 ADC 读取后查表/换算成物理量:
/* 读 NTC 分压电压 → 查温度表 */
os_adc_read((os_adc_device_t *)adc_dev, 0, &adc_value);
/* 把 adc_value 换算成电压后查 NTC 电阻-温度表得温度 */场景三:高速采样(中断通知)——HAL 层在转换完成中断调 os_hw_adc_isr 发 CONVERT_DONE 通知,应用用 os_device_recv_notify_wait 阻塞等待,避免忙轮询(见 6.4 示例)。
1.3 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要纳秒级精确采样定时 | 定时器 + DMA(非 ADC 设备职责) | ADC 设备是"读一个值",不负责精确定时采样序列 |
| 传感器直接输出数字量(I2C/SPI) | 5.3 I2C / 5.5 SPI / 5.10 Sensors | 数字传感器不需要 ADC 转换 |
| 需要 PWM 输出 | 5.12 PWM | ADC 是采集(输入),PWM 是输出,方向相反 |
结合源码的使用注意点:
- 读取前必须使能:ADC 外设需
os_adc_enable(或OS_ADC_CMD_ENABLE)后才有有效读数,否则可能读到 0 或上电残留值。- 电压换算公式:
电压(mV) = ADC值 × mult >> shift,mult/shift在os_hw_adc_register注册时由calc_mult_shift自动算出,应用无需自己算,但要知道单位是 mV。- 中断模式:
os_hw_adc_isr由 HAL 层在转换完成中断里调用,应用层用os_device_recv_notify_wait配合(见 6.4)。
二、核心数据结构
2.1 ADC 设备结构体 os_adc_device_t
来源:drivers/misc/adc.h
struct os_adc_device
{
struct os_device parent;
const struct os_adc_ops *ops;
int32_t ref_hight;
int32_t ref_low;
uint32_t max_value;
uint32_t mult; /* adc voltage(mV) = value * mult >> shift */
uint32_t shift;
};
typedef struct os_adc_device os_adc_device_t;| 字段 | 类型 | 含义 |
|---|---|---|
parent | struct os_device(内嵌在开头) | 标准设备基类,让这个结构体"是一个设备",可被 os_device_register 注册、被 os_device_open 打开 |
ops | const struct os_adc_ops * | ADC 操作函数表,指向 HAL 层实现 |
ref_hight | int32_t | 参考电压高值,单位 mV(如 3300 表示 3.3V) |
ref_low | int32_t | 参考电压低值,单位 mV(通常为 0) |
max_value | uint32_t | ADC 最大转换值(分辨率-1,如 12bit 为 4095) |
mult/shift | uint32_t | 电压换算系数,ADC 值转电压(mV)= value * mult >> shift,注册时自动计算 |
关键理解 —— 为什么 parent(os_device)内嵌在最开头:与 5.8 RTC、5.9 Watchdog 同理,parent 是第一个成员,所以"设备指针 os_device_t *dev"和"ADC 指针 os_adc_device_t *adc"数值相等。os_adc_read/_adc_control 里 (struct os_adc_device *)dev 只是用更大视角解读 dev 指向的内存,从而访问 parent 后面的 ops/ref_hight/mult。这是 OneOS 用"C 语言继承"实现设备多态的标准手法。
关键理解 —— mult/shift 由注册时自动计算:os_hw_adc_register 里 calc_mult_shift(&mult, &shift, max_value, ref_hight - ref_low, 1) 会根据"ADC 最大值"和"参考电压范围"算出换算系数(见 4.6 节数值演算)。应用读取电压时直接用 (value * mult) >> shift,不需要自己算比例。
2.2 ADC 操作函数表 struct os_adc_ops
来源:drivers/misc/adc.h
struct os_adc_ops
{
os_err_t (*adc_enabled)(struct os_adc_device *dev, os_bool_t enable);
os_err_t (*adc_control)(struct os_adc_device *dev, int cmd, void *arg);
os_err_t (*adc_read)(struct os_adc_device *dev, uint32_t channel, int32_t *buffer);
};| 函数指针 | 说明 |
|---|---|
adc_enabled | 使能/禁用 ADC 外设 |
adc_control | 发送控制命令 |
adc_read | 读取指定通道的 ADC 转换值 |
三、API 详解
3.1 读取 ADC 值
os_err_t os_adc_read(struct os_adc_device *adc, uint32_t channel, int32_t *buffer);| 参数 | 说明 |
|---|---|
adc | ADC 设备指针 |
channel | ADC 通道号(0 ~ N) |
buffer | 输出缓冲区,存放读取的 ADC 值 |
| 返回值 | OS_SUCCESS 成功,其他值表示失败 |
3.2 使能与禁用
os_err_t os_adc_enable(struct os_adc_device *dev);
os_err_t os_adc_disable(struct os_adc_device *dev);3.3 控制命令
#define OS_ADC_CMD_ENABLE IOC_MISCELLANEOUS(1) /* 使能 ADC */
#define OS_ADC_CMD_DISABLE IOC_MISCELLANEOUS(2) /* 禁用 ADC */通过 os_device_control() 接口发送控制命令,最终调用 ops->adc_enabled。
3.4 中断通知(HAL 层使用)
void os_hw_adc_isr(struct os_adc_device *dev, int event);HAL 层在 ADC 转换完成中断中调用此函数,向等待的任务发送通知。事件类型为 OS_ADC_EVENT_CONVERT_DONE。
3.5 注册 ADC 设备
os_err_t os_hw_adc_register(struct os_adc_device *device, const char *name, void *data);四、核心源码详解
说明:以下源码均取自
drivers/misc/adc.c,与项目源码逐字一致(含原始英文注释)。
4.1 os_adc_read —— 读取指定通道 ADC 值
一句话定位:把通道号透传给 HAL 层 ops->adc_read 读取转换值。
完整源码(drivers/misc/adc.c,与项目一致):
os_err_t os_adc_read(struct os_adc_device *adc, uint32_t channel, int32_t *buffer)
{
OS_ASSERT(adc != OS_NULL);
return adc->ops->adc_read(adc, channel, buffer);
}分步详解:
第 1 步:断言并透传(无条件执行)
OS_ASSERT(adc != OS_NULL);
return adc->ops->adc_read(adc, channel, buffer);
}详细解释:os_adc_read 是全局可调用的 API(非 static)。它把 channel 和 buffer 原样透传给 HAL 层的 ops->adc_read(adc, channel, buffer),由 HAL 真正触发采样并填充 buffer。返回 HAL 的结果。
4.2 os_adc_enable / os_adc_disable —— 使能/禁用
完整源码(drivers/misc/adc.c,与项目一致):
os_err_t os_adc_enable(struct os_adc_device *adc)
{
os_err_t result = OS_SUCCESS;
OS_ASSERT(adc != OS_NULL);
result = adc->ops->adc_enabled(adc, OS_TRUE);
return result;
}
os_err_t os_adc_disable(struct os_adc_device *adc)
{
os_err_t result = OS_SUCCESS;
OS_ASSERT(adc != OS_NULL);
result = adc->ops->adc_enabled(adc, OS_FALSE);
return result;
}分步详解:
第 1 步:调用 HAL 使能/禁用(无条件执行)
result = adc->ops->adc_enabled(adc, OS_TRUE); /* enable */
result = adc->ops->adc_enabled(adc, OS_FALSE); /* disable */详细解释:os_adc_enable 调 ops->adc_enabled(adc, OS_TRUE)(上电并使能 ADC),os_adc_disable 调 ops->adc_enabled(adc, OS_FALSE)(关断)。两者最终都走同一个 HAL 函数,用 enable 参数区分。
4.3 标准设备读 _adc_read(批量读)
一句话定位:设备框架的 read 操作,支持一次读多个通道(pos 起始通道,size 字节数)。
完整源码(drivers/misc/adc.c,与项目一致):
static os_ssize_t _adc_read(struct os_device *dev, os_off_t pos, void *buffer, os_size_t size)
{
int i;
os_err_t ret;
if ((size % sizeof(int32_t)) != 0)
{
os_kprintf("invalid adc buff size.\r\n");
return 0;
}
for (i = 0; i < size; i += sizeof(int32_t))
{
ret = os_adc_read((struct os_adc_device *)dev, pos, (int32_t *)((unsigned long)buffer + i));
if (ret != OS_SUCCESS)
{
os_kprintf("adc read failed %d, %d\r\n", i, size);
break;
}
}
return i;
}分步详解:
第 1 步:校验 size 是 int32 的整数倍(条件 不是整数倍)
if ((size % sizeof(int32_t)) != 0)
{
os_kprintf("invalid adc buff size.\r\n");
return 0;
}详细解释:关键理解:size 是字节数,必须是 sizeof(int32_t)(4)的整数倍,表示读取若干个通道。不是则返回 0。
第 2 步:循环按通道读取(无条件执行)
for (i = 0; i < size; i += sizeof(int32_t))
{
ret = os_adc_read((struct os_adc_device *)dev, pos, (int32_t *)((unsigned long)buffer + i));
if (ret != OS_SUCCESS)
{
os_kprintf("adc read failed %d, %d\r\n", i, size);
break;
}
}
return i;
}详细解释:循环体里 os_adc_read((struct os_adc_device *)dev, pos, ...) 的 pos 在循环过程中不变,所以 _adc_read 实际是对同一个通道 pos 连续采样 size/4 次,把 size/4 个样本依次放入 buffer(每次写入 sizeof(int32_t) 字节)。关键理解:它读的是"同一通道的多个样本",不是"多个不同通道"。若中途某次 os_adc_read 失败,则 break 并返回已成功读取的字节数 i。
注意与
os_adc_read的区别:想读多个不同通道,应像 6.2 示例那样,在应用层用os_adc_read分别指定不同channel循环读;os_device_read的pos对应的是"起始通道",但_adc_read不自动递增通道号,它只做"同一通道重复采样"。
_adc_read() 条件走向总结:
_adc_read(dev, pos, buffer, size) 条件走向总结:
├── size % sizeof(int32_t) != 0(字节数不是 4 的整数倍)
│ └── 打印 "invalid adc buff size." + 返回 0(一个都没读)
└── size 合法 → for 循环 i = 0; i < size; i += 4
├── os_adc_read(dev, pos, buffer+i) 失败(ret != OS_SUCCESS)
│ └── 打印 "adc read failed i, size" + break
└── 全部成功 → 循环结束
└── 返回 i(实际读取字节数)4.4 标准设备控制 _adc_control
4.4 标准设备控制 _adc_control
完整源码(drivers/misc/adc.c,与项目一致):
static os_err_t _adc_control(struct os_device *dev, int cmd, void *args)
{
os_err_t result = OS_SUCCESS;
struct os_adc_device *adc;
OS_ASSERT(dev != OS_NULL);
adc = (struct os_adc_device *)dev;
switch (cmd)
{
case OS_ADC_CMD_ENABLE:
result = adc->ops->adc_enabled(adc, OS_TRUE);
break;
case OS_ADC_CMD_DISABLE:
result = adc->ops->adc_enabled(adc, OS_FALSE);
break;
default:
/* Control device */
result = OS_NOSYS;
break;
}
return result;
}分步详解:
第 1 步:按命令使能/禁用(条件 cmd 值)
switch (cmd)
{
case OS_ADC_CMD_ENABLE:
result = adc->ops->adc_enabled(adc, OS_TRUE);
break;
case OS_ADC_CMD_DISABLE:
result = adc->ops->adc_enabled(adc, OS_FALSE);
break;
default:
result = OS_NOSYS;
break;
}详细解释:os_device_control(dev, OS_ADC_CMD_ENABLE, ...) 到这里后,把命令转成 HAL 的 ops->adc_enabled(adc, OS_TRUE/FALSE)。这与 os_adc_enable/disable 殊途同归。未知命令返回 OS_NOSYS。
_adc_control() 条件走向总结:
_adc_control(dev, cmd, args) 条件走向总结:
├── OS_ASSERT(dev != OS_NULL)(空指针则断言崩溃)
├── adc = (struct os_adc_device *)dev
├── cmd == OS_ADC_CMD_ENABLE
│ └── result = ops->adc_enabled(adc, OS_TRUE);break
├── cmd == OS_ADC_CMD_DISABLE
│ └── result = ops->adc_enabled(adc, OS_FALSE);break
└── 其他 cmd(未知命令)
└── result = OS_NOSYS;break
└── 返回 result4.5 中断服务 os_hw_adc_isr
完整源码(drivers/misc/adc.c,与项目一致):
void os_hw_adc_isr(struct os_adc_device *adc, int event)
{
os_device_t *dev = (os_device_t *)&adc->parent;
switch (event & 0xff)
{
case OS_ADC_EVENT_CONVERT_DONE:
{
os_device_recv_notify(dev);
break;
}
default:
break;
}
}分步详解:
第 1 步:转换完成则通知(条件 event == CONVERT_DONE)
switch (event & 0xff)
{
case OS_ADC_EVENT_CONVERT_DONE:
os_device_recv_notify(dev);
break;
}详细解释:HAL 层在 ADC 转换完成中断里调用本函数。若事件是 OS_ADC_EVENT_CONVERT_DONE,调 os_device_recv_notify 唤醒等待读取的任务(应用可用 os_device_recv_notify_wait 等待)。注意:event & 0xff 取低 8 位作为事件码,OS_ADC_EVENT_CONVERT_DONE 的值是 0x00,所以 case 匹配的就是转换完成。
os_hw_adc_isr() 条件走向总结:
os_hw_adc_isr(adc, event) 条件走向总结:
├── dev = &adc->parent(取内嵌设备)
├── (event & 0xff) == OS_ADC_EVENT_CONVERT_DONE(转换完成)
│ └── os_device_recv_notify(dev)(唤醒等待读取的任务)
└── 其他事件
└── break(不处理)4.6 注册 os_hw_adc_register
完整源码(drivers/misc/adc.c,与项目一致):
os_err_t os_hw_adc_register(struct os_adc_device *device, const char *name, void *data)
{
os_err_t result = OS_SUCCESS;
OS_ASSERT(device != OS_NULL);
calc_mult_shift(&device->mult, &device->shift, device->max_value, device->ref_hight - device->ref_low, 1);
device->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
device->parent.ops = &adc_ops;
device->parent.user_data = device;
result = os_device_register(&device->parent, name);
return result;
}分步详解:
第 1 步:计算电压换算系数(无条件执行)
calc_mult_shift(&device->mult, &device->shift, device->max_value, device->ref_hight - device->ref_low, 1);详细解释:关键理解:注册时用 calc_mult_shift 根据 max_value(如 4095)和参考电压范围(ref_hight - ref_low)自动算出 mult/shift。后续 电压(mV) = adc值 × mult >> shift。注册后应用无需手动算系数。
第 2 步:设置类型/操作表并注册(无条件执行)
device->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
device->parent.ops = &adc_ops;
device->parent.user_data = device;
result = os_device_register(&device->parent, name);详细解释:类型 OS_DEVICE_TYPE_MISCELLANEOUS(杂项),操作表 adc_ops(含 read/control),用 name 注册。
os_hw_adc_register() 条件走向总结:
os_hw_adc_register(device, name, data) 条件走向总结:
├── OS_ASSERT(device != OS_NULL)(空指针则断言崩溃)
├── calc_mult_shift(&mult, &shift, max_value, ref_hight-ref_low, 1)(算电压系数)
├── parent.type = OS_DEVICE_TYPE_MISCELLANEOUS
├── parent.ops = &adc_ops
├── parent.user_data = device
└── result = os_device_register(&parent, name)
└── 返回 result(注册成功则 OS_SUCCESS)五、电压换算公式
ADC 设备注册时自动计算电压换算系数:
实际电压(mV) = ADC值 × mult >> shiftcalc_mult_shift 是怎么算出 mult/shift 的(drivers/driver.c,与项目逐字一致):
void calc_mult_shift(uint32_t *mult, uint32_t *shift, uint32_t from, uint32_t to, uint32_t max_from)
{
uint64_t tmp;
uint32_t sft, sftacc = 32;
tmp = ((uint64_t)max_from * from) >> 32;
while (tmp)
{
tmp >>= 1;
sftacc--;
}
for (sft = 32; sft > 0; sft--)
{
tmp = (uint64_t)to << sft;
tmp += from / 2;
tmp /= from;
if ((tmp >> sftacc) == 0)
break;
}
*mult = tmp;
*shift = sft;
}核心思路是:找一对 (mult, shift) 使 mult ≈ (to << shift) / from,从而 值 × mult >> shift ≈ 值 × to / from。
from/to 的含义(为什么 ADC 这样填):
| 参数 | 传入值(本例) | 含义 | 通俗理解 |
|---|---|---|---|
from | max_value = 4095 | ADC 最大值(分辨率-1),作为"被除的基数" | 满量程 ADC 值(12 位 → 4095) |
to | ref_voltage = 3300 | 参考电压范围(mV),作为"要映射到的目标值" | 满量程对应的电压(3.3V = 3300 mV) |
max_from | 1 | 控制精度的辅助参数 | 固定为 1 |
to / from = 3300 / 4095 就是"每个 ADC 计数对应多少 mV"这个比例。它定义了一条从"ADC 原始值域 [0, 4095]"到"电压值域 [0, 3300 mV]"的线性映射:
ADC 值 0 ──────▶ 0 mV
ADC 值 4095 ──────▶ 3300 mV(满量程)
任意 ADC 值 x ──────▶ x × 3300 / 4095 mVcalc_mult_shift 就是把这个线性映射的比例(3300/4095)用一对整数系数(mult/shift)存起来,让运行时"乘 mult 再右移 shift"就能快速算出电压(mV)。
先理解本质:为什么不能直接"按比例除法"
最直觉的电压算法是比例换算——12 位 ADC、参考电压 3.3V 时,读值 2048 对应的电压:
电压(mV) = ADC值 × 参考电压(mV) / 最大ADC值
= 2048 × 3300 / 4095 ≈ 1650 mV这个公式没错,但直接写成 value * 3300 / 4095 有两个问题:
3300 / 4095是小数(约 0.806)。C 语言整数除法直接截断,3300 / 4095会算成0,整个比例就废了。2048 × 3300 = 6758400,多通道高频采样时这种 64 位中间乘法 + 除法开销大、且易溢出。
mult/shift 就是为了把"一次除法"变成"一次乘法 + 一次右移"(两者都是极快的整数位运算)。
核心思想:把"除以一个数"改写成"乘一个数再右移"
任何"除以 N"都可以近似改写成"乘 M 再右移 k 位":
value / N ≈ value × M >> k (令 M ≈ (1 << k) / N)因为"除以 2^k"就是右移 k 位(硬件一条指令),所以关键是预先算好 M,运行时只做乘法 + 移位。
套到 ADC 的 value × 3300 / 4095,等价于:
value × 3300 / 4095 = value × (3300 / 4095)
= value × M >> shift (令 M ≈ (3300/4095) × 2^shift)一句话:M 是"把小数 3300/4095 放大 2^shift 倍存成的整数",运行时 value × M 再右移 shift 位"把放大缩回去",结果就是电压(mV)。calc_mult_shift 干的就是"挑一对合适的 (M, shift)"。
放大的精度:为什么 mult 能保留小数
3300 / 4095 ≈ 0.806 无法用整数表示,但放大 2^32 倍后:
mult ≈ 0.806 × 2^32 ≈ 3461145806 (一个可存进 uint32 的整数)运行时:
value × mult >> 32 = value × (0.806 × 2^32) >> 32
= value × 0.806 (2^32 乘了又右移掉,等价于乘 1)
= 电压(mV)"先乘 2^32、再右移 32 位"等于乘了 1,但中间那个"×2^32"把小数 0.806 用整数保存了下来——这就是精度和速度兼顾的关键。下面用逐行演算验证(见下方"数值演算")。
数值演算
数值演算(12bit ADC,参考电压 3.3V,from = 4095,to = 3300,max_from = 1)。下面逐行追踪 calc_mult_shift 的执行,每一步的变量值都列出来。
① 求 sftacc(决定放大精度的位数)
uint32_t sftacc = 32;
tmp = ((uint64_t)max_from * from) >> 32; // = (1 × 4095) >> 32
while (tmp) { tmp >>= 1; sftacc--; }| 变量 | 计算 | 值 |
|---|---|---|
tmp | (1 × 4095) >> 32 | 0(4095 比 2^32 小得多,右移 32 位直接为 0) |
while(tmp) | tmp = 0 → 循环不执行 | sftacc 保持 32 |
sftacc = 32表示用 32 位来"装放大后的数",精度约 32 位。
② 求 shift(从 32 往下找,第一个满足条件的 sft)
for (sft = 32; sft > 0; sft--)
{
tmp = (uint64_t)to << sft; // 3300 << sft
tmp += from / 2; // + 4095/2 = +2047(四舍五入用)
tmp /= from; // ÷ 4095
if ((tmp >> sftacc) == 0) // 放大后右移 32 位是否为 0?
break;
}这三行合起来在算什么?(为什么是 tmp = (to << sft + from/2) / from)
循环体里三行代码可以合并成一句:tmp = (to << sft + from/2) / from。本例代入 from = 4095, to = 3300 就是表格里的 tmp = (3300 << sft + 2047) / 4095(补充例子里 from = 1000 → from/2 = 500,即你看到的 (3000 << sft + 500) / 1000)。它分三步算出一个候选的放大系数 mult:
to << sft(=to × 2^sft)——先把"目标电压"放大sft位:我们想要mult ≈ (to/from) × 2^sft,即"比例to/from放大2^sft倍"。但to/from在 C 整数除法里会先截断成0,比例直接丢了。所以不能先算to/from再放大,而是先把分子to放大成to × 2^sft,最后再统一除以from——这样除法是在"放大后的完整值"上做,小数位就保留住了。+ from/2(= +2047)——四舍五入:整数除法向下截断,X / from会把X/from的小数全部丢掉。加上"除数的一半from/2"后,(X + from/2) / from就会把结果四舍五入到最近的整数,让mult更接近精确值、误差最小。/ from(÷ 4095)——得到"每个 ADC 计数对应的电压放大值":比例to/from的本义是"满量程电压 ÷ 满量程计数 = 每个计数对应多少 mV"。把第 1 步放大的"满量程电压量"除以from,就得到放大的单计数比例,即候选mult。
一句话:先把电压
to左移放大、再四舍五入、再除满量程from,得到的就是"放大 2^sft 倍的比例"mult。
迭代 sft | tmp = (3300 << sft + 2047) / 4095 | tmp >> 32 | 判断 |
|---|---|---|---|
32 | (14173392076800 + 2047) / 4095 | 3461145806 >> 32 = 0 | ✅ 满足,break |
因为从 sft = 32 第一轮就满足 (tmp >> 32) == 0,所以直接得到 shift = 32。
为什么必须求 shift(右移位数)?
先看求完的用法:电压 = value × mult >> shift。其中 mult 是把比例 to/from(这里是 0.806)放大 2^shift 倍后存成的整数,>> shift 就是运行时"把放大缩回去"。所以:
shift和mult是成对的,缺一不可:没有shift,你只知道mult是个大整数,却不知道它被放大了多少倍,运行时就没法还原。mult是被放大的数,shift是放大倍数的"指数"(放大2^shift倍),二者必须一起算好、一起使用。放大的目的是"把小数藏进整数的低位":
to/from = 3300/4095 ≈ 0.806是小数,C 语言整数乘法里0.806会被截断成0,直接废掉。所以先把0.806左移shift位放大成0.806 × 2^shift的整数(即mult),运行时再>> shift把放大的部分抵消掉——这样小数位就被"藏"进mult的低位保留了下来。shift越大,mult越精确:shift 越大,放大倍数越大,mult里能保留的小数位数越多,舍入误差越小。所以在"不溢出"的前提下 shift 要尽量大,而不是随便取。这也是为什么从sft = 32往下找——32是uint32_t能表达的极限放大位数,从它开始取到的第一个合法值就是"精度最高"的那一个。sftacc就是"mult 能占用的最大位数":sftacc初始为32,表示mult最多能用 32 位(uint32_t)。判断条件(tmp >> sftacc) == 0的含义是:把候选mult右移sftacc位,若结果不为0,说明它超过 32 位(溢出,存不进uint32_t),必须减小 shift;若为0,说明它落在 32 位以内,合法。从 32 往下找到的第一个合法值,就是"尽可能大又不溢出"的最优 shift。
为什么这里只要满足
(tmp >> sftacc) == 0就 break:sftacc是能用的移位位数上限。tmp = (to << sft) / from就是放大后的mult。条件(tmp >> 32) == 0保证mult不溢出 32 位、能存进uint32_t。从最大sft=32开始找,找到的第一个就是能用的最大放大倍数(精度最高),所以sft = 32直接就中。
💡 补充:一个"第三轮才 break"的例子
上面的例子
to(3300) < from(4095),比例to/from < 1,放大后位数足够,第一轮sft=32就 break 了。下面换一个比例大于 1 的场景:from = 1000(ADC 满量程 1000),to = 3000(参考电压 3000 mV = 3V),to/from = 3。因为"放大后要能装下 3.0 倍的量",32 位不够,第一、二轮都不满足,第三轮才 break。① 求
sftacc:tmp = (1 × 1000) >> 32 = 0,循环不执行,sftacc保持32。② 求
shift(从 32 往下,逐个判断(tmp >> 32) == 0):
迭代 sfttmp = (3000 << sft + 500) / 1000tmp >> 32判断 3212884901888500 / 1000 = 1288490188812884901888 >> 32 = 3❌ 不满足,继续 316442450944500 / 1000 = 64424509446442450944 >> 32 = 1❌ 不满足,继续 303221225472500 / 1000 = 32212254723221225472 >> 32 = 0✅ 满足, break第三轮才 break,得到
shift = 30,mult = 3221225472。为什么前两轮不满足:
tmp = (to << sft)/from算出的就是"放大后的mult"。tmp >> 32 != 0说明它超过 32 位能表示的范围(放大倍数 > 1 时,to << sft本身就会溢出 32 位)。本例比例to/from = 3,放大 2^32 或 2^31 倍后mult都超过 2^32,只有放大 2^30 倍才落回 32 位内,所以第三轮才满足。③ 结果验证:
读取的 valuevalue × 3221225472 >> 30电压 000 mV✅500500 × 3221225472 >> 301500 mV✅1000(满量程)1000 × 3221225472 >> 303000 mV✅验证:因为
mult = 3221225472 = 3 × 2^30,value × mult >> 30 = value × 3,所以满量程1000 → 3000 mV、中间值500 → 1500 mV都与精确比例value × 3000 / 1000完全一致,无舍入误差。这也说明:to/from能化简成整数时,mult恰好等于(to/from) × 2^shift,精度最高。
③ 结果 mult/shift 与电压换算
| 变量 | 值 |
|---|---|
shift | 32 |
mult | 3461145806(= (14173392076800 + 2047) / 4095) |
读取的 value | value × 3461145806 >> 32 | 电压 |
|---|---|---|
0 | 0 | 0 mV ✅ |
1024 | 1024 × 3461145806 >> 32 | 约 825 mV |
2048 | 2048 × 3461145806 >> 32 | 约 1650 mV |
4095(满量程) | 4095 × 3461145806 >> 32 | 约 3300 mV ✅ |
验证:满量程 4095 正好得到参考电压 3300 mV,说明"放大 2^32 → 右移 32"精确对齐,mult 内部那个 ×2^32 被 >>32 完整抵消。中间值 2048 → 1650 mV 与精确比例 2048 × 3300 / 4095 ≈ 1650.3 mV 只差不到 1 mV(来自 mult 取整)。
对比验证公式:
电压 = value × mult >> shift ≈ value × to / from = value × 3300 / 4095。mult/shift只在注册时算一次,之后每次读取都用同一对系数做"乘法 + 右移",避免每次做 64 位除法或浮点运算(嵌入式里整数移位比浮点快得多)。
关键理解 —— 为什么用整数移位而不是直接除法:
mult >> shift是纯整数位运算,比value × 3300 / 4095的 64 位除法快很多,在中断/高频采样场景更优。而calc_mult_shift把3300/4095这个分数预先折算成"左移后取整"的形式,保证精度足够。
一图总结:直接算 vs mult/shift
直接比例算(慢、有精度损失):
电压(mV) = value × 3300 / 4095
└──────── 一次 64 位除法
用 mult/shift(快、保留精度):
① 注册时只算一次:mult = round( (3300 / 4095) × 2^32 ) = 3461145806
↑ 把小数 0.806 放大 2^32 倍存成整数
② 每次读取:电压(mV) = value × 3461145806 >> 32
└─ 一次乘法 + 一次右移,无除法
效果对比(value = 2048):
直接算 = 2048 × 3300 / 4095 ≈ 1650.3 mV
mult = 2048 × 3461145806 >> 32 ≈ 1650.4 mV
误差 < 1mV(来自 mult 取整,ADC 量化噪声本就远大于此,可接受)一句话总结:mult/shift 是"把 value×3300/4095 这个带小数的除法,改写成一次快速乘法 + 一次右移"的预计算系数;calc_mult_shift 负责在注册时挑出这对系数,之后每次读取都用 value × mult >> shift 得到电压(mV)。
六、代码示例
6.1 基本使用:单通道读取
#include <oneos_config.h>
#include <device.h>
#include <adc.h>
#include <os_util.h>
#include <os_task.h>
void adc_basic_example(void)
{
os_device_t *adc_dev;
int32_t adc_value;
os_err_t ret;
/* 查找 ADC 设备 */
adc_dev = os_device_open_s("adc1");
if (adc_dev == OS_NULL)
{
os_kprintf("ADC device not found!\r\n");
return;
}
/* 使能 ADC,需检查返回值 */
ret = os_device_control(adc_dev, OS_ADC_CMD_ENABLE, OS_NULL);
if (ret != OS_SUCCESS)
{
os_kprintf("enable ADC failed, ret=%d\r\n", ret);
return;
}
while (1)
{
/* 读取通道 0 的 ADC 值,需检查返回值 */
ret = os_adc_read((os_adc_device_t *)adc_dev, 0, &adc_value);
if (ret != OS_SUCCESS)
{
os_kprintf("read ADC failed, ret=%d\r\n", ret);
break;
}
/* 计算电压值(mV):value * mult >> shift */
os_adc_device_t *adc = (os_adc_device_t *)adc_dev;
int32_t voltage_mv = (int32_t)(((uint64_t)adc_value * adc->mult) >> adc->shift);
os_kprintf("ADC Channel 0: raw=%d, voltage=%d mV\r\n", adc_value, voltage_mv);
os_task_msleep(500);
}
}6.2 多通道循环读取
#define ADC_CHANNEL_COUNT 4
void adc_multi_channel_example(void)
{
os_device_t *adc_dev;
int32_t adc_values[ADC_CHANNEL_COUNT];
int i;
adc_dev = os_device_open_s("adc1");
if (adc_dev == OS_NULL)
return;
os_device_control(adc_dev, OS_ADC_CMD_ENABLE, OS_NULL);
while (1)
{
/* 循环读取多个通道 */
for (i = 0; i < ADC_CHANNEL_COUNT; i++)
{
os_adc_read((os_adc_device_t *)adc_dev, i, &adc_values[i]);
}
os_kprintf("ADC: CH0=%d CH1=%d CH2=%d CH3=%d\r\n",
adc_values[0], adc_values[1],
adc_values[2], adc_values[3]);
os_task_msleep(1000);
}
}6.3 使用标准设备接口读取(同一通道多次采样)
说明:如 4.3 所述,
os_device_read的_adc_read用同一个pos重复采样,得到的是同一通道的多个样本,不是多个不同通道。
void adc_device_read_example(void)
{
os_device_t *adc_dev;
int32_t buffer[4]; /* 同一通道的 4 个样本 */
int bytes_read;
int i;
adc_dev = os_device_open_s("adc1");
if (adc_dev == OS_NULL)
return;
os_device_control(adc_dev, OS_ADC_CMD_ENABLE, OS_NULL);
/* 使用 os_device_read,pos=0 表示起始通道,一次读 4 个样本 */
bytes_read = os_device_read(adc_dev, 0, buffer, sizeof(buffer));
if (bytes_read <= 0)
{
os_kprintf("adc read failed, bytes_read=%d\r\n", bytes_read);
return;
}
os_kprintf("Read %d bytes (%d samples)\r\n", bytes_read, bytes_read / sizeof(int32_t));
for (i = 0; i < bytes_read / sizeof(int32_t); i++)
{
os_kprintf(" sample[%d]: %d\r\n", i, buffer[i]);
}
}6.4 中断方式读取(配合通知机制)
static os_device_t *gs_adc_dev = OS_NULL;
/* ADC 读取任务 */
void adc_task(void *parameter)
{
int32_t adc_value;
while (1)
{
/* 等待 ADC 转换完成通知 */
os_device_recv_notify_wait(gs_adc_dev);
/* 读取 ADC 数据 */
os_adc_read((os_adc_device_t *)gs_adc_dev, 0, &adc_value);
os_kprintf("ADC IRQ: value=%d\r\n", adc_value);
}
}
void adc_irq_example(void)
{
gs_adc_dev = os_device_open_s("adc1");
if (gs_adc_dev == OS_NULL)
return;
os_device_control(gs_adc_dev, OS_ADC_CMD_ENABLE, OS_NULL);
}七、ADC 的移植/适配(更换 MCU)
7.1 先认识 HAL 层:谁在真正操作 ADC 硬件
回顾 1.1 节,框架把差异封装在 HAL 层。HAL 层就是 drivers/hal/<厂商>/drivers/drv_adc.c,它为这个 MCU 实现 os_adc_ops 的三个函数(adc_read/adc_enabled/adc_control),并注册为 "adcX" 设备。
关键认知:drivers/misc/adc.c 是跨平台统一的框架层,只负责透传命令、算电压系数、注册,不碰任何硬件寄存器。换 MCU 只需换 drv_adc.c。项目 drivers/hal/ 下每个厂商都有 drv_adc.c,并列平级,一个工程只编译当前所选 MCU 的那一份:
| 厂商 | 文件 | 实现方式 |
|---|---|---|
| ST | drivers/hal/st/drivers/drv_adc.c | STM32 HAL(HAL_ADC_Start/HAL_ADC_PollForConversion) |
| GD | drivers/hal/gd/drivers/f4xx/drv_adc.c | GD32 库 |
| nRF | drivers/hal/nrf/drivers/nrf52/drv_adc.c | nrfx 库(nrfx_saadc_*) |
| 华大 HDSC | drivers/hal/hdsc/drivers/lfxx/drv_adc.c | 华大库 |
| FM | drivers/hal/fm/drivers/15f3/drv_adc.c | 复旦微库 |
7.2 移植的全景图:只有 HAL 层与 MCU 相关
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_adc_read / os_device_read / os_device_control │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ ADC 框架层(drivers/misc/adc.c) │
│ os_adc_read/enable/disable/control/isr/register(不改) │
└──────────────────────────────┬───────────────────────────┘
│ os_adc_ops(adc_read/control/enabled)
▼
┌──────────────────────────────────────────────────────────┐
│ HAL 层(drivers/hal/<厂商>/drivers/drv_adc.c) ★需适配★ │
│ 实现 os_adc_ops + 通道映射 + HAL_ADC_* 寄存器操作 │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU 的 ADC 外设与参考电压) │
└──────────────────────────────────────────────────────────┘移植的关键洞察:换 MCU,应用层与框架层一行都不用改,只需在 HAL 层提供 os_adc_ops 三函数并注册为 "adcX" 设备。
7.3 移植需要提供的"接口契约"
HAL 层 ADC 驱动要能用,必须实现 os_adc_ops 三函数。源码参考——ST 的 struct stm32_adc 与 ops(drivers/hal/st/drivers/drv_adc.c,与项目逐字一致):
struct stm32_adc
{
struct os_adc_device adc;
ADC_HandleTypeDef *hadc;
uint32_t timeout;
uint8_t status;
};static const struct os_adc_ops stm32_adc_ops = {
.adc_enabled = stm32_adc_enabled,
.adc_control = stm32_adc_control,
.adc_read = stm32_adc_read,
};接口契约要点:
ops 函数 | 职责 | 关键点 |
|---|---|---|
adc_read | 读取指定通道转换值 | ①通道号映射(stm32_adc_get_channel,逻辑通道→ADC_CHANNEL_x);②检查是否已使能(status == OS_ADC_ENABLE);③配通道、启动、轮询转换完成、读值;④用 dev->mult >> dev->shift 换算成 mV 写入 buffer(见 stm32_adc_read 的 *buff = (int32_t)((uint64_t)HAL_ADC_GetValue(...) * dev->mult >> dev->shift)) |
adc_enabled | 使能/禁用 | enable=TRUE 调 HAL_ADC_Init,FALSE 调 HAL_ADC_DeInit;同步更新私有 status 字段 |
adc_control | 处理控制命令 | 本项目框架未发控制命令,可返回 OS_SUCCESS |
关键理解 —— os_container_of(dev, struct stm32_adc, adc):struct stm32_adc 内嵌 os_adc_device adc 作为首成员。os_container_of 通过 dev 指针减去 adc 成员在结构体中的偏移,得到外层 struct stm32_adc *,从而访问 hadc/status。这与"内嵌继承"是配套的标准手法(5.10 Sensors 同理)。
7.4 移植的标准流程(5 步)
第 1 步:确认硬件能力——新 MCU 的 ADC 分辨率(10/12/14/16 位)、通道数、参考电压、是否支持中断。
第 2 步:定义设备控制块——内嵌 os_adc_device + ADC 句柄 + 状态:
struct my_adc
{
struct os_adc_device adc; /* 内嵌 ADC 设备(首成员) */
MY_ADC_HandleTypeDef *hadc; /* MCU 的 ADC 句柄 */
uint32_t timeout; /* 转换超时(ms) */
uint8_t status; /* OS_ADC_ENABLE / OS_ADC_DISABLE */
};第 3 步:实现 os_adc_ops 三函数——adc_read(通道映射 + 轮询转换 + mult>>shift 换算)、adc_enabled(Init/DeInit + 状态)、adc_control(可返回 OS_SUCCESS):
static os_err_t my_adc_read(struct os_adc_device *dev, uint32_t channel, int32_t *buff)
{
struct my_adc *dev_adc = os_container_of(dev, struct my_adc, adc);
/* 1. 逻辑通道 channel 映射到芯片 ADC_CHANNEL_x(my_adc_get_channel) */
/* 2. 若 status != OS_ADC_ENABLE → 打印 "adc disabled" 返回 OS_FAILURE */
/* 3. 配置通道、启动、轮询转换完成(HAL_ADC_Start / PollForConversion) */
/* 4. *buff = (int32_t)((uint64_t)HAL_ADC_GetValue(...) * dev->mult >> dev->shift) */
return OS_SUCCESS;
}第 4 步:填字段并注册——注册前填 ops/max_value/ref_hight/ref_low(os_hw_adc_register 会用它们算 mult/shift):
dev_adc->ops = &my_adc_ops;
dev_adc->max_value = 0; /* 0 表示由 probe_check 自动填充 */
dev_adc->ref_low = 0; /* ref 0 - 3.3v */
dev_adc->ref_hight = 3300;
os_hw_adc_register(dev_adc, dev->name, NULL);关键理解 ——
ref_hight/ref_low/max_value决定电压换算:ref_hight是参考电压高值(mV),max_value是 ADC 最大值(分辨率-1)。os_hw_adc_register用calc_mult_shift(max_value, ref_hight - ref_low, 1)自动算mult/shift(见五、电压换算公式)。填错参考电压或分辨率,电压换算结果就全错——这是移植最易错的地方。
第 5 步:OS_DRIVER_INFO/OS_DRIVER_DEFINE 自动探测注册:
static int my_adc_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
struct my_adc *adc = os_calloc(1, sizeof(struct my_adc));
OS_ASSERT(adc);
adc->hadc = (MY_ADC_HandleTypeDef *)dev->info;
adc->status = OS_ADC_DISABLE;
/* 填 ops/max_value/ref_hight/ref_low → os_hw_adc_register */
return OS_SUCCESS;
}
OS_DRIVER_INFO my_adc_driver = {
.name = "ADC_HandleTypeDef",
.probe = my_adc_probe,
};
OS_DRIVER_DEFINE(my_adc_driver, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);7.5 移植检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
adc_read 检查了使能状态? | 未使能时读取应返回失败,避免读到无效值 |
| 逻辑通道→硬件通道映射正确? | 应用传的 channel 是逻辑号,必须映射到芯片 ADC_CHANNEL_x |
ref_hight/ref_low/max_value 正确? | 决定 mult/shift,错了电压换算全错 |
换算用了 dev->mult >> dev->shift? | HAL 应返回 mV 电压,不要返回裸 ADC 原始值给框架层覆盖 |
是否 OS_DRIVER_DEFINE 自动初始化? | 否则 ADC 设备不注册,os_device_open("adc1") 失败 |
八、注意事项
- 参考电压配置:
ref_hight和ref_low必须在 HAL 层注册时正确设置,否则电压换算结果错误 - 通道有效性:确保
channel参数不超过硬件支持的最大通道数 - 采样时间:不同 ADC 外设需要不同的采样时间,由 HAL 层配置
- 中断通知:HAL 层需在 ADC 转换完成中断中调用
os_hw_adc_isr()发送通知 - 使能控制:读取前需确保 ADC 已使能(调用
os_adc_enable或OS_ADC_CMD_ENABLE)
📝 本节小结
核心要点回顾
本节围绕 ADC 设备展开,核心要点如下:
- 统一抽象:OneOS ADC 设备通过
os_adc_device_t统一抽象,HAL 层实现os_adc_ops接口。 - 电压换算:
os_adc_read()读取指定通道的 ADC 原始值,通过mult/shift换算为电压(mV),mult/shift由calc_mult_shift(max_value, ref_voltage, 1)在注册时自动算出。 - 通道控制:支持使能/禁用控制、多通道读取、中断通知等机制。
- 双接口访问:既可调用专用 API(
os_adc_read,按通道读),也可通过标准设备接口(os_device_read,同一通道多次采样)。 - 关键结构体:
os_adc_device_t内嵌parent(os_device,首成员继承)+ops+ref_hight/ref_low/max_value+mult/shift。 - 更换 MCU 的移植要点:框架层
adc.c与 HAL 层drv_adc.c解耦,换 MCU 只换drv_adc.c;HAL 需实现os_adc_ops三函数并注册为"adcX";ref_hight/ref_low/max_value填错则电压换算全错。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 读取前未使能通道 | 读取 ADC 前先 os_adc_enable 使能对应通道,否则读到无效值 |
| 电压换算错误 | 原始值 × mult >> shift 得到 mV,mult/shift 由 ADC 参考电压与位宽决定,需确认 |
| 混淆 ADC 通道号 | 通道号是设备框架定义的逻辑编号,与芯片物理通道需按板级映射对应 |
| 忽略采样稳定时间 | 通道切换后需要稳定时间,连续快速读取可能采到上一样值 |
误以为 os_device_read 读多通道 | _adc_read 用同一个 pos 重复采样,是"同一通道多样本",读多通道用 os_adc_read 分别指定 channel |
| 换 MCU 改框架层 | 只改 HAL 层 drv_adc.c,adc.c 框架与应用层不动 |
ref_hight/max_value 填错 | 决定 mult/shift,填错电压换算结果全错 |
| HAL 返回裸原始值 | adc_read 应返回 value * mult >> shift(mV),不要返回未换算的原始寄存器值给应用 |
下一步
掌握了 ADC 模数转换设备后,接下来学习 PWM 设备:
- 5.12 PWM设备:了解 OneOS PWM 设备的周期/占空比配置与 LED 呼吸灯等应用。