
简介面向嵌入式初学者的MC9S12DG128 AD采集实验工程围绕飞思卡尔S12系列单片机片上ATD模块演示多通道8位高速采样、数据转换与12864液晶实时显示适合学习CodeWarrior环境下外设驱动开发也适用于工业控制、汽车电子等需要模拟量采集的场景。压缩包共79个文件以cmd调试命令、prm链接参数、map映射、s19烧录文件和c源码为主另有o目标文件、ini工程配置、mcp工程文件等涵盖PE、TBDML、SofTec等常见调试器配置整体仅432KB便于快速下载。已有131人学习浏览。工程内含完整CodeWarrior项目可直接打开运行或对照阅读帮助理解AD转换器初始化、采样触发、中断读取与LCD显示驱动的配合方式同时多套调试脚本和链接参数可作为模板迁移到类似HCS12项目中使用。1. MC9S12DG128的AD实验读电位器谁都会难在把它当成可靠数据源MC9S12DG128的AD实验在很多开发板上是第一道“外设入门”关拧一个电位器在数码管或者串口上看到 01023 的变化就宣布自己学会 AD 了。可真到做项目时这块芯片的 ATD 模块远比表面复杂——参考电压怎么接、ATD 时钟分到多少兆、通道之间会不会串扰、第一次采样为什么永远不对这些才是真正决定你能不能把模拟量交给控制逻辑去用的关键。这篇文章我按做这颗芯片的常见路径走一遍从寄存器原理讲到最小驱动代码再讲到多通道扫描、滤波和校准最后把最容易翻车的几个现象按“现象→原因→解决”拆开。适合手里有这块芯片的开发板、想绕过一堆旧资料里的坑、把 AD 实验直接做实的人。2. 先认识 ATD 模块参考电压、ATD 时钟与采样时间三个参数决定成败2.1 ATD0 和 ATD1、AN0 到 AN7引脚怎么找和哪些外设共用MC9S12DG128 内部有两个独立的 ADC 模块分别叫 ATD0 和 ATD1每个模块最多支持 8 个模拟输入通道对应到芯片引脚上通常标成 AN0AN7 和 AN8AN15也有板子直接复用 AD0AD7 的丝印。多数实验板只把 ATD0 的 8 个通道引出来因为 ATD0 的引脚排布更靠近电位器、传感器插座而 ATD1 的引脚往往和普通 IO、串口、PWM 复用在同一个管脚上布局上不够友好。所以新手做实验优先找 ATD0 对应的一组引脚别一上来就对着芯片管脚图找“AD”字样。找引脚有个非常实用的办法先看板子原理图找电位器或者传感器输出接到了哪个管脚那个管脚名字里带 AN、AD、PAD 字样基本就是模拟输入。如果手头没有原理图就看开发板丝印很多板子会在电位器旁边标注 AD0 或 AN0。找不到丝印时再用万用表蜂鸣档量电位器中间抽头连到芯片的哪个管脚这个方法虽然土但比对着几十页管脚表猜要快得多。还有一个非常容易踩的共用问题这些模拟输入引脚在上电复位后是普通 IO要等到你配置完 ATD 寄存器才会切到模拟功能。如果同一个引脚上还接了按键、LED、数码管段选之类的数字器件测量结果会变成“数字电平和模拟电压叠加”的奇怪值而不是单纯的传感器电压。我见过不止一个同学在 AN7 上接了一个开关然后死活想不通为什么读数只有 0 和 1023 两个值。所以配置模拟通道之前先确认这个引脚没有被其他外设占用。2.2 VRH 与 VRL 基准为什么 5V 供电不等于 5V 参考ATD 模块并不是拿芯片的 VDD 当作满量程参考它使用的是独立的参考电压引脚 VRH 和 VRL。多数实验板上 VRH 直接接 5V、VRL 接 GND所以你在软件里换算电压时默认满量程是 5.0V这没问题。但要注意VRH 上的电压纹波、毛刺会直接进入转换结果的最高位数字电源的开关噪声会通过这个引脚耦合进 ADC导致数据在某个固定值附近来回跳。如果你的应用只有一颗芯片和一个电位器直接共用 5V 数字电源也可以但要在 VRH 引脚旁边放一个 0.1μF 陶瓷电容走线尽量短这是成本最低的改善手段。更讲究一点的做法是用一颗低噪声稳压芯片单独给 VRH 供电模拟地和数字地单点连接。做项目时模拟部分和数字部分的地如果混在一起采样值的抖动会明显到用肉眼都能看出来。判断是不是基准问题很简单把输入通道短接到 GND读到的应该接近 0再把输入短接到 VRH读到的应该接近满量程。如果这两个数都不稳定说明基准或者地线有问题不是软件滤波能救回来的。2.3 ATD 时钟分频与采样时间一张表算出一次转换的真实耗时ATD 模块有一个内部转换时钟它是由总线时钟分频得到的公式是ATD 时钟 总线时钟 / (2 × (PRS 1))PRS 是 ATD0CTL4 寄存器低 4 位的值范围 015。芯片手册里通常要求 ATD 时钟落在 0.5MHz2MHz 之间太高转换结果不可靠太低转换速度没法看。实际调板子时我一般把 ATD 时钟压在 1MHz2MHz 之间这样既稳定速度也够用。很多开发板的总线时钟是 24MHz在这个条件下不同 PRS 的典型结果如下PRS 值分频系数ATD 时钟结果0212MHz严重超规格不可用264MHz偏高容易跳动5122MHz处于上限能用7161.5MHz推荐1532750kHz偏保守但很稳采样时间由 ATD0CTL4 里的采样时间位控制常见的设置有 2 个 ATD 时钟周期和 4 个 ATD 时钟周期两种。采样时间短适合信号源阻抗低的场景信号源阻抗高比如直接用一个大电阻串到 ADC 引脚时就要用更长的采样时间否则内部采样电容还没充满就进入逐次比较读出来的值会系统性偏低。拿一个 10kΩ 电阻和 0.1μF 电容组成的传感器电路来举例2 个周期的采样时间很可能不够换成 4 个周期之后读数立刻稳定。这个参数不需要频繁改但遇到“怎么采都是偏小”的怪异现象时优先怀疑采样时间。3. 最小驱动代码寄存器初始化、轮询读取与电压换算3.1 初始化序列ATD0CTL2 到 ATD0CTL5 一次写对看别人工程的初始化代码会发现 ATD 这部分通常只有四五行寄存器赋值但顺序和值都有讲究。我的习惯是先上电、再配置模式、最后选通道中间留一个上电稳定延时。void AD_Delay(unsigned long count) { while (count--) /* 简单的软件延时用于ATD模块上电稳定 */ ; } void ATD_Init(void) { ATD0CTL2 0xC0; /* ADPU1 模块上电AFFC1 读结果寄存器后自动清完成标志 */ AD_Delay(100000); /* 给模块上电和内部基准留出几毫秒稳定时间 */ ATD0CTL3 0x00; /* 转换序列长度取最短单次只出一个结果结果放在DR0 */ ATD0CTL4 0x83; /* PRS3ATD时钟约1.5MHz采样时间取较长的一档 */ ATD0CTL5 0x00; /* 单通道模式选中AN0写入同时触发第一次转换 */ }这段代码里ATD0CTL2 的 bit6 也就是 AFFC 位很关键。不设置它读完结果寄存器后必须手动去清完成标志否则下一次循环会拿到上一次的旧数据设置之后只要读一次结果寄存器硬件自动把标志位清掉代码会简洁很多。ATD0CTL3 控制转换序列长度0x00 是最短序列对只读一个通道来说已经足够。ATD0CTL4 的低 4 位是分频值这里用 3配合 24MHz 总线时钟算出来的 ATD 时钟是 1.5MHz比较居中。ATD0CTL5 是启动和通道选择的关键重新写这个寄存器会触发一轮新的转换这也是接下来的读取函数能反复调用的原因。上电稳定延时不能省。ATD 模块刚上电时内部采样电容和参考电压网络还没有建立好立即启动转换容易把第一轮数据弄成 0 或满量程。这个延时不需要精确到毫秒级一个足够长的空循环就行但不要把它写成“先启动转换再延时”的顺序。3.2 单通道轮询读取一个 ATD_ReadChannel 函数解决 90% 的需求初始化完成之后读取单个通道的逻辑非常简单选通道、启动转换、等待完成标志、读结果。下面这个函数适配 AN0AN7 任意单通道。unsigned int ATD_ReadChannel(unsigned char channel) { unsigned int value; ATD0CTL5 channel 0x07; /* 低3位选择AN0~AN7写入即触发新转换 */ while ((ATD0STAT0 0x80) 0) /* CCF0置1表示第一个结果已经出来 */ ; value ATD0DR0; /* AFFC模式下读操作同时清掉CCF0标志 */ return value; }调用方式很直接raw ATD_ReadChannel(0);就能拿到 AN0 的原始值。这里channel 0x07是为了防止传入大于 7 的通道号虽然写越界值也能被寄存器吞掉但会选中一个你根本没用到的内部通道。while 循环等待的是 CCF0 标志也就是结果寄存器 0 的转换完成位单通道模式下结果一定放在 ATD0DR0 里。有一点要提醒这个函数是轮询方式转换期间 CPU 会卡在循环里。对大多数实验场景这不是问题比如读温度、读电压、读电位器速度要求都不高。但如果某个项目的主循环里还有实时性很强的任务就不要在循环里频繁调用这个函数等待可以用中断方式这部分我们放到多通道章节里讲。3.3 原始值到真实电压除法、偏移和取整的写法ATD 结果寄存器里的是一个无量纲的原始值。10 位分辨率下满量程是 1024 个码也就是 01023对应的电压范围是 VRH 和 VRL 之间的电压差。如果 VRH 接 5.0V、VRL 接地真实电压就是原始值 × 5.0 / 1024。实际工程里我建议直接输出毫伏而不是输出“0.823456V”这种高精度假象。因为 10 位的分辨率换算成电压一位码大约对应 4.88mV把结果写成七八位小数没有任何工程意义。unsigned int ATD_ReadMillivolt(unsigned char channel) { unsigned int raw; unsigned long mv; raw ATD_ReadChannel(channel); mv (unsigned long)raw * 5000UL; /* 假设VRH5.0V先放大到毫伏 */ mv mv 10; /* 右移10位等价于除以1024 */ return (unsigned int)mv; }这里有个容易写错的小地方很多人会除以 1023其实对 10 位 ADC 来说满量程值 1023 对应的是“无限接近参考电压”而不是等于参考电压。校准和换算时用 1024 做分母数学上更干净也避免出现除以 1023 后最大读数算出 5000.xx mV 的尴尬。如果你用的参考电压不是标准 5V把代码里的 5000 换成实际量到的毫伏数即可。4. 多通道扫描与连续转换把采样变成一套稳定机制4.1 多通道顺序扫描的配置MULT 与序列长度的组合一个项目里通常不会只采一路模拟量电机电流一路、电池电压一路、电位器一路这时候就要用多通道扫描。ATD 模块的 MULT 位用来开启多通道模式配合转换序列长度可以实现“一次启动自动把 AN0 到 AN7 按顺序采一遍”。void ATD_MultiScan_Init(void) { ATD0CTL2 0xC0; /* 上电 AFFC和单通道模式保持一致 */ AD_Delay(100000); ATD0CTL3 0x07; /* 序列长度设为8对应8个通道轮询一次 */ ATD0CTL4 0x83; /* PRS3采样时间与单通道时相同 */ ATD0CTL5 0x30; /* MULT1从AN0开始单次扫描8个通道 */ }启动扫描之后转换结果按顺序自动填入 ATD0DR0ATD0DR7。也就是说AN0 的结果在 DR0AN1 的结果在 DR1依此类推。读取时先判断序列完成标志再一口气把 8 个结果全部搬走。注意ATD0CTL3 的低 4 位序列长度字段我这里的 0x07 对应 8 个转换这是按“长度 字段值 1”的规则来的。网上有些工程里写 0x08 甚至 0x10那是因为不同版本的芯片资料对这个字段的表述有差异你在自己板子上以官方寄存器表为准不必照抄陌生代码。多通道模式下AFFC 的优势更明显如果不启用 AFFC读一个结果就要手动清一次标志8 个通道加序列标志位代码量翻倍还容易漏。开了 AFFC读 DR 寄存器就顺带清了对应标志最后检查 SCF 序列完成标志即可。4.2 结果寄存器的顺序对齐D0 到 D7 与 AN0 到 AN7 的映射读取扫描结果的代码如下void ATD_ReadMulti(unsigned int buffer[8]) { unsigned char i; ATD0CTL5 0x30; /* 重新写入CTL5触发一轮新的多通道扫描 */ while ((ATD0STAT1 0x80) 0) /* SCF置1表示整轮序列转换完成 */ ; for (i 0; i 8; i) buffer[i] ATD0DR[i]; /* DR0对应AN0DR1对应AN1以此类推 */ }这里最容易翻车的地方是结果对应关系。有些开发板的丝印把通道标成 AD0、AD1和芯片内部的 DR0、DR1 在顺序上一致但也有板子中间插了别的硬件跳线导致 AN2 和 AN3 在 PCB 上互换过位置。不要依赖丝印名字直接拿一根杜邦线把 AN0 接到一个固定电压比如 3.3V然后看程序里哪一个 buffer 元素读出来是满量程附近用这个实测结果做一张通道映射表再写进代码注释里。4.3 中断方式收结果比轮询更快、比 DMA 更简单的中间选项轮询方式在单通道实验里没有问题但多通道扫描时 CPU 会空转等待整轮转换完成这段时间虽然不长在控制类程序里却很可能耽误事。中断方式是一个更好的中间选项ATD 转换完成时触发中断在中断服务程序里把结果读走主循环完全不用等待。void ATD_Interrupt_Init(void) { ATD0CTL2 0xE0; /* ADPU1AFFC1ADIE1 使能转换完成中断 */ AD_Delay(100000); ATD0CTL3 0x07; ATD0CTL4 0x83; ATD0CTL5 0x30; /* 启动多通道扫描 */ ATD0STAT0 0x00; /* 清掉残留的完成标志 */ } #pragma CODE_SEG __NEAR_SEG NON_BANKED void ATD0_ISR(void) { unsigned char i; for (i 0; i 8; i) sample[i] ATD0DR[i]; /* 中断里只搬数据不做浮点运算 */ ATD0STAT1 0x80; /* 清序列完成标志SCF */ } #pragma CODE_SEG DEFAULT中断服务程序里要注意三个习惯。第一只做数据搬运和标志清理不要做电压换算、滤波、显示这类耗时操作否则中断时间过长会阻塞其他中断。第二读取 DR 寄存器之后要确认标志确实被清了如果用的是非 AFFC 模式必须手动写 ATD0STAT1 清 SCF否则会反复进入中断。第三中断服务函数名和中断向量表的对应关系由开发环境决定移植代码时先查一下官方的中断向量配置不要直接复制网上不同芯片工程的函数名。5. AD 实验常见问题排查跳变、串扰、首测失效与毛刺5.1 现象采样值整体偏高或偏低还是无规律跳动现象一把输入接到一个精确的 2.5V 电压上读出来一直在 2.47V 到 2.53V 之间晃现象二读出来稳定在 2.45V用万用表量却是 2.50V。两种现象的根因完全不同。先看整体偏移。最常见原因是 VRH 实际电压不是 5V而是开发板上某个稳压器输出的 4.9V 或者 5.05V。你按 5.0V 换算电压自然整体偏移。解决方法是把 VRH 引脚的电压用万用表实测然后把换算代码里的 5000 改成实际毫伏数。还有一种偏移是因为信号源内阻太大采样时间不足导致采样电容没充满结果会比真实值偏低。再看无规律跳动。跳动通常和地线有关特别是电烙铁焊接的杜邦线、面包板长跳线都会引入几十毫伏的噪声。解决方法是缩短模拟信号线长度、使用带屏蔽的线、在 ADC 引脚和 GND 之间加 0.1μF 电容。不要把锅都甩给“芯片精度不够”这块 10 位 ADC 的静态精度没这么差多数跳动是外部布局造成的。5.2 现象改读 AN1AN0 的读数跟着被拉过去实验板上有两个电位器分别接 AN0 和 AN1。单独读 AN0 时正常单独读 AN1 也正常但只要快速交替读两个通道两个读数都会变得不准甚至 AN0 的值明显偏向 AN1。这个现象叫通道串扰根因有两个层面。第一芯片内部采样电容在转换完 AN1 之后还残留一部分 AN1 的电荷立刻去采 AN0 时残留电荷还没来得及释放就被并入了 AN0 的采样结果。第二外部电路上两个通道的引脚距离很近PCB 或者说杜邦线之间存在寄生电容高阻抗的传感器信号很容易被相邻通道耦合。解决通道串扰最有效的办法是放慢切换节奏。多通道扫描时序列长度已经给足的话芯片自己就带了“先复位采样电容再采样”的机制问题往往出现在你自己写的轮流调用单通道读取函数、切换速度非常快的情况下。另一种做法是硬件上在每个通道引脚串一个几百欧姆的电阻限制寄生电容的冲击电流效果立竿见影。如果最终读数还是要用于精确控制还有一个软件补救每个通道切换后丢第一次采样结果用第二次以后的数据代价是多花一点转换时间。5.3 现象板子上电后第一次转换结果永远是 0x0000 或满量程很多人在主循环里一开始就调用 ATD_ReadChannel结果发现程序跑起来后第一次数据要么是 0 要么是 1023之后的数据完全正常。这个问题出现得极其规律基本可以断定是上电时序问题。原因有两类。第一类是 ATD 模块上电后没有留稳定时间ADPU 位置 1 只是打开电源模块内部的偏置电路和参考电压建立需要一段时间在这个期间启动转换得到的就是无效值。第二类是外部传感器或电位器的供电还没稳定芯片已经开始采样采到的是电源爬升过程的中间值。解决方法是约定俗成的初始化之后先做一次“空转采样”把结果直接丢弃再用后面输出的数据。如果连丢一次都觉得不够稳就做两次空转。这不算浪费因为 ADC 的建立时间本来就是硬件特性不是代码能靠逻辑绕过去的。5.4 现象加了滤波还是抖问题出在结果寄存器被重复读有人写代码时为了取平均值在同一个转换序列结束前反复读同一个结果寄存器以为能拿到多个不同时刻的采样值。实际结果可能是滤波之后依旧抖动甚至抖动更厉害。原因在于没有清完成标志就重复读寄存器。AFFC 模式下读 DR0 的首次操作会清掉 CCF0第二次读同一个寄存器时转换序列并没有真正产生新数据你读到的只是 CPU 某个缓存或寄存器里残留的旧值。这时候做的“平均”实际上是把同一个数加了 N 次再除以 N等于没做。正确做法是把读到的原始值存进一个数组通过数组做滑动平均或去极值平均而不是重复读寄存器。6. 让 AD 实验能上项目滑动平均滤波与两点校准的十行收尾最后这个阶段把实验代码往工程上收。滤波和校准这两个环节是实验代码和量产代码之间最常见的分水岭。滤波我推荐用去极值滑动平均原理简单、占用资源小每轮连续读 5 个原始值去掉最大值和最小值剩下三个取平均。这种滤波方式对尖峰脉冲免疫又不像纯滑动平均那样滞后严重。unsigned int ATD_FilteredRead(unsigned char channel) { unsigned int sample[5]; unsigned long sum 0; unsigned char i; unsigned int max, min; for (i 0; i 5; i) sample[i] ATD_ReadChannel(channel); /* 连续读5次 */ max sample[0]; min sample[0]; for (i 0; i 5; i) { if (sample[i] max) max sample[i]; if (sample[i] min) min sample[i]; } for (i 0; i 5; i) sum sample[i]; sum - max min; /* 去掉一个最高和一个最低 */ return (unsigned int)(sum / 3); }校准更值得做。直接用原始值 × 5000 / 1024换算电压默认了满量程精确等于 5V、零点精确等于 0V实际芯片和外围电路总有一点误差。我的做法是在板子上做两点校准给某个通道输入 0V记录 raw0再输入一个已知精确电压 ref_mV记录 raw1。之后所有这个通道的转换都套用斜率和偏移修正。unsigned int ATD_CalibratedMillivolt(unsigned int raw, int raw0, int raw1, unsigned int ref_mv) { if (raw1 raw0) return 0; return ((unsigned long)(raw - raw0) * ref_mv) / (raw1 - raw0); }注意 raw 和 raw0 的差值要用 unsigned long 计算防止乘法规格溢出。两点校准只修正线性误差大多数情况下能把 10 位 ADC 的整体误差控制在几个毫伏量级。校准参数建议存到芯片内部的 EEPROM 或者 Flash 参数区不要在每次启动时重新测量否则温度漂移后你会分不清是硬件漂了还是校准参数过期了。关键不是把代码写得多花哨而是每次改动后做同一个验证动作拿万用表量参考电压和程序输出的毫伏值对比至少测 0V、中间电压、接近满量程三个点误差在预期范围内再加滤波和校准。我自己的习惯是永远保留一路“裸读”调试入口校验滤波效果时切到原始值看变化这样能很快判断到底是传感器信号本身在抖、还是电源在抖、还是算法把趋势给削平了。这些留一手的方法都是被各种莫名其妙的模拟量问题教育出来的希望你少走一遍弯路。本文还有配套的精品资源点击获取