资讯动态

CS1237称重ADC与Modbus RTU从站协议在N76E003上的固件实现与调试指南

发布时间:2026/10/16 15:11:50 来源:尧图企业网站定制
简介这份资源是一套面向工业自动化、物联网与嵌入式开发者的完整工程源码包聚焦CS1237高精度ADC数据采集、Modbus串行通信协议以及新唐N76E003μPD78F0034单片机程序设计适合具备一定单片机基础、需要快速移植或参考驱动与寄存器通信实现的读者。压缩包共28个文件约152KB主要包括C源码、头文件、Keil工程文件uvproj/uvopt、启动文件、汇编与链接文件以及编译生成的hex、lst、obj等输出文件能直接辅助查看工程配置、二次编译与烧录调试。代码中涉及128倍增益设置、高低位寄存器地址0x0000与0x0001的数据拆分/组合等关键细节便于理解CS1237驱动与Modbus寄存器的交互流程。目前已有669人学习下载对于正在做新唐003平台数据采集或工业设备通信开发的工程师而言是一份紧凑且具有直接参考价值的实战资料。1. 拆解 CS1237 Modbus RTU 新唐003 固件包一次搞懂称重仪表的数字链路做电子秤、料斗秤或者工控称重模块的人多半经历过这种尴尬传感器和高精度 ADC 都选好了CS1237 的 24 位数据也读出来了结果卡在「跟 PLC 或触摸屏通讯」这一关——协议不会写、帧格式对不上、寄存器地址没规划连夜加班搓 Modbus 从站代码。这份「cs1237modbus通讯新唐003程序.zip」解决的正是一整条链路用一颗新唐 N76E003 八位单片机同时驱动 CS1237 采集称重数据再通过串口走 Modbus RTU 协议把重量交给上位机。对电子秤二次开发、旧设备加通讯接口、低成本称重仪表改造这三类场景它的参考价值很高尤其是固件里寄存器划分和时序处理几乎可以直接搬到自己工程里。下面按我拆包的顺序把驱动、协议、联调、坑位逐个过一遍。2. 固件包结构与 CS1237 驱动原理先看懂压缩包里躺着的四类文件2.1 解压后先看什么源码、工程、烧录文件与说明文档的用途边界拿到压缩包别急着双击打开工程文件先把目录结构捋一遍。这种资源包通常不是单文件而是按「源码 工程 烧录 文档」四类组织我这次拆到的基本结构如下cs1237-modbus-n76e003/ ├── Source/ # 核心源代码目录 │ ├── main.c # 主循环与定时器调度 │ ├── adc_cs1237.c # CS1237 时序驱动 │ ├── modbus_slave.c # Modbus RTU 从站协议栈 │ └── uart.c # 串口初始化与收发缓冲 ├── Project/ # Keil C51 工程文件 │ └── cs1237_scale.uvproj ├── Output/ # 编译输出 │ ├── cs1237_scale.hex # 烧录文件给烧录器用 │ └── cs1237_scale.uvguix └── Doc/ └── register_map.pdf # 寄存器地址映射说明Output目录里的.hex是已经编译好的固件如果没有改动需求可以直接烧录验证硬件连线Source里的 C 文件则是拿来改功能用的。我的习惯是先打开req.txt或文档里的寄存器映射表确认固件把哪些地址分配给了重量、哪些分配给了 AD 原始值再决定要不要改代码——因为很多人在这一步会犯一个错直接拿烧录文件去刷自己的板子然后发现数据根本不对还以为固件是坏的。这个资源的适用前提是硬件上必须有 CS1237 和 N76E003 的引脚连接定义。如果压缩包内文档写的引脚和你的板子不一致第一件事是改adc_cs1237.c里的引脚宏定义而不要硬刷。2.2 CS1237 时序驱动DOUT 握手、24 位数据读出与增益再配置CS1237 的通讯接口长得像 SPI但又不完全是 SPI它只有 DOUT 和 SCLK 两根线没有片选。其输出时序的核心要点是DOUT 拉低表示一次转换完成此时主机开始在 SCLK 上产生脉冲每个上升沿从 DOUT 读出一位数据。数据是 24 位二进制补码先出最高位第 25、26 个时钟用于配置 PGA 增益和输出速率第 27 个左右用于通道配置。源码里的读取函数逻辑通常长这样// adc_cs1237.c // 从 CS1237 读取一次 24 位转换结果同时维持原增益配置 uint32_t CS1237_Read_Data(void) { uint32_t raw 0; int i; // 等待 DOUT 拉低表示转换完成 // 超时保护如果 SCLK 长时间无动作CS1237 会自动复位 uint16_t timeout 10000; while (CS1237_DOUT_READ() 1) { if (--timeout 0) { CS1237_Reset(); return 0xFFFFFF; // 返回异常值主程序要丢弃这帧数据 } } // 读出 24 位数据MSB first每个 SCLK 上升沿读一位 for (i 0; i 24; i) { CS1237_SCLK_HIGH(); raw 1; if (CS1237_DOUT_READ()) raw | 0x01; CS1237_SCLK_LOW(); } // 第 25、26 个 SCLK 用于配置增益和速率 // 这里维持默认配置增益 128、10SPS CS1237_Set_Gain(CS1237_GAIN_128, CS1237_SPEED_10HZ); return raw; }代码逻辑不复杂但有三处值得展开。第一等待 DOUT 拉低前面必须加超时因为 CS1237 如果 CS 脚或电源时序异常DOUT 可能永远置高没有超时保护整个主循环会被卡死在这里。第二24 位数据读完之后下一轮转换周期内 CS1237 的增益和速率保持不变但前提是你在第 25、26 个时钟配置了它期望的值如果不主动配置某些批次的 CS1237 会恢复默认设计值导致你前面标定的增益系数全部作废。第三返回值0xFFFFFF是异常哨兵值主程序里不能直接用这个值参与平均和滤波必须丢弃。2.3 增益与速率配置用 SCLK 脉冲个数切换 PGA 的真实目的是压低噪声CS1237 内置可编程增益放大器增益档位通常有 1、2、64、128 等输出速率常见为 10SPS 与 40SPS 档位。源码里一般会封装一个配置函数其本质是在数据读取完成后额外发送对应数量的 SCLK 并让 DOUT 保持对应电平。配置代码大致如下// 参数说明 // gain: 1、2、64、128对应 CS1237 内部 PGA 档位 // speed: 10 或 40对应内部振荡器分频后的输出速率 uint8_t CS1237_Set_Gain(uint8_t gain, uint8_t speed) { // 第 25 个 SCLK选择增益高/低位之一 // 第 26 个 SCLK选择速率 // 具体组合需参考 CS1237 数据手册时序表 CS1237_SCLK_HIGH(); if (gain 0x02) { CS1237_DOUT_SET_INPUT(); } else { CS1237_DOUT_SET_INPUT(); } CS1237_SCLK_LOW(); CS1237_SCLK_HIGH(); if (speed 40) { /* 高速档 */ } else { /* 低速档 */ } CS1237_SCLK_LOW(); return 0; }实际调试时我更喜欢把增益固定到 128然后根据传感器的灵敏度去处理数值而不是频繁切换增益。原因是 CS1237 在低增益档下输入等效噪声会明显变大高增益档配合桥式传感器信噪比反而更好。速率方面10SPS 适合静态称重40SPS 用于动态检测但噪声会变大。这个选型逻辑在称重应用里很关键——分辨率高不等于稳定稳定要靠速率和滤波配合。3. Modbus RTU 从站协议实现帧解析、寄存器映射与 CRC16 细节3.1 从站协议栈的三个边界帧头等待、超时判定、一帧只应答一次Modbus RTU 主从通讯本身不复杂难点在帧边界判定。RTU 模式下帧与帧之间的间隔要求大于 3.5 个字符时间N76E003 跑 16MHz 晶振、9600 波特率时一个字符约 1ms3.5 字符就是 3.5ms。源码里通常用定时器或串口空闲中断来判定一帧是否结束。这里最容易出问题的是「半帧」处理主站发过来的请求可能被拆成两次串口中断如果收到第一段就立刻解析并回复协议栈会收到一个不完整的帧CRC 校验必失败然后主站就认为从站无响应。固件里的标准做法是用一个接收缓冲区 接收超时计时器// Modbus 接收状态机按字节积累超时后判定帧结束 void UART_ISR(void) { uint8_t byte SBUF; if (rcv_len 0) { rcv_buf[rcv_len] byte; frame_timer 0; // 启动帧超时计时 } else if (rcv_len MODBUS_RX_BUFFER_SIZE) { rcv_buf[rcv_len] byte; frame_timer 0; } } void MODBUS_Poll(void) { // 主循环中检查帧间隔 if (rcv_len 0 frame_timer 4) { // 4ms 未再接收到新字节认为一帧结束 MODBUS_Parse_Frame(rcv_buf, rcv_len); rcv_len 0; } }这段代码里我把帧结束阈值定为 4ms略大于 3.5 字符时间留了余量。实际测试时需要注意不同的上位机组态软件在发送时可能本身就有帧间隔不稳定现象阈值太紧容易丢帧太松又会把两帧合成一帧解析。波特率 9600 时 4ms 合理如果改成 19200阈值要同步缩小到 2ms 左右否则通讯速度会被拖慢。3.2 寄存器映射设计重量、AD 原始值、状态码与写参数区的规划源码包最有价值的模块是寄存器映射表它直接决定了上位机怎么读数据、怎么下发控制命令。我拆到默认映射如下寄存器地址功能码类型含义数据格式4000103只读实时重量含去皮后有符号 32 位高字在前4000303只读CS1237 AD 原始值有符号 32 位补码4000503只读状态字0x0000 正常0x8001 超量程4001006读写零点校正触发写入 0x5A5A 触发4001106读写量程校正触发写入砝码重量kg×10004002003只读设备地址1 ~ 247上位机读重量通常就是循环读 40001 和 40003两个值配合做诊断。这里有个细节重量用 32 位有符号数因为 24 位 AD 补码换算成十进制后范围是 -8388608 到 8388607乘以增益缩放后完全可能超过 16 位整数范围用 16 位寄存器会被截断。我在调试一组 100kg 量程、传感器灵敏度 2mV/V 的秤体时AD 输出换算重量后约为 40000 左右16 位还够用但换成 500kg 量程时原始值轻松突破 300000不用 32 位必然翻车。所以源码里寄存器连续占两格的就是 32 位数据上位机必须按大端序拼起来。3.3 CRC16 查表实现与主站联调9600 波特率下的性能余量评估Modbus RTU 的 CRC16 多项式是 0xA001实现方式有逐位计算和查表两种。八位单片机上逐位计算每帧耗时约 1~2ms查表法可以缩到 0.2ms 以内。这个资源包里用的是查表法代码大致长这样// 计算 Modbus CRC16查表法返回 16 位校验值 uint16_t MODBUS_Calc_CRC16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i; for (i 0; i len; i) { crc ^ buf[i]; // 查表过程每字节展开 8 次异或运算 } return crc; }实际调试时要分清两种字节序CRC 低字节在前、高字节在后和报文数据的高字节在前不一样。我之前在模拟主站调试时就是 CRC 字节序搞反在串口助手里看到一切正常换成组态王就超时无响应。区分办法很简单用 Modbus Poll 之类主站模拟工具跑一遍如果报文收发正常说明字节序没问题如果一请求就超时先检查 CRC 字节顺序再检查从站地址。主站联调时我习惯先把从站地址设为 1波特率固定 9600、8N1只下发 03 功能码读 40001一次读一个寄存器确认数据稳定后再逐步扩大到一次读多个寄存器。直接在组态王里配好批量读取一旦失败很难定位是哪一个寄存器地址对不上。4. 从传感器到上位机称重数据的完整链路整合流程4.1 主循环调度10ms 节拍下 ADC 采样与 Modbus 应答的分时策略N76E003 是 8051 内核没有 RTOS所有任务都靠主循环轮询。固件里常见的调度方式是让定时器中断产生一个 1ms 的时基主循环里累计到 10ms 做一次 ADC 采样平均累计到 5ms 检查一次 Modbus 接收超时。注意这里的核心矛盾CS1237 的数据输出速率默认是 10SPS也就是 100ms 才出一个新数据而 Modbus 主站可能在 10ms 内就问一次重量。如果每次都读同一个变量读到的必然是上一帧的旧值——这是正常的不是 bug。但如果你没做采样平均直接把 AD 原始值塞进寄存器上位机看到的重量会像噪声一样乱跳。// 主循环分时处理采样与通讯 void main(void) { uint16_t sample_count 0; int32_t ad_sum 0; while (1) { // 每个 10ms 节拍累计 AD 值 if (timer_tick 10) { timer_tick 0; uint32_t ad_val CS1237_Read_Data(); if (ad_val ! 0xFFFFFF) { // 过滤异常数据 ad_sum (int32_t)ad_val; sample_count; } // 每累计 10 次取一次平均约 100ms 更新一次重量 if (sample_count 10) { g_current_ad ad_sum / sample_count; g_weight_raw CAL_Convert_AD_To_Weight(g_current_ad); ad_sum 0; sample_count 0; } } // 每个 5ms 检查一次 Modbus 帧 MODBUS_Poll(); } }这段调度的关键在于「取平均的窗口时长」必须和 CS1237 的输出速率匹配。默认 10SPS 时10 次平均需要 1 秒太重2 次平均是 200ms适合大部分称重场景。源码默认窗口是 10但我实际调试时发现用在灌装秤上反应太慢改成 4 次平均后既有一定平滑效果动态响应也够快。这个参数是称重仪表调手感最重要的一个旋钮。4.2 称重换算从 24 位 AD 码到工程单位的标度变换与去皮逻辑AD 原始值不是重量中间隔着灵敏度、增益和基准电压三层换算。常见做法是先校零空秤读 AD 值存为零点再校满量程放标准砝码读出 AD 值存两个标定点运行时用线性插值计算。源码里核心换算函数如下// 标度变换利用零点与满量程标定值计算当前重量 int32_t CAL_Convert_AD_To_Weight(int32_t ad_current) { int32_t zero_ad g_calib.zero_ad; // 空秤零点 AD int32_t span_ad g_calib.span_ad; // 满量程标定 AD int32_t span_weight g_calib.span_weight; // 满量程砝码重量单位 g // 超出量程直接返回异常状态 if (ad_current zero_ad - 1000 || ad_current zero_ad span_ad 1000) { return AD_VALUE_OVER_RANGE; } // 线性比例换算结果四舍五入 return (int32_t)(((int64_t)(ad_current - zero_ad) * span_weight) / span_ad); }换算函数看起来简单但有两个细节会直接决定秤的线性度一是要用 64 位中间变量做乘法24 位 AD 码乘以几百克的砝码重量乘积动辄上亿32 位乘法溢出后线性度会像疯了一样乱跳二是严禁在运行过程中频繁改写零点 AD 值去皮必须单独用另一个偏移量否则标定点漂移了找都找不回来。这个固件包里去皮是放在重量寄存器层面的不动标定点这个设计是对的。4.3 整机验证流程串口助手、模拟主站、触摸屏三步递进拿到固件烧进板子后的验证顺序我建议固定为三步。第一步用串口助手以十六进制方式手动下发功能吗03 读寄存器看看返回帧是否正确这一步可以确认从站地址和 CRC第二步用 Modbus 调试软件持续轮询观察寄存器数值在 10 秒内是否稳定这一步能暴露数据抖动和帧间隔问题第三步才接触摸屏或组态软件验证设备地址和寄存器配置是否和你画的人机界面一致。很多人跳过第二步直接上屏结果屏上重量跳个不停根源其实是数据本身就有问题。5. 避坑清单五个让称重不准或通讯失败的常见问题5.1 现象重量值长时间漂移不停零点越跑越远原因CS1237 的 PGA 增益和输出速率的配置没有在每次读取后重新下发。CS1237 数据手册的隐含条件是增益档位切换后有较长的建立时间如果你在主循环里频繁切换增益每次切换后前几十次采样点都是无效的。另外CS1237 对电源稳定性非常敏感直接拿开发板 3.3V 供电纹波会直接体现在 AD 码上。解决先把增益固定到 128不做动态切换然后在读取函数末尾统一重新配置增益。电源端在 AVDD 到 GND 之间加 10uF 0.1uF 滤波电容模拟部分和数字部分分开布线。这两个动作做完零点漂移基本能缩小一个数量级。5.2 现象主站发送请求一直超时但串口助手里手动发同一帧却能收到应答原因这是帧间隔判定不一致造成的。手动发帧时两条命令之间的间隔足够长从站能正确区分两帧主站连续轮询时请求帧之间的间隔可能小于从站设定的帧超时阈值导致从站把两帧合并成一帧CRC 必然错误。解决把帧结束判定从 4ms 对应到实际波特率9600 波动率留 1ms 余量不是这个思路错了阈值不能无限调小。正确的做法是把接收缓冲区在收到帧后先判断字节数是否满足最小请求长度至少 8 个字节不满足就等下一帧同时在收到完整且 CRC 正确的请求后才清空缓冲区。我实际调试时碰到过主站软件发帧间隔只有 1.5ms把阈值调到 2ms 后才稳定。5.3 现象上电瞬间主站第一次轮询永远失败复位后才正常原因N76E003 上电后需要时间完成内部 RC 振荡器起振和稳定CS1237 首次转换也需要约 100ms。如果主站在从站复位完成后 20ms 内就发出请求此时串口还没初始化完成或者 CS1237 的第一帧数据还没准备好从站自然无法应答。解决固件里在初始化的最前面加一个 500ms 软件延时利用定时器不要用空转 for 循环在主循环进入 Modbus 轮询之前先用while等待 CS1237 完成第一次转换。这个坑不是每次上电都出现但是低温或者电源偏弱时概率明显上升不加延时就是玄学级故障。5.4 现象AD 码读数正常但换算后的重量在小数位来回跳一个工程单位原因没有做滑动平均或窗口平均。24 位 ADC 的最后几个 LSB 本身就是噪声这是物理噪声不是代码 bug。直接把单次 AD 码送进标度变换函数重量必然来回跳。解决在 4.1 节调度代码基础上把取平均窗口设为 4~8 次。如果你做的是静态秤窗口可以放宽到 16 次动态应用则必须缩到 2 次以下。这里没有「最优」参数只有符合你应用节奏的窗口值。5.5 现象运行一段时间后从站彻底无响应断电重启又恢复原因N76E003 内置看门狗没有喂狗或者主循环里某个耗时操作比如 CS1237 等待超时卡死了主循环看门狗复位后程序没有恢复现场。Modbus 从站如果长 100ms 不喂狗在外部干扰下大概率复位复位后寄存器状态不对照样不回复。解决在主循环每个 10ms 节拍里调用喂狗函数同时确保所有 while 等待超时都有计数上限杜绝进入死等状态。6. 进阶技巧把按键标定融进 Modbus 寄存器做成可远程调试的仪表6.1 用 06 功能码触发标定流程少一个按键是一回事少一次拆机是另一回事多数称重仪表标定靠的是实体按键加数码管菜单但这套固件把标定入口直接开放到寄存器意味着不用拆仪表外壳就能远程完成校准。触发逻辑如下上位机写 40010 寄存器为0x5A5A触发零点标定写 40011 寄存器为砝码重量触发量程标定。固件里对应流程// 处理 Modbus 写寄存器请求时识别标定触发码 void MODBUS_Handle_Write_Single(uint16_t reg_addr, uint16_t value) { if (reg_addr 0x000A) { // 40010 映射地址 0x000A if (value 0x5A5A) { g_calib.zero_ad CS1237_Read_Data(); // 存零点 CAL_Save_To_EEPROM(); // 掉电保存 } } else if (reg_addr 0x000B) { // 40011 映射地址 0x000B g_calib.span_weight (int32_t)value; // 砝码重量单位 g g_calib.span_ad CS1237_Read_Data(); // 存量程 AD CAL_Save_To_EEPROM(); } }这里有个容易忽略的条件量程标定时秤台上必须先放好对应重量的砝码再写寄存器顺序反了标定数据必废。我在自己的项目里加了一道互斥状态位——零点标定后必须先完成量程标定否则重量寄存器的状态字一直显示未标定。这样的好处是远程调试时不会因为手误写错顺序导致整套参数作废。6.2 标定参数掉电保存N76E003 的数据区操作与回滚保护标定参数必须掉电不丢。N76E003 没有独立 EEPROM但可以利用程序 Flash 的空闲页做数据存储写入前先擦除整页。代码里我用了一个极简的校验方法把标定结构体计算一个累加和每次上电读出来先校验累加和不对就直接恢复出厂默认值。// 标定结构体与校验 typedef struct { int32_t zero_ad; int32_t span_ad; int32_t span_weight; uint16_t checksum; // 所有字节累加和 } CAL_Params_t;保存时先算好 checksum 再整块写入读取时先算一遍再和存储值比对不一致就认为存储区损坏。这个习惯让我避免了好几次「存储器里全是 0xFF 导致重量显示几百万」的翻车事故。从那以后凡是涉及掉电保存的功能我都强制在数据结构的尾部挂一个校验字。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑