S12芯片ATD模拟量采集与12864显示实践全解析
简介一份基于飞思卡尔 MC9S12DG128 的 AD 采集与显示实验资源适合嵌入式初学者、单片机课程设计或 S12 系列开发者参考。资源演示了如何利用芯片内置的多通道 8 位高速 AD 转换器进行数据采集并通过 12864 液晶屏实时显示结果覆盖 AD 寄存器配置、转换结果读取和 LCD 驱动等关键环节。RAR 压缩包共 79 个文件容量约 432KB主要包含 CodeWarrior 工程源码、C 文件、链接参数 prm、调试命令 cmd、编译生成的 map/s19/abs 文件以及多个调试器配置文件结构完整可直接导入工程查看和编译已有 131 人学习下载。借助包内的 TBDML、PE Multilink 和全芯片仿真等调试配置可快速理解不同调试环境下的工程设置同时源码中的初始化与显示逻辑也为后续扩展多通道采集、电压显示等应用提供了基础。对于正在做飞思卡尔课程设计或入门单片机外设开发的读者这份压缩包提供了从工程配置到代码实现的完整闭环参考。1. 从摸不清的AD实验说起一块S12芯片、一路模拟量、一块12864屏如果你刚拿到 MC9S12DG128 的 AD 实验资料包第一反应大概率是原理图有了代码也有了但一上电12864 液晶上显示的数字要么纹丝不动要么跳得像心电图。这个实验的核心其实只有三件事——S12 内部的 ATD 模块怎么配置、模拟量怎么转换成数字量、结果怎么在 12864 上显示出来。任何一个环节的寄存器位配错出来的数据都是废的。这套资源适合两类人一类是正在做课程设计、需要快速跑通流程的学生另一类是准备竞赛、想拿 S12 练手但不想从零翻数据手册的开发者。下面按我实际拆过的流程把原理、代码、接线和坑一次说清。2. 先看透 S12 的 ADC 模块ATD 寄存器与硬件选型的底层逻辑2.1 ATD0 与 ATD1两个 10 位 ADC 模块的硬件分配MC9S12DG128 内部集成了两个独立的 ADC 模块命名分别是 ATD0 和 ATD1。每个模块都是 10 位分辨率但它们在引脚分配上完全独立——ATD0 对应 PAD0-PAD7 这 8 个通道ATD1 对应 PAD8-PAD15 另外 8 个通道。不少人第一次用的时候只盯着 ATD0 看结果发现引脚不够用其实 ATD1 就是为多通道采集准备的。我一般会把 ATD0 用在主采集通道上ATD1 留给辅助监测。这样做不是性能有差异而是代码逻辑好分——主流程读 ATD0中断或后台任务读 ATD1两个模块的转换序列互不干扰。实际项目中如果只需要一路模拟量用 ATD0 单通道就够了ATD1 可以完全不初始化省下的功耗和配置时间都在明面上。还有个常见的硬件分配问题参考电压引脚。ATD0 的 VRH 和 VRL 引脚是独立的硬件设计上必须接稳定的基准源不能直接悬空。我见过不少板子为了省事把 VRH 接 VCC、VRL 接 GND这在电源纹波小的实验板上问题不大但如果电源是从 USB 取电纹波会影响低几位数据的稳定性。2.2 从 ATD0CTL2 到 ATD0CTL5四个核心寄存器的配置逻辑ATD 模块的配置看似分散在十多个寄存器里但真正决定采集行为的只有四个ATD0CTL2、ATD0CTL3、ATD0CTL4、ATD0CTL5。这四个寄存器各管一段配完之后基本不用再动。寄存器关键位作用典型值ATD0CTL2ADPU、AFFC模块上电、快速清零0xE0 或 0xC0ATD0CTL3S8C-S1C、FIFO转换序列长度、FIFO 模式0x10单通道或 0x08ATD0CTL4SRES8、SMP、PRS分辨率、采样时间、分频系数0x60 或 0x40ATD0CTL5MULT、SCAN、CH多通道、连续扫描、通道选择0x30 或 0x00ATD0CTL2 里的 ADPU 位是模块总开关置 1 后 ADC 才开始工作这一位漏掉是代码跑不通最常见的原因。AFFC 置 1 后转换结束标志自动清零省去手动清标志的麻烦。ATD0CTL3 里的 S8C-S1C 位决定一次转换序列里包含几个通道单通道就设成 0x10四通道就设成 0x04。ATD0CTL4 里的 SRES8 是分辨率选择位——置 1 是 8 位清零是 10 位。很多人误以为默认就是 10 位实际复位值是 8 位模式。PRS 位是总线时钟分频系数S12 外部晶振一般是 16MHz内部总线时钟 8MHzPRS 配 0x07 左右才能让 ATD 时钟落在 2MHz 附近的合理区间。ATD0CTL5 里的 MULT 和 SCAN 是连续采集的关键MULT 置 1 才能按序列扫描多通道SCAN 置 1 是循环转换单次触发则清零。2.3 参考电压与采样时间精度问题的两个根源AD 转换的原理是内部的电容阵列逐次逼近转换结果的绝对精度直接取决于参考电压 VRH/VRL 的稳定性。实验板上常用 5V 供电VRH 接 5V、VRL 接 GND理论上 10 位分辨率就是 5V/1024 ≈ 4.88mV 每 LSB。但如果电源是开关稳压输出纹波 50mV 的话低两位基本是噪声读出来的数据跳 3-5 个 LSB 属于正常现象。采样时间这个参数经常被忽略。ATD0CTL4 里的 SMP 位控制采样时间默认值是 2 个 ATD 时钟周期。对于低阻抗信号源——比如电位器直接输出——这个值够用。但如果信号源是高阻抗输出比如运放输出电压经过一个大电阻限流采样电容充电时间不够转换结果就会偏低而且通道间还会互相串扰。我一般把 SMP 位设成 01 或 10让采样时间拉长到 4-6 个时钟周期。效果不会在实验室里立刻体现但在现场电磁环境差的地方差别很大。提示ATD 时钟频率不要超过 8.4MHz这是数据手册里给的硬件上限。分频系数算好之后再留 20% 余量否则转换结果会飘得离谱。3. 写一个能跑通的初始化与采集代码寄存器配置到数据读取的全过程3.1 初始化代码ATD0 模块的时钟、精度与触发方式打开 CodeWarrior 新建工程后先别急着写业务逻辑第一步是确认 MCU 的总线时钟是多少。S12 的 ATD 分频需要以总线时钟为基准外部晶振 16MHz、总线时钟 8MHz 是典型配置。初始化代码写成一个独立的函数方便后续单片复用。void ATD0_Init(void) { ATD0CTL2 0xE0; // ADPU1 模块上电AFFC1 快速清零禁止外部触发 ATD0CTL3 0x10; // 转换序列长度为1FIFO模式关闭 ATD0CTL4 0x60; // SRES81 8位模式SMP00 最短采样时间PRS7 分频 ATD0CTL5 0x30; // MULT0 单通道SCAN1 连续扫描选择通道0 }这段配置里 ATD0CTL5 写成 0x30 有个讲究bit5 是 SCAN、bit4 是 MULTSCAN 置 1 后模块自动循环转换读结果寄存器时永远拿到的是最新值。MULT 置 0 表示只转换单个通道通道号由低四位决定0x30 的低四位是 0对应 PAD0 引脚。ATD0CTL2 的 0xE0 展开后是 ADPU、AFFC、AWAI 三个位为 1AWAI 置 1 的作用是 MCU 进入 wait 模式时 ATD 也停止省电。这个位在实际调试时可以清零否则单步调试时模块不工作数据读出来是 0。注意 ATD0CTL3 的值是 0x10 而不是 0x01——S8C-S1C 这几位在寄存器里的排列顺序是从高位到低位0x10 二进制是 00010000对应 S8C0、S4C0、S2C0、S1C1即序列长度为 1。很多人按直觉写成 0x01结果转换序列长度变成 2读出来的数据错位。3.2 单次转换与连续转换的代码逻辑对比初始化为连续扫描模式后主循环里只需要不断从结果寄存器取数。结果寄存器 ATD0DR0-ATD0DR7 对应序列中第 1 到第 8 次转换的结果。单通道模式下结果存在 ATD0DR0 里。这里有个细节AFFC 置 1 后读结果寄存器会自动清除转换完成标志 CC0不需要额外写代码清标志。unsigned int Read_AD_Channel(unsigned char ch) { unsigned int result; ATD0CTL5 0x80 | (ch 0x07); // bit70 单通道模式低三位为通道号 while(!(ATD0STAT0 0x01)); // 等待CC0标志置1表示转换完成 result ATD0DR0; // 读取转换结果读后自动清标志 return result; }这个函数支持动态切换通道。调用时传 0-7 的通道号函数把它拼进 ATD0CTL5 的低三位下一次转换就发生在指定引脚上。while 循环等待标志位是同步阻塞方式在裸机程序里最直观。ATD0STAT0 的 bit0 是 CC0序列中第 1 个通道转换完成就置 1。10 位模式下结果寄存器的高 8 位在 ATD0DR0H、低 2 位在 ATD0DR0L 的高两位直接读 ATD0DR0 寄存器时编译器会自动拼成 16 位整数。8 位模式下结果只占 ATD0DR0H。3.3 软件滤波去掉毛刺的两种写法硬件上的滤波电容只能滤高频噪声对于 50Hz 工频干扰无能为力。软件滤波的做法是连续采样多次后取平均或去极值。我的习惯是取 8 次采样去掉最大值和最小值剩下 6 个求平均。这样既能滤掉随机毛刺又不会因为某个偶然的尖峰把平均值拉偏。unsigned int AD_Filter(unsigned char ch) { unsigned int buf[8]; unsigned int sum 0, max 0, min 0xFFFF; unsigned char i; for(i 0; i 8; i) { buf[i] Read_AD_Channel(ch); } for(i 0; i 8; i) { if(buf[i] max) max buf[i]; if(buf[i] min) min buf[i]; } for(i 0; i 8; i) { if(buf[i] ! max buf[i] ! min) { sum buf[i]; } } return sum / 6; }这个滤波函数有个边界问题如果最大值和最小值各出现两次if 判断会把两个重复值都剔除导致除数的实际个数少于 6。我实测中的解决方法是先统计剔除数量再动态调整除数。但实验室环境下概率很低真碰上再改不迟。注意连续转换模式下while 循环等待标志位可能永远等不到——如果上一次的转换完成标志已经被硬件自动清零且 SCAN 模式下的转换周期极短标志位置 1 到清零之间的窗口很窄。建议在等待前先读取一次 ATD0STAT0 丢弃残留标志或者把 SCAN 位清零后再重新触发。4. 把 AD 值送上 12864 液晶SPI 驱动与显示流程的完整组装4.1 12864 的 SPI 模式接线与初始化时序12864 液晶常见的控制器是 ST7920带中文字库支持并口和 SPI 两种接口。S12 的端口资源虽然不算紧张但用 SPI 模式只需要 3 根线——CS、SID串行数据、SCLK比并口的 8 根数据线省太多。接线方案我一般固定为PAD0 接电位器输出PTH0 接 CS、PTH1 接 SID、PTH2 接 SCLK。12864 的 PSB 引脚在 SPI 模式下必须拉高这是进入串行模式的前提。很多人接线时漏了这一步液晶上电后只有背光亮、屏幕全黑或者全是稀疏的条纹——就是 PSB 没处理对。初始化序列比常见的并口版本多一个步骤因为 SPI 模式下控制器要求先发送同步位。void LCD12864_Init(void) { // 拉高PSB进入串行模式 PSB 1; delay_ms(50); // 等待液晶内部复位 LCD_WriteCmd(0x30); // 基本指令集8位模式 delay_ms(5); LCD_WriteCmd(0x0C); // 显示开光标关 delay_ms(5); LCD_WriteCmd(0x01); // 清屏 delay_ms(10); LCD_WriteCmd(0x06); // 光标右移整屏不移动 }LCD_WriteCmd 函数内部处理 SPI 时序。ST7920 的串行模式要求每个字节前面加 5 个连续的 1 作为同步位然后是 1 位 RW写指令是 0、写数据是 1、1 位 RS指令是 0、数据是 1之后才是 8 位数据。同步位的作用是让控制器识别串行数据的起始位置——如果连续发送多个字节同步位就是帧头。4.2 显示函数清屏、光标定位、数字字符串的拼接显示 AD 值的过程是把十六进制或十进制的转换结果转成 ASCII 字符串然后按位写入液晶的显示 RAM。ST7920 内部的 DDRAM 地址是按行列排布的行为 0-3、列为 0-19但 12864 实际可见的是每行 16 个汉字或 32 个 ASCII 字符。写字符之前要先把光标定位到目标行列。void LCD_WriteString(unsigned char row, unsigned char col, char *str) { unsigned char addr; switch(row) { case 0: addr 0x80 col; break; case 1: addr 0x90 col; break; case 2: addr 0x88 col; break; case 3: addr 0x98 col; break; } LCD_WriteCmd(addr); // 设置DDRAM地址 while(*str) { LCD_WriteData(*str); // 逐个写入ASCII字符 } }第二行的地址是 0x90、第二页的地址是 0x88——这不是笔误。ST7920 的 DDRAM 地址排列是第一行 0x80-0x8F、第二行 0x90-0x9F、第三行 0x88-0x97、第四行 0x98-0xA7。中间行地址跳变是因为硬件设计上每个地址对应 16 位宽度第三行地址从 0x88 开始而不是 0xA0。这个地址映射是 12864 显示错乱的头号原因。4.3 数据映射10 位数字怎么变成「1.23V」这样的字符串AD 转换结果是 0-1023 的整数要显示成电压值需要换算。5V 参考下每个 LSB 对应约 4.88mV整数除法会丢掉小数精度得用定点小数技巧。我习惯先求整数部分、再求小数部分拼成字符串显示。void Display_Voltage(unsigned int ad_value) { unsigned int mv (ad_value * 5000UL) / 1023; // 换算成毫伏 char buf[16]; unsigned char i 0; // 拼接数字格式如 1.23V buf[i] (mv / 1000) 0; // 整数位 buf[i] .; buf[i] ((mv % 1000) / 100) 0; // 十分位 buf[i] ((mv % 100) / 10) 0; // 百分位 buf[i] ((mv % 10)) 0; // 千分位 buf[i] V; buf[i] \0; LCD_WriteString(1, 0, buf); }ad_value 是 10 位采样值乘以 5000 是为了避免中间结果出现浮点数。注意这里的 1023 是 10 位模式下的满量程值如果改用了 8 位模式必须替换成 255。有个容易忽略的点乘法结果是 unsigned long 类型务必加 UL 后缀否则 16 位整形的溢出会让满量程显示变成乱码。这套做法在工程上就是「定点小数」没有浮点开销在 8 位机上跑得飞快。5. 避坑与排查AD 实验里最容易翻车的五个瞬间5.1 现象一烧录器握手失败CodeWarrior 报连接超时开发板接上烧录器后 CodeWarrior 提示无法与目标板通信检查了供电和接线都正常。原因在 S12 的复位引脚上——很多开发板在复位引脚上接了较大的下拉电容而烧录器握手时需要捕捉复位脉冲电容把脉冲边沿拉平导致握手失败。解决方法是把复位引脚上并联的电容从 100nF 换成 10nF或者先断开电容、烧录完成后再接回去。如果是自制的最小系统板晶振起振时间也会影响握手——上电后等 1 秒再点烧录按钮往往就通了。5.2 现象二AD 采集值固定在 0x3FF不随电位器变化读出来的数值一直是 1023旋转电位器没反应。排查后确认是 ATD0CTL5 的通道选择没生效——我在初始化时写成 0x30 后又在主循环里调用了 Read_AD_Channel(2) 切换通道但 ATD0CTL5 的最高位是 0 表示单通道模式下低三位是通道号而 0x30 的低三位是 0后续切换时应该用 0x80 做基地址而不是保留原来的 0x30。解决方法是把通道选择函数改成ATD0CTL5 0x80 | (ch 0x07)确保每次操作只设置通道号而不影响 SCAN 位。这个坑的根源是寄存器读改写操作没有用掩码保护。5.3 现象三12864 显示乱码字符位置错乱写入的字符串总能显示出来但位置不在期望的行列上。观察后发现一个规律——第一、三行显示正常第二、四行的内容跑到了第一行中间。原因是 ST7920 的 DDRAM 地址映射里第二行从 0x90 开始但很多网上流传的驱动库里写的是 0x91差了一个字符位。排查时用最简单的LCD_WriteCmd(0x02)让光标归位再逐行写入固定字符就能定位到底是驱动库地址表错了还是行列换算错了。我的经验是遇到显示错位不要猜直接把四条行的地址都打印到屏幕上。5.4 现象四AD 值跳变低两位数据像随机数采集的数据在某个值附近上下跳 5-10 个 LSB电位器拧到中间时尤其明显。用示波器测 VRH 引脚的纹波发现有 80mV 的噪声尖峰。原因有两个一是主控板的地线回路不合理电位器的 GND 与 ATD 模块的 VRL 之间隔了一段较长的走线二是电位器的滑片接触电阻本身就有微伏级的噪声。解决方法是把电位器两端直接并联一个 100nF 的陶瓷电容软件侧开启 8 次平均滤波。硬件加软件双管齐下后数据跳动控制在 2 个 LSB 以内。5.5 现象五连续转换模式下程序卡死在等待标志位的循环里将 SCAN 置 1 后ATD0STAT0 的 CC0 标志位在读取结果后自动清零但程序仍然卡在 while(!(ATD0STAT0 0x01)) 里。原因是在 10 位模式下结果寄存器的高 8 位和低 2 位分处两个不同的地址使用ATD0DR0直接读取时编译器生成的读指令顺序是先读低字节再读高字节而快速清零机制在低字节被读时就已经清了标志位。解决方法是强制将 AFFC 位清零改为手动写 1 清除标志——虽然多一行代码但时序更可控。提示S12 的 ADC 结果寄存器在 8 位模式下可以直接读 8 位值但 10 位模式下务必使用 16 位类型的读取方式。用 8 位变量去接计算结果换算出的电压永远差 4 倍这是新手容易踩而不自知的坑。6. 进阶多通道循环采样与噪声的专项抑制做完单通道的电位器实验下一步就是四通道或八通道扩展。ATD0CTL3 里 S8C-S1C 配置成 4 通道序列长度ATD0CTL5 的 MULT 位置 1转换结果依次存入 ATD0DR0-ATD0DR3。需要留意的是ATD0STAT0 里的 CC0-CC3 标志位分别对应该序列中第几个通道完成。如果开了 FIFO 模式结果寄存器的映射关系还会变化需要在代码里做通道-数据的配对表。噪声抑制上我最常用的是「中位值平均滤波」的变体——每通道连续采样 16 次排序后取中间 10 个的平均。这套算法在通道数增加后执行时间会线性增长所以只在前台主循环里跑不在中断里做。实际项目中遇到通道间串扰的话优先检查采样时间是不是太短而不是一味加滤波阶数。从那以后我每次拿到陌生的 S12 开发板都会强制自己先跑一遍「单通道电位器采集 12864 显示」的裸板程序——寄存器配置、通道切换、数据显示全链路跑通后再往工程里塞业务逻辑。这套流程帮我快速区分硬件问题、驱动问题和配置问题省掉大量排错时间。希望这篇拆解能让你少走几步弯路直接把代码烧进板子里看到跳动的数字。本文还有配套的精品资源点击获取