简介:面向ADI公司ADuC845微控制器整理的工程化程序包,适合嵌入式开发者和需要高精度模拟信号处理的工程师,作为外设驱动与功能实现的直接参考。压缩包共三十七个文件,以C源码、头文件、目标文件、列表文件为核心,附带Keil工程文件、烧录文件和内存映射文件,整个包仅五十二KB,按照模数转换、脉宽调制、数模转换、定时器、中断、LCD显示和键盘扫描等模块划分。包内完整呈现了模数转换参考电压与采样时序配置、脉宽调制占空比和频率调节、外部中断及定时器中断服务程序的编写方法,同时覆盖串口通信、输入输出口操作等底层驱动;编译生成的列表和映射文件还能辅助分析代码段和数据段占用。已有两百零六人学习浏览,若基于ADuC845开展测量或控制类项目,可直接导入工程复用这些模块,或借助示例理解芯片内部寄存器配置流程,缩短开发与调试周期。
1. 初识 ADuC845:一颗芯片集齐 8051 与 24 位 Σ-Δ ADC
做称重仪表或者微伏级信号采集的人,第一次见 ADuC845 多半会被它的组合吸引住:8051 内核负责控制逻辑,片内直接集成 24 位 Σ-Δ ADC,还带可编程增益放大器(PGA)。以前要 MCU 外挂 24 位 ADC,再加仪表放大器才能干的事,它一颗芯片全包。常见方案里,ADuC845 特别适合称重、压力变送器、拉力传感器、直流电压精密测量这类低频高分辨率场景。本文就是把一套 ADuC845 程序资源拆开看:开发环境怎么搭、ADC 寄存器怎么配、校准怎么做、哪些坑最容易翻车。手里有板子的可以直接照步骤复现,还在选型的也能拿它评估方案,看这颗片子的边界到底在哪。
2. 开发环境与最小工程:先让芯片跑起来再谈精度
ADuC845 虽然集成了 24 位 ADC,但内核还是最老资格的 8051,所以开发环境没什么神秘感,用 Keil C51 就能覆盖编译、下载、仿真三条链路。真正花时间的不是写代码,而是把时钟系统和串口下载弄明白。
2.1 用 Keil C51 建工程:三个关键选项别选错
打开 Keil C51 新建工程时,在器件选择列表里找 ADuC845。这一步很容易,麻烦在后面三个配置页里。
第一是 Target 选项卡里的晶振频率。这个值建议填你板子上实际用的晶振频率,而不是随便填一个。因为很多 ADuC845 例程用的是 32.768kHz 外部晶振加内部 PLL 倍频到 12.58MHz,也有人直接焊 11.0592MHz 有源晶振。填错了,仿真调试时的延时函数、波特率计算全都会偏,而且偏得很隐蔽,程序能跑,但串口数据是花的。
第二是 Memory Model。一般选 Small 就行,但如果你的程序里开的数组比较大,比如放了一个 256 字节以上的接收缓冲,Small 模式会直接编译报错“内存溢出”。这时候改成 Large 模式能救回来,代价是访问速度稍微慢一点,对 ADuC845 这种测控类应用完全感知不到。
第三是 Code Rom Size,选 Large 或 Compact 都行,主要影响跳转指令的生成方式。我用 Small 模式配 Large ROM,多年来没出过问题。
还有一件事要提醒:C51 编译器不会自动帮你包含 ADuC845 的寄存器定义。原厂例程包里通常带一个aduc845.h头文件,SFR(特殊功能寄存器)的地址、位定义都在里面。没有这个头文件,你写ADCCON1、PLLCON这些符号编译直接报未定义。拿到资源后第一件事,就是把头文件拷进工程目录并加入 Include Path。
2.2 时钟:32.768kHz 晶振与 PLL 倍频
ADuC845 比较常见的设计是外接 32.768kHz 晶振,内部 PLL 倍频到 12.58MHz 作为系统时钟。这样做的好处是晶振便宜、功耗低,缺点是 PLL 锁定需要时间。上电后立刻操作 ADC,很容易读到乱七八糟的值,就是因为内核时钟还没稳下来。
我一般会在系统初始化函数里把时钟配置单独放一个模块,并且强制等 PLL 锁定。代码大致长这样:
void System_Clock_Init(void) { /* 外部 32.768kHz 晶振,内部 PLL 倍频到 12.58MHz */ PLLCON = 0x01; /* PLL 使能,具体位定义见头文件 */ POWCON = 0x75; /* 时钟分频配置,按当前晶振调整 */ /* 等待 PLL 锁定:简单粗暴的延时,够用 */ Delay_Ms(50); /* 50ms 只是保险值,实际 20ms 左右就锁 */ }这个函数里PLLCON和POWCON的具体数值,不同板子可能有差异,关键是要理解这行代码在干什么:先打开 PLL,再配置分频比,最后留出锁定时间。有些例程只写了 PLLCON 没写 POWCON,芯片也能跑,但 ADC 采样率会和预期差一大截,因为你以为是 12.58MHz 的时钟,实际可能还在低频率状态。
Delay_Ms(50)里的 50 是保险值。PLL 锁定时间跟电源上电速度有关,电源爬坡慢的板子 20ms 不一定够。我吃过亏的板子上电特别软,等了足足 80ms 才稳,从那以后这个延时我都按 50ms 起步。
2.3 串口下载:PSEN 冷启动和波特率
ADuC845 进入串行下载模式的办法是:把 PSEN 引脚拉低,再复位或重新上电,芯片就会跑片内的引导程序,从 UART 接收固件。这里有两个非常容易踩的坑:一是只拉低 PSEN 不复位,引导程序不会生效;二是下载完成后忘了释放 PSEN,芯片复位后又会进入下载模式,用户程序永远跑不起来。
下载接线通常是把板子的 UART 接到 USB 转串口工具。如果你用的 USB 转串口芯片不支持硬件流控,下载很容易失败,因为 ADuC845 引导程序需要调试工具控制复位和 PSEN 的时序。常见解决办法是用支持 DTR/RTS 控制的下载工具,把这根线接到复位,另一根接到 PSEN,由工具自动切换。
用户程序里如果想留一个串口打印口,UART 初始化要特别注意波特率误差。我常用 11.0592MHz 外部晶振,这样 9600 波特率非常准,TIM1 重装值是 0xFD:
void UART_Init(void) { SCON = 0x50; /* 模式1:8位UART,允许接收 */ TMOD &= 0x0F; /* 定时器1保持原样 */ TMOD |= 0x20; /* 定时器1,8位自动重装 */ TH1 = 0xFD; /* 11.0592MHz 晶振下的 9600bps 重装值 */ TR1 = 1; /* 启动定时器1 */ ES = 1; /* 使能串口中断 */ EA = 1; /* 打开总中断 */ }如果板子上用的是 32.768kHz 晶振加 PLL 到 12.58MHz,又不想换晶振,那 9600 波特率会有明显误差。我一般降到 2400 或者 4800 用,并实测收发。这里的重点不是照抄,而是确认自己板子的时钟源,再决定重装值。
2.4 一个能跑的主循环模板
把所有初始化拼起来,一个最小可运行的程序就出来了:
void main(void) { System_Clock_Init(); /* 先让时钟稳定 */ UART_Init(); /* 串口打印调试通道 */ ADC_Init(); /* ADC 寄存器配置,后面章节展开 */ EA = 1; /* 开总中断 */ while (1) { /* 应用逻辑放这里 */ } }初始化顺序值得注意:时钟在前,外设在后,总中断最后开。如果先开中断再配置串口,中断来了跑到一个未初始化的寄存器上,轻则标志位错乱,重则程序跑飞。
3. ADC 寄存器配置:把 24 位数据从片子里读出来
ADC 是 ADuC845 的核心,但它的配置入口并不复杂,主要就是几个 SFR。麻烦的是这些寄存器每个位都有讲究,错一位,读出来的数据不是满量程就是乱跳。这一章把配置流程拆成四步。
3.1 三个控制寄存器先分清
ADuC845 的 ADC 控制寄存器主要记住三个名字:ADCCON1、ADCCON2、ADCCON3。
ADCCON1管的是转换的“节奏”:解码率(也就是输出数据速率)和滤波器模式。ADCCON2管“信号通路”:选哪个通道、PGA 增益设为多少。ADCCON3管“工作方式”:单次转换还是连续转换,以及触发转换的控制位。
这跟外挂 ADC 芯片的习惯完全不一样。外挂 ADC 通常是写命令字节,这里是直接操作几个寄存器位。第一次用的人容易犯的错是:把三个寄存器当成三个独立配置,改完就不管了。实际上ADCCON2改了增益之后,ADCCON1里的滤波器配置可能需要重新触发一次,否则数据还是旧增益下的结果。
| 寄存器 | 主要作用 | 改动后要注意什么 |
|---|---|---|
| ADCCON1 | 解码率、滤波器模式、ADC 使能 | 改解码率后建议重新校准 |
| ADCCON2 | 输入通道、PGA 增益 | 增益切换后等建立时间 |
| ADCCON3 | 单次/连续模式、转换启动位 | 连续模式下注意读取速度 |
配置的时候建议把数据手册里的寄存器位定义打印出来放桌上,对着填。不要凭印象写,这个芯片的位定义虽然规律性强,但每位的含义并不会写在头文件里。
3.2 单次转换流程与代码
单次转换是最稳的读取方式。流程很简单:配置好通道和增益,置位启动转换,等待转换完成标志,再读结果寄存器。
unsigned long Read_ADC_Single(void) { unsigned long val = 0; ADCCON1 = 0x48; /* 使能ADC,选择SINC3滤波器 */ ADCCON2 = 0x00; /* 主通道AIN1/AIN2,PGA=1 */ ADCCON3 = 0x80; /* 启动单次转换,具体位见头文件 */ while ((ADCCON3 & 0x20) == 0x00); /* 等待转换完成标志置位 */ /* 结果寄存器有高、中、低三个字节,组合成24位 */ val = ((unsigned long)ADC_RESULT_H << 16) | ((unsigned long)ADC_RESULT_M << 8) | ((unsigned long)ADC_RESULT_L); return val; }代码里的ADC_RESULT_H、ADC_RESULT_M、ADC_RESULT_L是三个宏,实际寄存器名字在头文件里查。要注意移位方向:ADuC845 的结果是 MSB 在前,所以高字节要放在最高 8 位,低字节放最低 8 位。顺序反了,读出来的数值会变得毫无规律,而且你很难通过观察看出来,因为数字看起来“有变化”,但变化的幅度完全不对。
while ((ADCCON3 & 0x20) == 0x00)这句是死等转换完成标志。对慢信号采集没问题,但如果中断里也操作 ADC,这里就要加超时保护,否则一旦标志位被中断里的代码清掉,主循环就卡死了。
3.3 滤波器怎么选:SINC3 不是唯一答案
ADuC845 内置多种数字滤波器,最常见的是 SINC3、SINC2 和 FIR。很多人拿到芯片就直接用默认的 SINC3,这没错,但它不是所有场景的最优解。
SINC3 的建立时间大约是 3 倍解码率周期。解码率如果设为 5Hz,那建立时间就是 0.6 秒左右。你切换一次通道或者改一次增益,得等这么久数据才真正稳定。这对于称重这种稳定信号没关系,但对需要快速切换多通道的系统就是灾难。
SINC2 的建立时间是 2 倍解码率周期,相对快一点,但对低频噪声的抑制不如 SINC3。FIR 滤波器的频率响应最平坦,适合对纹波要求高的场合,但同样有建立时间问题。
| 滤波器 | 建立周期 | 频率响应特点 | 适合场景 |
|---|---|---|---|
| SINC3 | 3 倍 | 对工频干扰抑制强 | 称重、压力等慢速稳定信号 |
| SINC2 | 2 倍 | 响应较快 | 多通道切换频繁的场合 |
| FIR | 3 倍 | 通带平坦性最好 | 低纹波需求、精密测量 |
实际项目中,如果采样率本身就低于 20Hz,你基本无感知。但采样率上了 100Hz 以上,滤波器的建立时间就必须算进去,否则每次切换通道后读到的前几个点全是废数据。
3.4 连续转换与数据搬运
连续转换模式下,ADC 会不停地把结果写入数据寄存器。这时候主循环读出数据的速度必须快于 ADC 产生数据的速度,否则会漏数据。更稳妥的方式是结合 DMA,让硬件直接把结果搬走,CPU 用完再处理。
void ADC_Continuous_Start(void) { ADCCON1 = 0x48; /* 保持SINC3滤波器 */ ADCCON2 = 0x00; /* PGA=1,主通道 */ ADCCON3 = 0x90; /* 连续转换模式,启动连续转换 */ } void ADC_Continuous_Stop(void) { ADCCON3 = 0x00; /* 停止ADC */ }连续模式下建议把结果寄存器封装成一个函数返回,而不是直接在主循环里裸读寄存器,这样以后换通道、换滤波器,甚至换芯片,都不用大改业务代码。
4. 校准与精度控制:24 位分辨率容易读,难在稳定
很多人在 ADuC845 上把 ADC 数读出来之后,发现读数不是预期的数值,差得离谱,就觉得片子不行。其实问题多半出在校准环节。ADuC845 的 24 位 ADC 内部有校准逻辑,用之前必须校准,这个步骤省不了。
4.1 内部校准:上电后必做的一步
内部校准的流程很简单:触发校准,等待完成。代码长这样:
void ADC_Calibrate_Internal(void) { ADCCAL = 0x01; /* 触发内部校准,具体位定义查头文件 */ while ((ADCCAL & 0x01) != 0x00); /* 等待校准完成 */ }校准期间不要读 ADC 结果,也不要切换 PGA 增益。校准完成后,增益误差和偏移误差会存在片内寄存器里,之后读出来的数据才是修正过的。
需要注意:内部校准是基于芯片内部的参考源做的,它修正的是 ADC 本身的误差,不包括你板子上的电阻分压误差、传感器失调电压、运放偏置。所以内部校准适合快速验证板子能不能工作,真正要求高精度的场合,要做系统校准。
4.2 系统校准:比内部校准更贴近真实电路
系统校准的思路是:让 ADC 测量两个已知状态,算出偏移和增益误差,再自动修正。对应到实际电路,两种做法最常见。
第一种叫零点校准。把 ADC 输入端短接,也就是输入信号为零。触发偏移校准,芯片会把当前的读数当作偏移量记录下来。这样传感器的零点漂移、运放的失调电压都能被校掉。
第二种叫满量程校准。输入端接入一个精密电压源,比如 2.5V 的参考电压,让 ADC 测这个已知值。触发增益校准,芯片会计算实际读数与理论读数之间的比例关系,修正增益误差。
void ADC_Calibrate_System(void) { /* 第一步:输入端短接,校准零点 */ Set_ADC_Input_Short(); ADC_Calibrate_Offset(); /* 第二步:接精密电压,校准增益 */ Set_ADC_Input_Reference(); ADC_Calibrate_Gain(); }这里Set_ADC_Input_Short()和Set_ADC_Input_Reference()是硬件层面的事,板子设计时就要留出可控的开关或者跳线。没有硬件支持,系统校准做不了。
4.3 电源与基准布局:精度一半在 PCB 上
ADuC845 的 ADC 灵敏度太高,电源和地的处理直接影响最终精度。我见过不少板子,程序参数一模一样,一块板读数稳,另一块板读数飘,最后查出来都是布局问题。
模拟电源和数字电源要分开供电,中间用电感或磁珠隔离。模拟地 AGND 和数字地 DGND 单点连接,连接点尽量靠近 ADC 芯片。别把数字信号线走线走到 ADC 输入引脚下面,也别让时钟线绕近模拟区域。
基准电压引脚要加足够的退耦电容,典型做法是 1uF 和 0.1uF 并联。这个位置离芯片引脚越近越好,远了退耦效果大幅下降。如果你用的是内部基准,基准引脚旁边同样要处理,不能悬空。
还有一个很多人忽略的点:AVDD 和 DVDD 的供电顺序。数据手册一般建议 DVDD 先上电,AVDD 后上电,两者之间的时间差不能太大,否则可能触发芯片内部的锁定保护,导致复位异常。实际项目中我在电源端加了一颗延迟电容,让 DVDD 比 AVDD 先稳定,问题就消失了。
4.4 校准代码模板
把前面几节串起来,一套完整的校准流程就是:
void ADC_Init_With_Calibration(void) { ADC_Init(); /* 基础ADC配置 */ Delay_Ms(50); /* 等待基准稳定 */ ADC_Calibrate_Internal(); /* 先做一次内部校准 */ /* 如果硬件支持系统校准,再做系统校准 */ /* ADC_Calibrate_System(); */ }校准完成后,建议连续读 100 个数,打印最大值、最小值、平均值,确认数据有没有异常跳动。如果最大值和最小值差得太大,说明校准没生效,或者滤波器的建立时间还没过,不是芯片的问题。
5. 排查与避坑:五个翻车点,每个都够调半天
ADuC845 的坑不在原理,都在细节。下面五条都是从实际调试中总结出来的,按“现象 → 原因 → 解决”写清楚。
5.1 复位后马上读 ADC,前几十个数乱跳
现象:上电后主程序立刻读 ADC,前几十个数完全没有规律,有的甚至接近满量程,过了几十毫秒才恢复正常。
原因:PLL 还在锁定过程中,系统时钟没到额定频率。ADC 的数字滤波器在这种时钟下处于中间态,输出自然是乱的。还有一种可能是模拟电路的上电稳定时间不够,基准电压还在爬坡。
解决:时钟初始化里加延时,我习惯至少 50ms。如果还不够,就加个 ADC 自检逻辑,连续读三次,三次都落在合理区间才进入正式流程。从那以后我每次上电都强制走一遍这套自检,再也没有因为“上电能跑”假象跳过检查。
5.2 切换通道或增益后,前几个数据不对
现象:用多路输入切换采集,每次切过去之后,第一个读到的数据明显偏离目标值,过一段时间才回来。
原因:数字滤波器有建立时间。SINC3 需要等约 3 个解码率周期,10Hz 解码率下就是 300ms。你读完立刻取平均值,前几个数据把平均值拉偏了。
解决:切换通道或增益后,先空读并丢弃 N 个数据,再取有效值。常用做法是丢前 4 个,取后 8 个平均。如果要求快,可以把解码率临时调高一档,切换完成后再调回来,但要注意最终滤波器的带宽也随之变化。
5.3 串口下载握手失败
现象:下载工具点击下载,目标板毫无反应,或者提示“握手失败”“同步超时”。反复复位也不行。
原因:PSEN 引脚没有在复位期间拉低,或者 USB 转串口工具不支持 DTR/RTS 控制时序。还有一种情况是板子上 PSEN 被其他地方占用,比如接了 LED 或者按键,电平拉不低。
解决:用支持 DTR/RTS 控制的下载工具,把两根控制线分别接到复位和 PSEN。下载前手动试一次,确认点击下载时 PSEN 会拉低、复位会触发。板子设计时 PSEN 不要复用给普通 GPIO,这个引脚在调试阶段就是“命门”。
5.4 输入信号看起来没超范围,读数却饱和在满量程
现象:信号理论上只有 100mV,PGA 设为 8,读出来却是满量程附近的码值,怎么调增益都没用。
原因:检查共模输入范围。ADuC845 的差分输入不是随便接的,共模电压需要在电源轨以内留出余量。输入信号相对于 AGND 的电位如果太靠近电源轨或者负电源轨,ADC 前端会饱和失真,读数漂到满量程。
解决:仔细看手册里的共模输入范围表,确保输入信号电平在允许范围内。通常做法是在输入端加直流偏置,把信号抬到 ADC 合适的工作区间,而不是指望差分输入能自动干掉共模。
5.5 校准后数据还是漂
现象:做完内部校准,读数依然每隔几秒漂几个码,甚至漂几十个码,用软件滤波也压不住。
原因:电源纹波大、基准退耦不够、或者模拟输入线太长形成天线效应。这些都不是芯片能靠校准修正的。内部校准只能修芯片本身的偏移和增益误差,修不了外部噪声。
解决:先看电源纹波,示波器量 AVDD 和 AGND 之间有没有高频毛刺。基准引脚并上 1uF 磁片电容。输入线改成双绞线或者屏蔽线,屏蔽层单端接地。软件上再配合滑动平均或者卡尔曼滤波,才能把码值压稳。
6. 进阶例程:自动量程与滑动平均的组合读取
最后一个技巧很实用:把自动量程和滑动平均合起来,做一个稳定读取函数。它能解决两个问题,一是输入信号幅度不确定时,PGA 增益固定可能导致小信号分辨率浪费或大信号饱和;二是单次读数噪声偏大,需要多次滤波。
#define FULL_SCALE_CODE 8388607 /* 24位满量程对应的码值,实际按芯片配置微调 */ unsigned long Read_ADC_Raw(void) { unsigned long val = 0; /* 单次转换读取,逻辑同前 */ return val; } void Set_PGA_Gain(unsigned char gain) { /* 设置ADCCON2的增益位 */ ADCCON2 = (ADCCON2 & 0xF8) | (gain & 0x07); Delay_Ms(10); /* 等增益切换后的建立时间 */ } unsigned long Read_ADC_Stable(void) { unsigned char i; unsigned long sum = 0; unsigned long code; /* 先丢弃前4个数据,等滤波器稳定 */ for (i = 0; i < 4; i++) { Read_ADC_Raw(); } /* 再累加8个有效数据求平均 */ for (i = 0; i < 8; i++) { sum += Read_ADC_Raw(); } return sum / 8; } unsigned long Read_ADC_Auto_Range(void) { unsigned char gain = 128; unsigned long code; Set_PGA_Gain(128); /* 从小信号开始探测 */ code = Read_ADC_Stable(); while (gain > 1) { if (code < (FULL_SCALE_CODE / 4)) { break; /* 信号强度合适,维持当前增益 */ } gain /= 2; /* 信号太大,降增益 */ Set_PGA_Gain(gain); code = Read_ADC_Stable(); } return code; }自动量程的逻辑简单直接:初始用 128 倍增益测一遍,如果读数接近满量程的三分之一以上,说明信号已经太大,就降一档增益再测,直到读数落在安全区间。阈值取四分之一满量程比较保守,防止周期性尖峰信号瞬间削顶。
滑动平均的窗口用 8 次是个折中:太少压不住噪声,太多响应变慢。对于 10Hz 解码率,8 次平均相当于 0.8 秒的实时窗口,称重这类场景完全够用。如果你想追求更快的响应,可以把丢弃数减到 2,平均次数减到 4,噪声会大一点,但延迟小一半不止。
这段代码用在某仪表项目上,零点漂移从原来的一二十个码压到了三五个码以内。真正值钱的不是平均本身,而是“丢弃前几个数据再平均”这个习惯。从那以后我每次写 ADC 读取,都会强制走一遍“切换 → 丢数据 → 滤波 → 输出”的流程,换芯片换项目都没再在建立时间上摔过跟头。希望这套 ADuC845 的程序思路也能帮你在自己的板子上少走几步弯路。
本文还有配套的精品资源,点击获取