
简介面向嵌入式开发者的一份MC9S12DG128模数转换与12864液晶显示实验工程包覆盖飞思卡尔HCS12系列微控制器的多通道八位高速模数转换配置、模拟信号采集、中断读取及液晶屏驱动显示等完整流程并涉及采样率、参考电压、输入通道选择等关键参数设置实验从底层寄存器操作入手有助于理解芯片内部外设工作机制。压缩包为RAR格式共七十九个文件总大小432KB主要包含C语言源文件、工程配置文件、调试烧录命令、映射内存文件及S19烧写文件等文件类型配套较完整。已有131人学习使用。包内工程可在CodeWarrior集成开发环境中直接打开运行结合源码与项目结构可快速掌握模数转换结果读取、数据处理以及12864液晶屏显示控制的编程方法目录组织清晰驱动代码便于二次修改与复用也可为采样精度调试和显示异常排查提供参考思路适合初次接触HCS12系列的嵌入式学习者对照实践。1. MC9S12DG128的AD实验从模拟量到数字量你早晚要过的这道坎MC9S12DG128的AD实验是很多人在接触这款16位单片机后的第一个完整调试任务。它的核心目标很直接把电位器转动产生的电压变化、光敏电阻的阻值变化、或者某个传感器输出的模拟信号变成单片机内部能够识别的数字量并能在调试窗口里看到稳定的读数。这个实验解决的实际问题就是“传感器信号如何进入MCU”所以它几乎出现在所有基于这款芯片的课程设计、毕业设计和智能车方案里。适合正在学单片机、准备做传感器采集、或者刚开始调底层驱动的同学。这篇文章不打算泛泛讲原理而是从寄存器配置开始把轮询和中断两套采集代码完整落地再把我实际调板过程中遇到的数据跳变、通道串扰等问题一次性说清楚。2. 看懂ATD0模块MC9S12DG128的AD实验核心寄存器配置MC9S12DG128内部集成的是ATD模块Analog-to-Digital Converter严格来说它有两套ATD0和ATD1。AD实验一般只用ATD0就够了它有8个输入通道支持10位和12位两种分辨率。很多新手看到头文件里一堆ATD0CTL开头的寄存器就蒙了实际上AD实验真正要独占的寄存器不过四五个每个都有明确的分工。把ATD0CTL2到ATD0CTL5这四个控制寄存器配好再管好结果寄存器和状态标志数据就能正常读出来。我一般把配置分成两步先把模块上电、设置转换序列长度、分辨率和采样参数再在运行时切换通道、启动转换并读取结果。下面按寄存器逐个说明最后落到一段完整的初始化代码上。2.1 ATD0CTL2到ATD0CTL5四类控制寄存器怎么配才不出错ATD0CTL2是整个模块的总开关。它的最高位是ADPUAD Power Up写1之后ATD模块才被供电内部参考电路和采样保持电路开始工作。这里有一个容易忽略的时序要求上电之后不能立刻启动第一次转换必须等内部电路稳定。这个时间一般在微秒量级代码里做几次循环延时就够了不用精确计时。同时这个寄存器的bit6可以配置成自动清除标志开启后读结果寄存器时硬件会自动复位转换完成标志省去手动清标志的麻烦这也是它为什么常被写成0xC0的原因——上电和自动清标志一起打开。ATD0CTL3控制的是转换序列长度也就是一次“启动”包含了几个通道的转换。写0x10表示转换序列里只有1个通道。当你想一次把AN0到AN3四个通道全扫一遍时要把低四位改成3表示序列长度为4然后依次把四个通道的转换结果从ATD0DR0一直读到ATD0DR3。这里有个非常典型的坑ATD0CTL3低四位存的是序列长度减一不是通道个数我见过好几个同学在这里写错导致只采到半个数据。然后是ATD0CTL4它管分辨率、采样时间和时钟分频。AD实验最常用的配置就是把分辨率设为12位同时保持一个合适的采样时间。分辨率越高读数的台阶越细12位模式下满量程对应的数字量是4095这比10位模式的1023细腻得多适合用来做传感器的小信号测量。采样时间则要跟信号源内阻匹配如果用分压电阻直接驱动ADC引脚内阻往往有几kΩ采样时间不够的话内部采样电容还没充满电就被打断了读出的值会比实际电压偏低。这种情况下需要把采样时间设长一档具体对应值从数据手册的时序表里查。ATD0CTL5是通道选择与时序模式寄存器。写这个寄存器的时候低四位选当前转换序列要访问的通道号选AN0就写0选AN1就写1以此类推。bit4的SCAN位决定工作模式置1表示连续转换每次转换完成后自动开始下一轮置0则是单次转换启动后只转一轮就停下。AD实验如果做实时监控我会把SCAN置1这样结果寄存器里的数据一直是“新鲜”的。如果只是偶尔测一次电压用单次模式更省资源。要注意一个细节ATD0CTL5一旦写入转换会立刻开始不需要额外的手动触发指令。下面是一段完整的初始化示例代码把上面几个寄存器的配置落在实处void ADC_Init(void) { ATD0CTL2 0xC0; // 位7 ADPU1ATD模块上电 // 等待内部电路稳定约需几个微秒 for (volatile int i 0; i 10; i); ATD0CTL3 0x10; // 转换序列长度1每次只采一个通道 ATD0CTL4 0x01; // 12位分辨率采样时间取默认档位 ATD0CTL5 0x30; // bit41 连续转换模式选中通道 AN0 }代码里先写ATD0CTL2再延时是因为ATD模块上电后需要短暂预热后面三个寄存器之间没有严格的先后顺序但习惯上先定序列长度再定分辨率最后选通道。ATD0CTL5的0x30低四位是0对应AN0高字节里只有bit4位置1表示连续扫描。如果你想换成AN3就把低四位改成3即0x33。这里补充一个遇到过的细节因为ATD0CTL5写入后转换立刻开始初始化完成后结果寄存器里马上会有一轮数据只是这轮数据可能是在模块还没完全稳定时产生的。我一般会在正式读取前做一次“空读”或者等标志位出现一次之后再开始采集。另外ATD0CTL4的值在不同编译环境的头文件里可能被封装成宏实际值以你手头的数据手册为准重点是12位分辨率这个意图要对应到位。做个简单的寄存器速查表方便调试时对照寄存器主要作用AD实验最常用配置备注ATD0CTL2模块上电与中断控制0xC0上电后需延时ATD0CTL3转换序列长度0x10低四位序列长度减一ATD0CTL4分辨率、采样时间、分频0x0112位分辨率ATD0CTL5工作模式与通道选择0x30低四位选通道ATD0STAT0转换状态与完成标志读后判断SCF位置1表示转换完成ATD0DR0H/L结果寄存器读取12位数据注意数据对齐方式2.2 结果寄存器与转换标志位12位数据怎么读才不出错寄存器配好只是第一步真正要从模拟量拿到可用的数字量还要知道数据放在哪里、什么时候能读、怎么拼。ATD模块每完成一次转换结果会存到ATD0DR0到ATD0DR7这组结果寄存器里。如果设置的是12位分辨率一次转换的结果由两个字节组成ATD0DR0H存高位ATD0DR0L存低位。要注意的是这两个寄存器的数据对齐方式受ATD0CTL4里对齐位的影响多数实验配置默认是左对齐的也就是12位有效数据紧挨着寄存器的最高位存放。当你把ATD0DR0H和ATD0DR0L按16位读出来时数值需要右移4位才是真正的12位结果。这一点如果没搞对得到的数字会莫名其妙地比理论值大16倍。那么什么时刻读才是有效的ATD0STAT0寄存器里的SCFSequence Complete Flag标志位会告诉你。SCF置1表示当前转换序列已经完成结果寄存器里的数据是新鲜可用的。轮询模式下代码就是在读结果之前先检查这个标志位。下面是一段轮询读取函数演示了12位数据的拼接和换算unsigned int ADC_ReadChannel(unsigned char ch) { ATD0CTL5 0x30 | (ch 0x07); // 切换通道仍保持连续模式 while ((ATD0STAT0 0x80) 0); // 等待SCF位置1表示转换完成 unsigned int raw (unsigned int)ATD0DR0; raw 4; // 左对齐转右对齐得到12位有效值 return raw; }ATD0STAT0的bit7就是SCF如果配置了自动清标志模式读完ATD0DR0后标志会自动复位不需要手动写1清除。代码里把ATD0CTL5的低四位改成新通道后转换会自动针对新通道进行轮询标志位就能拿到新一批数据。拿到raw之后要换算成真正的模拟电压。12位模式下满量程数字4095对应参考电压VREFH。假设VREFH接的基准是5V那么电压的计算式就是电压值 数字量 / 4095 × 5.0换句话说理论上每一位数字对应约1.22mV。但这里有个隐藏误差单片机内部的参考电压不是理想的5.000V而是由外部VREFH引脚的实际电平决定。如果供电本身有1%的偏差最终换算出来的电压也会带同样的系统误差。这个问题我在第5章的校准部分会专门处理。另外要提醒一个很多人踩过的坑读取结果寄存器时有些头文件把ATD0DR0定义成8位字节有些定义成16位变量。MC9S12DG128默认是大端存储直接按16位读是安全的但如果你把代码模板移植到别的架构上就要重新检查高低字节的读取顺序。到这里寄存器的配置和数据的读取链路就完整了。做AD实验如果卡住十有八九是上面某一个环节没有闭环不是寄存器的值没设对就是读数时刻不对再不然就是高低字节的拼接方式错了。接下来把这套流程跑起来先看轮询方式再看中断方式。3. 跑通MC9S12DG128的AD实验轮询与中断两套采集代码第2章配好了寄存器这一章要做的是把代码真正跑起来看到串口或者调试窗口里有一个会跟着电位器变化的数值。AD实验的代码实现有两条常见路线一是轮询主循环一边等着转换结束一边读数据二是中断转完一轮自动跳进中断服务程序数据更新不占用主流程。这两条路线在MC9S12DG128上都能用但写法差别很大下面分别展开。3.1 轮询方式采集单通道最小可运行代码与流程拆解轮询是最直观的采集方式。流程就三步等转换完成标志读结果寄存器做后处理。我把一个能直接运行的最小工程片段给出来省略了时钟初始化和串口部分只保留AD相关逻辑void ADC_Init(void) { ATD0CTL2 0xC0; // 上电自动清标志 for (volatile int i 0; i 10; i); ATD0CTL3 0x10; // 序列长度1 ATD0CTL4 0x01; // 12位分辨率 ATD0CTL5 0x30; // 连续转换AN0 } unsigned int Read_AD_Value(void) { unsigned int result; while ((ATD0STAT0 0x80) 0); // 等待SCF置1 result ATD0DR0; // 读结果寄存器自动清标志 result 4; // 左对齐转12位有效值 return result; }这段代码在主循环里反复调用Read_AD_Value就行。每次调用时如果上一轮转换还没结束程序会卡在while循环里一旦转换完成读回的数据就是最新的。连续模式的好处体现在这里——不需要每次手动启动转换因为完成一轮后模块自己会继续下一轮结果寄存器里的数据永远在更新。轮询的代价是等待期间CPU空转这在只做单通道采集的AD实验里完全无所谓但如果你同时在跑LED闪烁、按键扫描等任务空转就会挤占其他逻辑。读数据的函数最好加一个防超时机制。轮询等待标志位虽然简单但如果模块因为配置错误根本没有启动转换while循环就会永远卡住。我一般会给等待加一个计数器比如等待超过一定次数就直接返回上一次有效值。这种做法在正式项目里很常见但AD实验的调试期不推荐因为掩盖了真正的问题——你要的是在调试窗口看到卡住的现象才知道配置没对。这里还要注意一个细节SCF标志位的读取和结果寄存器的读取必须是同一轮转换的。也就是先等到SCF置1立刻读ATD0DR0如果中间插入了别的任务导致延时结果寄存器可能已经被下一轮转换覆盖了。在做单纯的AD实验时不会遇到但在嵌进了实时操作系统或者主循环代码较长时就会变成一个隐蔽问题。更稳妥的做法是用标志位配合结果寄存器的锁存机制或者干脆改用中断方式让数据一到就搬走。3.2 中断方式连续采集用中断把转换从主循环里解放出来中断方式比轮询多配置一个环节但对主循环的干扰小得多。它的大致流程是初始化时让ATD模块的中断请求处于允许状态打开全局中断每次转换完成硬件自动拉出一个中断请求跳进中断服务函数在函数里读结果并做后续处理。对MC9S12DG128来说中断使能并不需要额外改ATD0CTL2的某个位因为在初始化值0xC0里bit4INTRQ本来就是0也就是允许中断请求。真正要做的是把中断服务函数挂到编译环境的中断向量表上。初始化代码和轮询版本几乎一样void ADC_Init_With_INT(void) { ATD0CTL2 0xC0; // 上电INTRQ0允许中断请求 for (volatile int i 0; i 10; i); ATD0CTL3 0x10; ATD0CTL4 0x01; ATD0CTL5 0x30; // 连续转换AN0 asm(cli); // 打开全局中断语法按编译环境调整 }中断服务程序里要做的事情很简单读结果、存全局变量。因为在自动清标志的配置下读ATD0DR0这个动作本身就会清除SCF所以不需要额外写标志位volatile unsigned int g_adc_value; void ADC0_ISR(void) { g_adc_value ATD0DR0; g_adc_value 4; // 得到12位数据 }主循环里压根不需要主动去接触AD模块哪个时刻要用最新的电压值直接读g_adc_value这个全局变量就行。中断方式最大的好处是AD转换的时序和主业务逻辑解耦了电机调速、PID计算照常跑电压数据在后台自动更新。代价是全局变量在多处访问时需要注意用volatile修饰否则编译器可能优化掉对它的读写取到的一直是旧值。写中断服务程序时有两个常见的坑。第一个是中断服务函数的名字要和编译环境的中断向量表对上不能自己随便起名。第二个是如果配置了自动清标志那么在ISR里千万不要再去写ATD0STAT0做手动清除多余的一写反而可能把状态位搞乱反过来如果你初始化时把AFFC关掉了那读完数据后就需要手动给SCF写1清除否则中断会反复触发程序看起来像死循环一样卡住。如果你的AD实验需要同时看两三个信号比如电位器电压和光敏电阻电压一起读可以用ATD0CTL3把序列长度扩大再配合ATD0CTL5的多通道扫描。具体做法是ATD0CTL3低四位改成通道数减一ATD0CTL5低四位填入起始通道SCAN位置1中断服务程序里连续读多个结果寄存器。需要注意通道之间的建立时间——如果信号源内阻大切换通道后的第一笔数据往往不准宁可多采一轮再取值。轮询还是中断怎么选我的建议是课程实验用轮询在调试窗口看波形更方便做实际项目或者要接入控制回路用中断把CPU让给真正需要实时响应的逻辑。4. MC9S12DG128的AD实验排查手册采样值跳变、通道配置失效与参考电压干扰AD实验看起来简单真正把板子焊好、程序烧进去之后各种玄学问题就来了。这里把我在调试过程中遇到过、也帮别人排过的几个高频问题整理成一份排查手册每条都按“现象 → 原因 → 解决”的思路写希望能帮你少走几个弯路。4.1 采样值大幅度跳变但外部电路用万用表量着很稳定通道悬空与共模噪声现象程序下载进去后串口打印的AD值在几百到三千多之间乱跳但用万用表测量ADC引脚的电压明明很稳定波动不超过10mV。原因MC9S12DG128的ADC输入引脚是高阻输入如果信号源没有和板子共地或者信号源输出阻抗太高外界电磁干扰会直接耦合进采样电容。更常见的情况是某一个通道的引脚完全悬空——初始化里选的是AN0但AN0引脚没有接任何东西悬空的引脚就是一个天线采集到的数值自然到处飘。解决先检查通道对应引脚是否确实连接到了信号源并且信号源的参考地要和单片机的地是同一个。用万用表量的时候表笔本身会给引脚提供一个低阻路径量出来稳定不代表悬空状态下也稳定。临时验证的办法是用一根导线把该引脚接到VREFH或者地再去读数值如果读数稳定在4095或0附近基本可以确定问题出在输入信号源上。更彻底的方案是给信号源串联一个几百欧到1kΩ的电阻并在ADC引脚对地并联一个0.1μF的电容构成一个低通滤波能明显改善受干扰的情况。4.2 采样结果始终是4095或者始终是0但电位器转动时电压明明在变化通道没选对还是模式没配对现象把电位器接在AN0上转电位器打印出来的数值纹丝不动地停留在4095或者0。有时候换一个通道接数值又正常了。原因这种问题要先区分是数值“锁死”还是“通道选错”。数值锁死在4095通常是ADC引脚被接到了VREFH附近或者测量量程搞错了锁死在0则可能是引脚被拉到地或者始终没有正确触发转换。更隐蔽的是通道选择失败ATD0CTL5的低四位确实写了通道号但ATD0CTL3里的序列长度配置和通道数量不匹配。比如序列长度设成了4但实际只采1个通道程序读到的ATD0DR1以后的数据全是残留值。解决先打印当前ATD0CTL5和ATD0STAT0的原始值确认通道号和连续模式是否真的写进去了。然后在轮询模式下单步执行第一次运行到while循环时检查SCF位有没有在置1后又被清零。如果SCF从来没有置1过说明ATD模块根本没有完成转换回头查ATD0CTL2的ADPU位是不是被后续代码无意间改掉。另外一个笨但有效的办法把代码里ATD0CTL5低四位强制改成和实际接线一致的通道比如接AN3就写0x33再重新编译排除是调试器看到的寄存器映射位置和物理引脚不一致的问题。4.3 多通道扫描时数据串位FIFO模式与结果寄存器映射的一笔糊涂账现象用ATD0CTL3把序列长度设成4程序里依次读ATD0DR0到ATD0DR3。结果发现AN0的电压变化会影响到ATD0DR1里的数据或者四个寄存器里的数据像是同一路输入被延迟了四次。原因MC9S12DG128的ATD模块支持FIFO模式也支持普通模式。在普通模式下结果寄存器ATD0DRn和转换序列的次序是对应的一旦开了FIFO模式数据会被按完成顺序堆进寄存器这时的映射关系就跟通道号没有直接关系了。同时如果转换序列里包含多个通道但ATD0CTL5配置的是固定单通道模式模块会一直采集同一个通道那么其他结果寄存器里保留的是上一次的旧数据读出来自然像串位。解决先确认ATD0CTL3里FIFO位是清掉的普通模式读出的数据顺序最直观。多通道扫描的配置要同时满足三个条件ATD0CTL3低四位数值等于通道数减一ATD0CTL5低四位写入起始通道把ATD0CTL5的SCAN置1让它持续扫描整个序列。改完这三个点再分别给每个通道输入不同电压看读出的四组数据是否各自跟随对应输入。如果还串检查一下头文件里ATD0DRn的定义是否真的对应0到7号结果寄存器有些编译环境的库文件里寄存器地址偏移可能有两字节误差。4.4 数据整体偏高或偏低且随供电电压变化VREFH参考电压没接稳现象同样的电位器分压用万用表量出来是2.50V程序里按公式算出来却是2.58V或者2.43V而且这个偏差会随着板子供电电压波动而改变。原因AD转换的满量程取决于VREFH引脚的电压而不是核心电源的电压。如果VREFH引脚悬空或者接到了核心电源的某个滤波电容前面那么VREFH的电平就会随负载波动换算公式里的参考基准就不是稳定的5.000V。这种现象在实验板上特别容易出现因为很多最小系统板的模拟参考和数字电源是直接连在一起的电源纹波一个跳动AD值就跟着整体漂。解决VREFH必须单独接到稳定的参考电压源上。简单做法是把VREFH和VREFL分别接到模拟电源和模拟地并且靠近引脚放置0.1μF去耦电容。对AD实验来说最实用的校准办法是测一组已知电压软件做两点修正。比如先给ADC引脚输入精确的0V和一个接近满量程的已知电压记录对应的数字量然后建立线性修正公式。这个做法不只是修补参考电压误差还能一并消除部分内部偏移具体公式在第5章给出。4.5 单步正常、全速异常标志位竞争与读取时机现象用调试器单步执行每一步都停在正确的地方AD值读数正常按下全速运行串口输出的数据错乱或者有一半是旧值。原因这种问题通常是读取时机和转换完成时机发生了竞争。单步执行时每行代码之间都有充足的时间让转换完成全速运行时while循环看起来在等待SCF但实际上可能在SCF置1的前一刻读到了旧数据并退出循环下一次循环时数据已经被新转换覆盖。尤其是连续模式加轮询的组合SCF每次转换完成都会重新置位读代码和转换硬件之间的时间关系非常微妙。解决优先把轮询改成读结果寄存器并等待“新转换完成”的判定方式。一种可靠的口诀是“先等标志再读数据读完立刻处理”。如果有条件直接换中断方式让SCF置1的时刻和读取动作之间没有任何其他代码插入的可能性。还有一种中间做法是在主循环里每次读取前先清掉SCF前提是手动清标志模式等它再次置1再去读取这样可以保证读到的是更新后的数据而不是上一轮的旧值。5. 把AD实验做成稳定可用的测量滑动平均滤波与两点校准到这里数据已经能从MC9S12DG128里读出来了。但实验收尾之前还有两件事值得做一是让数值不抖二是让数值准。前者靠滤波后者靠校准。5.1 滑动平均滤波压住波动又不牺牲响应速度直接读回来的数字量在最后几位上总会有一点跳动这是量化噪声和电源纹波共同作用的结果。最简单的处理办法是多采几次求平均但平均次数太多会让响应变得迟钝。我常用的折中方案是滑动平均维护一个长度为N的环形缓冲区每次读到一个新值就替换掉最旧的值输出缓冲区里N个数的平均。N取8或者16在大多数传感器采集场景下都有不错的效果。#define FILTER_LEN 8 unsigned int filter_buf[FILTER_LEN]; unsigned char filter_index 0; unsigned int ADC_Filter(void) { unsigned int sum 0; unsigned char i; filter_buf[filter_index] Read_AD_Value(); filter_index (filter_index 1) % FILTER_LEN; for (i 0; i FILTER_LEN; i) { sum filter_buf[i]; } return sum / FILTER_LEN; }滑动平均的好处是每次输出只需要重新算一遍N个数的和复杂度固定缺点是窗口长度N是全局的想同时滤掉50Hz工频又保持足够快的响应N要按采样率来推算。如果采样率是每秒采集100个点要滤掉50Hz的周期波动窗口至少应覆盖一个完整工频周期。做AD实验的话这个值不需要太纠结先取8看波形毛刺明显就往上加加到16之后基本就平滑了。5.2 两点校准修掉VREFH误差和内部偏移滤波解决的是“稳”校准解决的是“准”。前面提过理想情况下数字量4095对应VREFH电压但VREFH实际不一定是整5V芯片内部比较器也有一点偏移。这些误差在实验环境里可以通过两点校准轻松修掉。做法是先给ADC输入一个低参考电压记录数字量D_low再输入一个高参考电压记录数字量D_high。然后建立线性映射电压(V) V_low (raw - D_low) × (V_high - V_low) / (D_high - D_low)把这两组值记录到代码里测量函数用这个公式换算实际电压。MC9S12DG128的ATD模块线性度其实不错用两点校准就可以把常温下的整体误差压到1%以内。如果后续换了一批板子只要重新标定这两组值即可。我在做AD实验时养成一个习惯每次焊好一块新板子先不做功能开发而是先把AD自检跑一遍给每个通道输入已知电压并校对数值。这样后面所有依赖ADC的模块都不用怀疑采集链路。这个习惯帮我省掉了大量从头查硬件问题的debug时间希望也能帮到你。本文还有配套的精品资源点击获取