找回密码
 立即注册
楼主: wuzhengmin

有关DMA,山东大学陈桂友教授

[复制链接]
  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 20:19:15 | 显示全部楼层
移 0:此字节为从机设备地址,同上述描述。.
偏移1:此字节为功能码,同上述描述。.
偏移2:此2字节为要写入的寄存器首地址,同上述描述。.
偏移4:此2字节为已经写入的寄存器个数,0x0004表示已经写入4个寄存器。.
最后两个字节:CRC16校验值0x33C0,低字节在前,高字节在后(算法与电脑通用,小端模式)。


如果命令解析错误,则从机返回错误帧:
数据功能:地址  错误码  CRC16偏移字节:
                 0        1          最后 2 字节。
字节数量:1 byte  1 byte     2 byte
                01          83         404D

回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:23:29 | 显示全部楼层
梁工详细介绍了CRC校验:

//========================================================================
// 函数: u16        MODBUS_CRC16(u8 *p, u8 n)
// 描述: 计算CRC16函数.
// 参数: *p: 要计算的数据指针.
//        n: 要计算的字节数.
// 返回: CRC16值.
// 版本: V1.0, 2022-3-18 梁工
//========================================================================
u16        MODBUS_CRC16(u8 *p, u8 n)
{
        u8        i;
        u16        crc16;

        crc16 = 0xffff;        //预置16位CRC寄存器为0xffff(即全为1)
        do
        {
                crc16 ^= (u16)*p;                //把8位数据与16位CRC寄存器的低位相异或,把结果放于CRC寄存器
                for(i=0; i<8; i++)                //8位数据
                {
                        if(crc16 & 1)        crc16 = (crc16 >> 1) ^ 0xA001;        //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位,
                                                                                                                        //再异或多项式0xA001
                        else        crc16 >>= 1;                                                        //如果最低位为0,把CRC寄存器的内容右移一位(朝低位),用0填补最高位
                }
                p++;
        }while(--n != 0);
        return        (crc16);
}
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:27:19 | 显示全部楼层
重点还是ModBus 协议的解析函数:

/********************* modbus协议 *************************/
/***************************************************************************
写多寄存器
数据:          地址    功能码   寄存地址 寄存器个数  写入字节数   写入数据   CRC16
偏移:     0        1        2 3      4 5          6          7~        最后2字节
字节:   1 byte   1 byte    2 byte   2 byte      1byte       2*n byte   2 byte
         addr     0x10      xxxx     xxxx        xx         xx....xx    xxxx

  返回
数据:          地址    功能码   寄存地址 寄存器个数   CRC16
偏移:     0        1        2 3      4 5         6 7
字节:   1 byte   1 byte    2 byte   2 byte      2 byte
         addr     0x10      xxxx     xxxx        xxxx


读多寄存器
数据:站号(地址)  功能码   寄存地址 寄存器个数  CRC16
偏移:      0       1        2 3      4 5         6 7
字节:    1 byte  1 byte    2 byte   2 byte     2 byte
          addr    0x03      xxxx     xxxx       xxxx

  返回
数据:站号(地址)  功能码   读出字节数  读出数据  CRC16
偏移:      0       1        2           3~      最后2字节
字节:   1 byte   1 byte    1byte      2*n byte  2 byte
         addr     0x03       xx       xx....xx   xxxx

  返回错误代码
数据:站号(地址)  错误码   CRC16
偏移:      0       1      最后2字节
字节:   1 byte   1 byte   2 byte
         addr     0x93     xxxx
***************************************************************************/
u8        MODBUS_RTU(void)
{
        u8        i,j,k;
        u16        reg_addr;        //寄存器地址
        u8        reg_len;        //写入寄存器个数
        u16        crc;

        if(RX1_Buffer[1] == 0x10)        //写多寄存器
        {
                if(RX1_cnt < 9)                return 0x91;                //命令长度错误
                if((RX1_Buffer[4] != 0) || ((RX1_Buffer[5] *2) != RX1_Buffer[6]))        return 0x92;        //写入寄存器个数与字节数错误
                if((RX1_Buffer[5]==0) || (RX1_Buffer[5] > REG_LENGTH))        return 0x92;        //写入寄存器个数错误

                reg_addr = ((u16)RX1_Buffer[2] << 8) + RX1_Buffer[3];        //寄存器地址
                reg_len = RX1_Buffer[5];        //写入寄存器个数
                if((reg_addr+(u16)RX1_Buffer[5]) > (REG_ADDRESS+REG_LENGTH))        return 0x93;        //寄存器地址错误
                if(reg_addr < REG_ADDRESS)                return 0x93;        //寄存器地址错误
                if((reg_len*2+7) != RX1_cnt)        return 0x91;        //命令长度错误

                j = reg_addr - REG_ADDRESS;        //寄存器数据下标
                for(k=7, i=0; i<reg_len; i++,j++)
                {
                        modbus_reg[j] = ((u16)RX1_Buffer[k] << 8) + RX1_Buffer[k+1];        //写入数据, 大端模式
                        k += 2;
                }

                if(RX1_Buffer[0] != 0)        //非广播地址则应答
                {
                        for(i=0; i<6; i++)        TX1_Buffer[i] = RX1_Buffer[i];        //要返回的应答
                        crc = MODBUS_CRC16(TX1_Buffer, 6);
                        TX1_Buffer[6] = (u8)crc;        //CRC是小端模式, 先发低字节,后发高字节。
                        TX1_Buffer[7] = (u8)(crc>>8);
                        B_TX1_Busy = 1;                //标志发送忙
                        TX1_cnt    = 0;                //发送字节计数
                        TX1_number = 8;                //要发送的字节数
                        TI = 1;                                //启动发送
                }
        }
        else if(RX1_Buffer[1] == 0x03)        //读多寄存器
        {
                if(RX1_Buffer[0] != 0)        //非广播地址则应答
                {
                        if(RX1_cnt != 6)                return 0x91;                //命令长度错误
                        if(RX1_Buffer[4] != 0)        return 0x92;        //读出寄存器个数错误
                        if((RX1_Buffer[5]==0) || (RX1_Buffer[5] > REG_LENGTH))        return 0x92;        //读出寄存器个数错误

                        reg_addr = ((u16)RX1_Buffer[2] << 8) + RX1_Buffer[3];        //寄存器地址
                        reg_len = RX1_Buffer[5];        //读出寄存器个数
                        if((reg_addr+(u16)RX1_Buffer[5]) > (REG_ADDRESS+REG_LENGTH))        return 0x93;        //寄存器地址错误
                        if(reg_addr < REG_ADDRESS)                return 0x93;        //寄存器地址错误

                        j = reg_addr - REG_ADDRESS;        //寄存器数据下标
                        TX1_Buffer[0] = SL_ADDR;        //站号地址
                        TX1_Buffer[1] = 0x03;                //读功能码
                        TX1_Buffer[2] = reg_len*2;        //返回字节数

                        for(k=3, i=0; i<reg_len; i++,j++)
                        {
                                TX1_Buffer[k++] = (u8)(modbus_reg[j] >> 8);        //数据为大端模式
                                TX1_Buffer[k++] = (u8)modbus_reg[j];
                        }
                        crc = MODBUS_CRC16(TX1_Buffer, k);
                        TX1_Buffer[k++] = (u8)crc;        //CRC是小端模式, 先发低字节,后发高字节。
                        TX1_Buffer[k++] = (u8)(crc>>8);
                        B_TX1_Busy = 1;                //标志发送忙
                        TX1_cnt    = 0;                //发送字节计数
                        TX1_number = k;                //要发送的字节数
                        TI = 1;                                //启动发送
                }
        }
        else        return 0x90;        //功能码错误

        return 0;        //解析正确
}
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:34:42 | 显示全部楼层
主要是合法性检测,对收到的数据进行解析

截图202602112134402664.jpg
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:37:05 | 显示全部楼层
应答之前,先检测是否是广播地址:

if(RX1_Buffer[0] != 0)        //非广播地址则应答
                {
                        for(i=0; i<6; i++)        TX1_Buffer[i] = RX1_Buffer[i];        //要返回的应答
                        crc = MODBUS_CRC16(TX1_Buffer, 6);
                        TX1_Buffer[6] = (u8)crc;        //CRC是小端模式, 先发低字节,后发高字节。
                        TX1_Buffer[7] = (u8)(crc>>8);
                        B_TX1_Busy = 1;                //标志发送忙
                        TX1_cnt    = 0;                //发送字节计数
                        TX1_number = 8;                //要发送的字节数
                        TI = 1;                                //启动发送
                }
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:42:27 | 显示全部楼层
这个协议好处很明显:简单高效,容易理解:

看看梁工的说明:

本例程只支持多寄存器读和多寄存器写, 寄存器长度为64个, 别的命令用户可以根据需要按MODBUS-RTU协议自行添加.

本例子数据使用大端模式(与C51一致), CRC16使用小端模式(与PC一致).

默认参数:
串口1设置均为 1位起始位, 8位数据位, 1位停止位, 无校验.
串口1(P3.0 P3.1): 9600bps.

定时器0用于超时计时. 串口每收到一个字节都会重置超时计数, 当串口空闲超过35bit时间时(9600bps对应3.6ms)则接收完成.
用户修改波特率时注意要修改这个超时时间.

本例程只是一个应用例子, 科普MODBUS-RTU协议并不在本例子职责范围, 用户可以上网搜索相关协议文本参考.
本例定义了64个寄存器, 访问地址为0x1000~0x103f.
命令例子:
写入4个寄存器(8个字节):
10 10 1000 0004 08 1234 5678 90AB CDEF 4930
返回:
10 10 10 00 00 04 C64B
读出4个寄存器:
10 03 1000 0004 4388
返回:
10 03 08 12 34 56 78 90 AB CD EF D53D

命令错误返回信息(自定义):
0x90: 功能码错误. 收到了不支持的功能码.
0x91: 命令长度错误.
0x92: 写入或读出寄存器个数或字节数错误.
0x93: 寄存器地址错误.

注意: 收到广播地址0x00时要处理信息, 但不返回应答.
回复

使用道具 举报 送花

  • 打卡等级:常住居民III
  • 打卡总天数:153
  • 最近打卡:2026-03-30 19:54:03
已绑定手机

22

主题

2426

回帖

3422

积分

论坛元老

积分
3422
发表于 2026-2-11 21:50:24 | 显示全部楼层
可以用DMA发送和接收

放进入自动发送接收

3.5个字符间隔的意思的,如何区分2帧数据!

好了 梁工的ModBus 协议课程到此结束!!!

下一次我们学习:串口库函数、串口DMA,USB-CDC


感谢梁工的辛苦劳动和无私奉献,讲所有源码共享给大家!感谢感谢感谢!!!
回复

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-3-31 20:19 , Processed in 0.108968 second(s), 67 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表