12下一页
返回列表 发新帖我要提问本帖赏金: 10.00元(功能说明)

[AT32L021] 【AT-START-L021测评】Modbus RTU 从站的实现

[复制链接]
 楼主| bingbing12138 发表于 2024-12-25 22:04 | 显示全部楼层 |阅读模式
本帖最后由 bingbing12138 于 2024-12-26 21:20 编辑

#申请原创# #技术资源# 非常感谢雅特力官方给我这样一个测评的机会,本次主要使用AT32L021实现ModbusRTU从站功能,支持的功能码 01 02 03 04 05 06 0F 10
一、Modbus标准简介
Modbus 是由 Modicon(现为施耐德电气公司的一个品牌)在 1979 年发明的,是全球第一个真正用于工业现场的总线协议。
ModBus 网络是一个工业通信系统,由带智能终端的可编程序控制器和计算机通过公用线路或局部专用线路连接而成。其系统结构既包括硬件、亦包括软件。它可应用于各种数据采集和过程监控。
为更好地普及和推动 Modbus 在基于以太网上的分布式应用,目前施耐德公司已将 Modbus 协议的所有权移交给 IDA(Interface for Distributed Automation,分布式自动化接口)组织,并成立了Modbus-IDA 组织,为 Modbus 今后的发展奠定了基础。
在中国,Modbus 已经成为国家标准。
标准编号:GB/T19582-2008
标准名称:《基于 Modbus 协议的工业自动化网络规范》
分 3 个部分:
《GB/T 19582.1-2008 第 1 部分:Modbus 应用协议》
《GB/T 19582.2-2008 第 2 部分:Modbus 协议在串行链路上的实现指南》
《GB/T 19582.3-2008 第 3 部分: Modbus 协议在 TCP/IP 上的实现指南》
二、Modbus协议概述
49230676c14efeba88.png
23662676c150a4e135.png
Modbus 串行链路协议是一个主/从协议。该协议位于 OSI 模型的第二层。
一个主从类型的系统有一个向某个“子”节点发出显式命令并处理响应的节点(主节点)。典型的子节点在没有收到主节点的请求时并不主动发送数据,也不与其它子节点通信。
在物理层,Modbus 串行链路系统可以使用不同的物理接口(RS485、RS232)。最常用的是TIA/EIA-485 (RS485) 两线制接口。

三、Modbus 主站/从站协议原理
Modbus 串行链路协议是一个主-从协议。在同一时刻,只有一个主节点连接于总线,一个或多个子节点 (最大编号为 247 ) 连接于同一个串行总线。Modbus 通信总是由主节点发起。子节点在没有收到来自主节点的请求时,从不会发送数据。子节点之间从不会互相通信。主节点在同一时刻只会发起一个Modbus 事务处理。
主节点以两种模式对子节点发出 Modbus 请求:

单播模式:
主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返回一个报文(一个'应答')。在这种模式,一个 Modbus 事务处理包含 2 个报文:一个来自主节点的请求,一个来自子节点的应答。
每个子节点必须有唯一的地址 (1 到 247),这样才能区别于其它节点被独立的寻址。

广播模式:
主节点向所有的子节点发送请求。对于主节点广播的请求没有应答返回。广播请求一般用于写命令。
所有设备必须接受广播模式的写功能。地址 0 是专门用于表示广播数据的。

地址规则:
Modbus 寻址空间有 256 个不同地址。
0 1~247 248~255
广播地址 子节点单独地址 保留

地址 0 为广播地址。所有的子节点必须识别广播地址。
Modbus 主节点没有地址,只有子节点必须有一个地址。 该地址必须在 Modbus 串行总线上唯一。
四、Modbus报文RTU
由发送设备将 Modbus 报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。
在 RTU 模式,报文帧由时长至少为 3.5 个字符时间的空闲间隔区分。在后续的部分,这个时间区间被称作 t3.5。
47967676c1707a21d9.png

整个报文帧必须以连续的字符流发送。
如果两个字符之间的空闲间隔大于 1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃。


8666676c172b3d1b8.png

注 :
RTU 接收驱动程序的实现,由于 t1.5 和 t3.5 的定时,隐含着大量的对中断的管理。在高通信速率下,这导致 CPU 负担加重。因此,在通信速率等于或低于 19200 bps 时,这两个定时必须严格遵守;
对于波特率大于 19200 bps 的情形,应该使用 2 个定时的固定值:

建议的字符间超时时间(t1.5)为 750µs,
帧间的超时时间 (t1.5) 为 1.750ms。
下图表示了对 RTU 传输模式状态图的描述。 "主节点" 和 "子节点" 的不同角度均在相同的图中表示:

94726676c177666ca6.png
上面状态图的一些解释:
1从 "初始" 态到 “空闲” 态转换需要 t3.5 定时超时: 这保证帧间延迟
1“空闲” 态是没有发送和接收报文要处理的正常状态。
1在 RTU 模式, 当没有活动的传输的时间间隔达 3.5 个字符长时,通信链路被认为在 “空闲” 态。
1当链路空闲时, 在链路上检测到的任何传输的字符被识别为帧起始。 链路变为 "活动" 状态。 然后,当链路上没有字符传输的时间间个达到 t3.5 后,被识别为帧结束。
1检测到帧结束后,完成 CRC 计算和检验。然后,分析地址域以确定帧是否发往此设备,如果不是,则丢弃此帧。 为了减少接收处理时间,地址域可以在一接到就分析,而不需要等到整个帧结束。这样,CRC 计算只需要在帧寻址到该节点 (包括广播帧) 时进行。
五、CRC校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical RedundancyChecking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
CRC 包含由两个 8 位字节组成的一个 16 位值。
CRC 域作为报文的最后的域附加在报文之后。计算后,首先附加低字节,然后是高字节。CRC 高字节为报文发送的最后一个字节。
附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的 CRC 值相比较。如果两个值不相等,则为错误。
CRC 的计算, 开始对一个 16 位寄存器预装全 1。然后将报文中的连续的 8 位子节对其进行后续的计算。只有字符中的 8 个数据位参与生成 CRC 的运算,起始位,停止位和校验位不参与 CRC 计算。
CRC 的生成过程中, 每个 8–位字符与寄存器中的值异或。然后结果向最低有效位(LSB)方向移动(Shift) 1 位,而最高有效位(MSB)位置充零。 然后提取并检查 LSB:如果 LSB 为 1, 则寄存器中的值与一个固定的预置值异或;如果 LSB 为 0, 则不进行异或操作。
这个过程将重复直到执行完 8 次移位。完成最后一次(第 8 次)移位及相关操作后,下一个 8 位字节与寄存器的当前值异或,然后又同上面描述过的一样重复 8 次。当所有报文中字节都运算之后得到的寄存器的最终值,就是 CRC。
六、Modubs从站源码
主程序
  1. #include "at32l021_clock.h"
  2. #include "at32l021_board.h"
  3. #include "modbus_slave.h"
  4. #include "bsp_dma.h"
  5. #include "bsp_usart.h"
  6. #include "bsp_time.h"
  7. #include "crc16.h"

  8. //定时器3中断服务函数 处理Modbus主任务
  9. void TMR3_GLOBAL_IRQHandler(void)//1ms中断
  10. {
  11.     if (tmr_flag_get(TMR3, TMR_OVF_FLAG) == SET)
  12.     {
  13.         MODBUS_SLAVE_Pool();
  14.         tmr_flag_clear(TMR3, TMR_OVF_FLAG);
  15.     }
  16. }

  17. //定时器6中断服务函数 计算帧间隔超时计数
  18. void TMR6_GLOBAL_IRQHandler(void)//50us中断
  19. {
  20.     if (tmr_flag_get(TMR6, TMR_OVF_FLAG) == SET)
  21.     {
  22.         timer_record.timer_counter++;
  23.         tmr_flag_clear(TMR6, TMR_OVF_FLAG);
  24.     }
  25. }

  26. //串口2中断服务函数 串口发送完成中断,用于判断报文发送完成从而开启接收
  27. void USART2_IRQHandler(void)
  28. {
  29.     if(usart_interrupt_flag_get(USART2, USART_TDC_FLAG) != RESET)
  30.     {
  31.         RS485_RX_EN();//接收使能
  32.         dma_channel_enable(DMA1_CHANNEL5, TRUE); //DMA接收使能
  33.         usart_interrupt_enable(USART2, USART_TDC_INT, FALSE);//关闭串口发送完成中断
  34.     }
  35. }

  36. crm_clocks_freq_type clocks_struct;
  37. int main(void)
  38. {
  39.     system_clock_config();
  40.     crm_clocks_freq_get(&clocks_struct);
  41.    
  42.     rs485_send_init();//MAX485使能引脚初始化
  43.     bsp_usart_init();//串口2初始化
  44.    
  45.     bsp_dma_uart_tx_init(modbus_slave.tx_buf,TX_LEN_MAX);//初始化串口发送dma
  46.     bsp_dma_uart_rx_init(modbus_slave.rx_buf,RX_LEN_MAX);//初始化串口接收dma
  47.    
  48.     bsp_timer3_init();//1ms
  49.     bsp_timer6_init();//50us
  50.     /*报文接收时间间隔 由于此处使用115200波特率
  51.     ModbusRTU协议规定 大于19200 bps时,帧间隔超时时间为1.75ms
  52.     1.75ms = 1750us  1750/50 = 35*/
  53.     timer_record.overtime = 35;
  54.     while(1)
  55.     {
  56.         ;
  57.     }
  58. }
由于内容限制,这里只展示从站主要源码,程序实现Modbus功能码 01 02 03 04 05 06 0F 10:
  1. /************************************************************************/
  2. /*  “文件包含”处理                                                       */
  3. /************************************************************************/
  4. #include "modbus_slave.h"
  5. #include "bsp_dma.h"
  6. #include "bsp_time.h"
  7. #include "bsp_usart.h"
  8. #include "crc16.h"

  9. /************************************************************************/
  10. /*  全局变量定义                                                         */
  11. /************************************************************************/
  12. Modbus_Slave_t modbus_slave = {0};
  13. uint16_t test_hold_reg[3] = {0};// 03  06 10 功能码使用
  14. uint8_t test_led[15] = {0};// 01 05  功能码使用
  15. uint8_t test_adc[5] = {0x66, 0x77};// 04  功能码使用
  16. /************************************************************************/
  17. /*  函数声明                                                             */
  18. /************************************************************************/
  19. static void MODBUS_SLAVE_FunctionCodeHandle(void);
  20. static void MODBUS_SLAVE_01H(void);// 01功能码通过修改test_led[15]数组的值
  21. static void MODBUS_SLAVE_02H(void);// 02功能码通过函数MODBUS_SLAVE_BSP_GetKeyState 返回值
  22. static void MODBUS_SLAVE_03H(void);// 03功能码通过读取test_hold_reg[3]数组的值
  23. static void MODBUS_SLAVE_04H(void);// 04功能码通过读取test_adc[5]数组的值
  24. static void MODBUS_SLAVE_05H(void);// 05功能码通过修改test_led[15]数组的值
  25. static void MODBUS_SLAVE_06H(void);// 06功能码通过修改test_hold_reg[3]数组的值
  26. static void MODBUS_SLAVE_10H(void);// 10功能码通过修改多个test_hold_reg[3]数组的值
  27. static void MODBUS_SLAVE_0FH(void);// 0F功能码通过修改多个test_led[15]数组的值
  28. void MODBUS_SLAVE_SendAckErr(uint8_t ErrCode);

  29. //Modbus主程序
  30. void MODBUS_SLAVE_Pool(void)
  31. {
  32.     uBit16 addr;
  33.     uBit16 crc1;
  34.    
  35.     modbus_slave.curr_dma_cnt = dma_data_number_get(DMA1_CHANNEL5);//获取当前串口接收DMA传输计数器中的值
  36.     if (timer_record.timer_start_flag == 0 && modbus_slave.curr_dma_cnt != RX_LEN_MAX)
  37.     {
  38.         timer_record.timer_start_flag = 1;//表示开始接收了
  39.         timer_record.StartTime = timer_record.timer_counter; // 记录开始时间
  40.         
  41.         modbus_slave.last_dma_cnt = modbus_slave.curr_dma_cnt;
  42.     }
  43.     if (timer_record.timer_start_flag == 1)
  44.     {
  45.         if (modbus_slave.curr_dma_cnt == modbus_slave.last_dma_cnt)
  46.         {
  47.             timer_record.Currvalue = timer_record.timer_counter - timer_record.StartTime;//获取经过的时间
  48.             if (timer_record.Currvalue > timer_record.overtime)//判断是否超时
  49.             {
  50.                 timer_record.timer_start_flag = 0;//清除接收标志
  51.                 modbus_slave.recv_len = RX_LEN_MAX - modbus_slave.curr_dma_cnt;//已经接收的字节数
  52.                
  53.                 //成功接收到一帧报文
  54.                 bsp_dma_uart_rx_init(modbus_slave.rx_buf,RX_LEN_MAX);//初始化串口接收DMA
  55.                 dma_channel_enable(DMA1_CHANNEL5, FALSE);//接收DMA失能
  56.                 RS485_TX_EN();//发送使能
  57.                
  58.                 if (modbus_slave.recv_len < 4)  /* 接收到的数据小于4个字节就认为错误 */
  59.                 {
  60.                     RS485_RX_EN();//接收使能
  61.                     dma_channel_enable(DMA1_CHANNEL5, TRUE); //DMA接收使能
  62.                     goto err_ret;
  63.                 }
  64.                
  65.                 /* 计算CRC校验和 */
  66.                 crc1 = CRC16_Modbus(modbus_slave.rx_buf, modbus_slave.recv_len);
  67.                 if (crc1 != 0)
  68.                 {
  69.                     RS485_RX_EN();//接收使能
  70.                     dma_channel_enable(DMA1_CHANNEL5, TRUE); //DMA接收使能
  71.                     goto err_ret;
  72.                 }
  73.                 /* 站地址 (1字节) */
  74.                 addr = modbus_slave.rx_buf[0];                                /* 第1字节 站号 */
  75.                 if (addr != SADDR485)                                         /* 判断主机发送的命令地址是否符合 */
  76.                 {
  77.                     RS485_RX_EN();//接收使能
  78.                     dma_channel_enable(DMA1_CHANNEL5, TRUE); //DMA接收使能
  79.                     goto err_ret;
  80.                 }
  81.                
  82.                 //功能码处理
  83.                 MODBUS_SLAVE_FunctionCodeHandle();
  84.             }
  85.         }
  86.         else
  87.         {
  88.             // 如果 DMA 接收计数值发生变化,则更新计数
  89.             timer_record.StartTime = timer_record.timer_counter; // 更新开始时间
  90.         }
  91.         modbus_slave.last_dma_cnt = modbus_slave.curr_dma_cnt;//更新此次DMA值
  92.     }
  93. err_ret:
  94.     memset(modbus_slave.rx_buf, 0, modbus_slave.recv_len);
  95.     modbus_slave.recv_len = 0;/* 必须清零计数器,方便下次帧同步 */
  96. }

  97. //Modbus功能码处理
  98. static void MODBUS_SLAVE_FunctionCodeHandle(void)
  99. {
  100.     switch (modbus_slave.rx_buf[1])                                /* 第2个字节 功能码 */
  101.     {
  102.         case 0x01:  /* 读取线圈状态(此例程用led代替)*/
  103.             MODBUS_SLAVE_01H();
  104.             break;

  105.         case 0x02:  /* 读取输入状态(按键状态)*/
  106.             MODBUS_SLAVE_02H();
  107.             break;
  108.         
  109.         case 0x03:  /* 读取保持寄存器*/
  110.             MODBUS_SLAVE_03H();
  111.             break;
  112.         
  113.         case 0x04:  /* 读取输入寄存器(ADC的值)*/
  114.             MODBUS_SLAVE_04H();
  115.             break;
  116.         
  117.         case 0x05:  /* 强制单线圈(设置led)*/
  118.             MODBUS_SLAVE_05H();
  119.             break;
  120.         
  121.         case 0x06:  /* 写单个保存寄存器*/
  122.             MODBUS_SLAVE_06H();        
  123.             break;
  124.             
  125.         case 0x10:  /* 写多个保存寄存器*/
  126.             MODBUS_SLAVE_10H();
  127.             break;
  128.         
  129.         case 0x0F:  /* 写多个线圈寄存器*/
  130.             MODBUS_SLAVE_0FH();
  131.             break;
  132.         default:
  133.             modbus_slave.RTU_response_code = RSP_ERR_CMD;
  134.             MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);        /* 告诉主机命令错误 */
  135.             break;
  136.     }
  137. }


  138. //将两字节转换为16位
  139. uBit16 MODBUS_SLAVE_BEBufToUint16(uBit8 *_pBuf)
  140. {
  141.     return (((uBit16)_pBuf[0] << 8) | _pBuf[1]);
  142. }


  143. //串口发送
  144. void MODBUS_SLAVE_UartSend(uBit8 *buf, uBit16 len)
  145. {
  146.     //发送函数
  147.     bsp_dma_uart_tx_init(modbus_slave.tx_buf, len);//初始化dma,重置传输计数器
  148.     dma_channel_enable(DMA1_CHANNEL4, TRUE); /* usart2 tx begin dma transmitting */
  149.     usart_flag_clear(USART2, USART_TDC_FLAG);
  150.     usart_interrupt_enable(USART2, USART_TDC_INT, TRUE);
  151. }


  152. //发送加crc
  153. void MODBUS_SLAVE_SendWithCRC(uint8_t *buf, uint8_t len)
  154. {
  155.     uint16_t crc;
  156.    
  157.     crc = CRC16_Modbus(buf, len);
  158.     buf[len++] = crc >> 8;
  159.     buf[len++] = crc;
  160.    
  161.     MODBUS_SLAVE_UartSend(buf, len);
  162. }


  163. //发送错误应答  ErrCode : 错误代码
  164. void MODBUS_SLAVE_SendAckErr(uint8_t ErrCode)
  165. {
  166.     modbus_slave.send_len = 0;
  167.    
  168.     modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[0];          /* 485地址 */
  169.     modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[1] | 0x80;   /* 异常的功能码 */
  170.     modbus_slave.tx_buf[modbus_slave.send_len++] = ErrCode;                      /* 错误代码(01,02,03,04) */

  171.     MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  172. }

  173. //判断LED指示灯是否已经点亮
  174. uint8_t MODBUS_SLAVE_BSPIsLedOn(uint8_t num)
  175. {
  176.     if (num == 1)
  177.     {
  178.         if (test_led[0])
  179.         {
  180.             return 1;
  181.         }
  182.         return 0;
  183.     }
  184.     else if (num == 2)
  185.     {
  186.         if (test_led[1])
  187.         {
  188.             return 1;
  189.         }
  190.         return 0;
  191.     }
  192.     else if (num == 3)
  193.     {
  194.         if (test_led[2])
  195.         {
  196.             return 1;
  197.         }
  198.         return 0;
  199.     }
  200.     else if (num == 4)
  201.     {
  202.         if (test_led[3])
  203.         {
  204.             return 1;
  205.         }
  206.         return 0;
  207.     }

  208.     return 0;
  209. }


  210. //读线圈状态功能码 01H
  211. static void MODBUS_SLAVE_01H(void)
  212. {
  213.     uBit16 reg;
  214.     uBit16 num;
  215.     uBit16 i;
  216.     uBit16 m;
  217.     uBit8 status[10];

  218.     modbus_slave.RTU_response_code = RSP_OK;

  219.     if (modbus_slave.recv_len != 8)
  220.     {
  221.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; // 数据值域错误
  222.         return;
  223.     }
  224.    
  225.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   // 寄存器号
  226.     num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);   // 寄存器个数

  227.     m = (num + 7) / 8;

  228.     if ((reg >= REG_D01) && (num > 0) && (reg + num <= REG_DXX + 1))
  229.     {
  230.         for (i = 0; i < m; i++)
  231.         {
  232.             status[i] = 0;
  233.         }
  234.         for (i = 0; i < num; i++)
  235.         {
  236.             if (MODBUS_SLAVE_BSPIsLedOn(i + 1 + reg - REG_D01)) // 读LED的状态,写入状态寄存器的每一位
  237.             {  
  238.                 status[i / 8] |= (1 << (i % 8));
  239.             }
  240.         }
  241.     }
  242.     else
  243.     {
  244.         modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  // 寄存器地址错误
  245.     }

  246.     if (modbus_slave.RTU_response_code == RSP_OK)   // 正确应答
  247.     {
  248.         modbus_slave.send_len = 0;
  249.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[0];
  250.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[1];
  251.         modbus_slave.tx_buf[modbus_slave.send_len++] = m;   // 返回字节数

  252.         for (i = 0; i < m; i++)
  253.         {
  254.             modbus_slave.tx_buf[modbus_slave.send_len++] = status[i];
  255.         }
  256.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  257.     }
  258.     else
  259.     {
  260.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    // 告诉主机命令错误
  261.     }
  262. }


  263. //02读取输入状态
  264. uint8_t MODBUS_SLAVE_BSP_GetKeyState(uint8_t _ucKeyID)
  265. {
  266.     return 1;//此处应该返回按键按下状态,这里模拟都为1
  267. }

  268. //读取输入状态功能码 02H
  269. static void MODBUS_SLAVE_02H(void)
  270. {
  271.     uint16_t reg;
  272.     uint16_t num;
  273.     uint16_t i;
  274.     uint16_t m;
  275.     uint8_t status[10];

  276.     modbus_slave.RTU_response_code = RSP_OK;

  277.     if (modbus_slave.recv_len != 8)
  278.     {
  279.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  280.         return;
  281.     }

  282.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   /* 寄存器号 */
  283.     num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);   /* 寄存器个数 */

  284.     m = (num + 7) / 8;
  285.     if ((reg >= REG_T01) && (num > 0) && (reg + num <= REG_TXX + 1))
  286.     {
  287.         for (i = 0; i < m; i++)
  288.         {
  289.             status[i] = 0;
  290.         }
  291.         for (i = 0; i < num; i++)
  292.         {
  293.             if (MODBUS_SLAVE_BSP_GetKeyState(reg - REG_T01 + i))
  294.             {
  295.                 status[i / 8] |= (1 << (i % 8));
  296.             }
  297.         }
  298.     }
  299.     else
  300.     {
  301.         modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  302.     }

  303.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  304.     {
  305.         modbus_slave.send_len = 0;
  306.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[0];
  307.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[1];
  308.         modbus_slave.tx_buf[modbus_slave.send_len++] = m;   /* 返回字节数 */

  309.         for (i = 0; i < m; i++)
  310.         {
  311.             modbus_slave.tx_buf[modbus_slave.send_len++] = status[i];   /* T01-02状态 */
  312.         }
  313.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  314.     }
  315.     else
  316.     {
  317.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  318.     }
  319. }

  320. //读取保持寄存器的值
  321. uint8_t MODBUS_SLAVE_ReadRegValue(uint16_t reg_addr, uint8_t *reg_value)
  322. {
  323.     uint16_t value;

  324.     switch (reg_addr)   /* 判断寄存器地址 */
  325.     {
  326.         case SLAVE_REG_P01:
  327.             value = test_hold_reg[0];
  328.             break;

  329.         case SLAVE_REG_P02:
  330.             value = test_hold_reg[1];   /* 将寄存器值读出 */
  331.             break;

  332.         default:
  333.             return 0;   /* 参数异常,返回 0 */
  334.     }

  335.     reg_value[0] = value >> 8;
  336.     reg_value[1] = value;

  337.     return 1;   /* 读取成功 */
  338. }


  339. //读取保持寄存器功能码 03H
  340. static void MODBUS_SLAVE_03H(void)
  341. {
  342.     uint16_t reg;
  343.     uint16_t num;
  344.     uint16_t i;
  345.     uint8_t reg_value[64];

  346.     modbus_slave.RTU_response_code = RSP_OK;

  347.     if (modbus_slave.recv_len != 8) /* 03H命令必须是8个字节 */
  348.     {
  349.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  350.         goto err_ret;
  351.     }

  352.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   /* 寄存器号 */
  353.     num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);   /* 寄存器个数 */
  354.     if (num > sizeof(reg_value) / 2)
  355.     {
  356.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  357.         goto err_ret;
  358.     }

  359.     for (i = 0; i < num; i++)
  360.     {
  361.         if (MODBUS_SLAVE_ReadRegValue(reg, ®_value[2 * i]) == 0) /* 读出寄存器值放入reg_value */
  362.         {
  363.             modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  364.             break;
  365.         }
  366.         reg++;
  367.     }

  368. err_ret:
  369.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  370.     {
  371.         modbus_slave.send_len = 0;
  372.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[0];
  373.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[1];
  374.         modbus_slave.tx_buf[modbus_slave.send_len++] = num * 2; /* 返回字节数 */

  375.         for (i = 0; i < num; i++)
  376.         {
  377.             modbus_slave.tx_buf[modbus_slave.send_len++] = reg_value[2*i];
  378.             modbus_slave.tx_buf[modbus_slave.send_len++] = reg_value[2*i+1];
  379.         }
  380.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);   /* 发送正确应答 */
  381.     }
  382.     else
  383.     {
  384.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 发送错误应答 */
  385.     }
  386. }

  387. //读取输入寄存器功能码 04H
  388. static void MODBUS_SLAVE_04H(void)
  389. {
  390.     uint16_t reg;
  391.     uint16_t num;
  392.     uint16_t i;
  393.     uint16_t status[10];

  394.     memset(status, 0, 10);

  395.     modbus_slave.RTU_response_code = RSP_OK;

  396.     if (modbus_slave.recv_len != 8)
  397.     {
  398.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  399.         goto err_ret;
  400.     }

  401.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   /* 寄存器号 */
  402.     num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);   /* 寄存器个数 */

  403.     if ((reg >= REG_A01) && (num > 0) && (reg + num <= REG_AXX + 1))
  404.     {
  405.         for (i = 0; i < num; i++)
  406.         {
  407.             switch (reg)
  408.             {
  409.                 /* 测试参数 */
  410.                 case REG_A01:
  411.                       status[i] = test_adc[0];
  412.                     break;
  413.                     
  414.                 default:
  415.                     status[i] = 0;
  416.                     break;
  417.             }
  418.             reg++;
  419.         }
  420.     }
  421.     else
  422.     {
  423.         modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  424.     }

  425. err_ret:
  426.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  427.     {
  428.         modbus_slave.send_len = 0;
  429.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[0];
  430.         modbus_slave.tx_buf[modbus_slave.send_len++] = modbus_slave.rx_buf[1];
  431.         modbus_slave.tx_buf[modbus_slave.send_len++] = num * 2; /* 返回字节数 */

  432.         for (i = 0; i < num; i++)
  433.         {
  434.             modbus_slave.tx_buf[modbus_slave.send_len++] = status[i] >> 8;
  435.             modbus_slave.tx_buf[modbus_slave.send_len++] = status[i] & 0xFF;
  436.         }
  437.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  438.     }
  439.     else
  440.     {
  441.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  442.     }
  443. }

  444. //强制单线圈功能码 05H
  445. static void MODBUS_SLAVE_05H(void)
  446. {
  447.     uint16_t reg;
  448.     uint16_t value;

  449.     modbus_slave.RTU_response_code = RSP_OK;

  450.     if (modbus_slave.recv_len != 8)
  451.     {
  452.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  453.         goto err_ret;
  454.     }

  455.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   /* 寄存器号 */
  456.     value = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]); /* 数据 */

  457.     if ((value>>8) != 0x00 && (value>>8) != 0xFF)
  458.     {
  459.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  460.         goto err_ret;
  461.     }

  462.     //此处处理可以优化
  463.     if (value>>8)//右移8位为0xFF 则值1 表示打开
  464.         value = 1;
  465.     else//右移8位为0x00 则值0 表示关闭
  466.         value = 0;
  467.    
  468.     if (reg == REG_D01)
  469.     {
  470.         test_led[0] = value;//模拟LED状态
  471.     }
  472.     else if (reg == REG_D02)
  473.     {
  474.         test_led[1] = value;
  475.     }
  476.     else if (reg == REG_D03)
  477.     {
  478.         test_led[2] = value;
  479.     }
  480.     else if (reg == REG_D04)
  481.     {
  482.         test_led[3] = value;//模拟LED状态
  483.     }
  484.     else
  485.     {
  486.         modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  487.     }
  488. err_ret:
  489.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  490.     {
  491.         //此命令码应答报文与请求报文一致
  492.         memcpy(modbus_slave.tx_buf, modbus_slave.rx_buf, modbus_slave.recv_len - 2);//不复制crc位
  493.         modbus_slave.send_len = (modbus_slave.recv_len - 2);//不复制crc位
  494.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  495.     }
  496.     else
  497.     {
  498.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  499.     }
  500. }


  501. //读取保持寄存器的值
  502. uint8_t MODBUS_SLAVE_WriteRegValue(uint16_t reg_addr, uint16_t reg_value)
  503. {
  504.     switch (reg_addr)   /* 判断寄存器地址 */
  505.     {
  506.         case SLAVE_REG_P01:
  507.             test_hold_reg[0] = reg_value;   /* 将值写入保存寄存器 */
  508.             break;
  509.         
  510.         case SLAVE_REG_P02:
  511.             test_hold_reg[1] = reg_value;   /* 将值写入保存寄存器 */
  512.             break;
  513.         
  514.         default:
  515.             return 0;   /* 参数异常,返回 0 */
  516.     }

  517.     return 1;   /* 读取成功 */
  518. }

  519. //写单个寄存器功能码 06H
  520. static void MODBUS_SLAVE_06H(void)
  521. {
  522.     uint16_t reg;
  523.     uint16_t value;

  524.     modbus_slave.RTU_response_code = RSP_OK;

  525.     if (modbus_slave.recv_len != 8)
  526.     {
  527.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  528.         goto err_ret;
  529.     }

  530.     reg = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);   /* 寄存器号 */
  531.     value = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]); /* 寄存器值 */

  532.     if (MODBUS_SLAVE_WriteRegValue(reg, value) == 1)    /* 该函数会把写入的值存入寄存器 */
  533.     {
  534.         ;
  535.     }
  536.     else
  537.     {
  538.         modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  539.     }

  540. err_ret:
  541.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  542.     {
  543.         //此命令码应答报文与请求报文一致
  544.         memcpy(modbus_slave.tx_buf, modbus_slave.rx_buf, modbus_slave.recv_len - 2);//不复制crc位
  545.         modbus_slave.send_len = (modbus_slave.recv_len - 2);//不复制crc位
  546.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  547.     }
  548.     else
  549.     {
  550.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  551.     }
  552. }

  553. //写多个寄存器功能码 10H
  554. static void MODBUS_SLAVE_10H(void)
  555. {
  556.     uint16_t reg_addr;
  557.     uint16_t reg_num;
  558.     uint8_t byte_num;
  559.     uint8_t i;
  560.     uint16_t value;

  561.     modbus_slave.RTU_response_code = RSP_OK;

  562.     if (modbus_slave.recv_len < 11)
  563.     {
  564.         modbus_slave.RTU_response_code = RSP_ERR_VALUE; /* 数据值域错误 */
  565.         goto err_ret;
  566.     }

  567.     reg_addr = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[2]);  /* 寄存器号 */
  568.     reg_num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);   /* 寄存器个数 */
  569.     byte_num = modbus_slave.rx_buf[6];  /* 后面的数据体字节数 */

  570.     if (byte_num != 2 * reg_num)
  571.     {
  572.         ;
  573.     }

  574.     for (i = 0; i < reg_num; i++)
  575.     {
  576.         value = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[7 + 2 * i]); /* 寄存器值 */

  577.         if (MODBUS_SLAVE_WriteRegValue(reg_addr + i, value) == 1)
  578.         {
  579.             ;
  580.         }
  581.         else
  582.         {
  583.             modbus_slave.RTU_response_code = RSP_ERR_REG_ADDR;  /* 寄存器地址错误 */
  584.             break;
  585.         }
  586.     }

  587. err_ret:
  588.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  589.     {
  590.         //此命令码应答报文与请求报文一致
  591.         memcpy(modbus_slave.tx_buf, modbus_slave.rx_buf, 6);//不复制crc位
  592.         modbus_slave.send_len = 6;//不复制crc位
  593.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  594.     }
  595.     else
  596.     {
  597.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  598.     }
  599. }

  600. //写多个线圈
  601. uint8_t MODBUS_SLAVE_BSPSetLed(uint8_t num, uint8_t value)
  602. {
  603.     static uint8_t last, last_flag;
  604.     if (num / 8)
  605.     {
  606.         num = 8;
  607.         last++;
  608.     }
  609.     else
  610.     {
  611.         num %= 8;
  612.         last_flag = 1;
  613.     }
  614.     for (uint16_t i = 0; i < num; i++)
  615.     {
  616.         if (last_flag == 0)
  617.         {
  618.             test_led[i+((last - 1) * 8)] = (value & (0x01 << i)) >> i;
  619.         }
  620.         if (last_flag == 1)
  621.         {
  622.             test_led[i + (last * 8)] = (value & (0x01 << i)) >> i;
  623.         }
  624.         
  625.     }
  626.    
  627.     if (last_flag)
  628.     {
  629.         last_flag = 0;
  630.         last = 0;
  631.     }
  632.     return 0;
  633. }

  634. //写多个线圈功能码 0FH
  635. static void MODBUS_SLAVE_0FH(void)
  636. {
  637.     uint16_t reg_num;
  638.     uint8_t byte_num;
  639.     uint8_t i;
  640.     uint16_t value;

  641.     modbus_slave.RTU_response_code = RSP_OK;

  642.     if (modbus_slave.recv_len < 10)
  643.     {
  644.         modbus_slave.RTU_response_code = RSP_ERR_VALUE;                        /* 数据值域错误 */
  645.         goto err_ret;
  646.     }

  647.     reg_num = MODBUS_SLAVE_BEBufToUint16(&modbus_slave.rx_buf[4]);                /* 寄存器个数 */
  648.     byte_num = modbus_slave.rx_buf[6];                                        /* 后面的数据体字节数 */

  649.     //判断寄存器数量与字节数是否对应
  650.     if (reg_num % 8)//寄存器数量不是整数字节,需要+1之后比较
  651.     {
  652.         if (byte_num != ((reg_num / 8) + 1))
  653.         {
  654.             ;
  655.         }
  656.     }
  657.     else//寄存器数量是整数字节直接进行比较
  658.     {
  659.         if (byte_num != (reg_num / 8))
  660.         {
  661.             ;
  662.         }
  663.     }
  664.    
  665.     for (i = 0; i < byte_num; i++)
  666.     {
  667.         value = modbus_slave.rx_buf[7 + i];/* 寄存器值 */
  668.         MODBUS_SLAVE_BSPSetLed(reg_num, value);
  669.         reg_num -= 8;
  670.     }

  671. err_ret:
  672.     if (modbus_slave.RTU_response_code == RSP_OK)   /* 正确应答 */
  673.     {
  674.         //此命令码应答报文与请求报文一致
  675.         memcpy(modbus_slave.tx_buf, modbus_slave.rx_buf, 6);//不复制crc位
  676.         modbus_slave.send_len = 6;//不复制crc位
  677.         MODBUS_SLAVE_SendWithCRC(modbus_slave.tx_buf, modbus_slave.send_len);
  678.     }
  679.     else
  680.     {
  681.         MODBUS_SLAVE_SendAckErr(modbus_slave.RTU_response_code);    /* 告诉主机命令错误 */
  682.     }
  683. }

  1. #ifndef  _MODBUS_SLAVE_H_
  2. #define  _MODBUS_SLAVE_H_

  3. /************************************************************************/
  4. /*  “文件包含”处理                                                       */
  5. /************************************************************************/
  6. #include "data_types.h"


  7. #ifdef __cplusplus
  8. extern "C" {
  9. #endif

  10. /************************************************************************/
  11. /*  宏定义                                                               */
  12. /************************************************************************/
  13. #define SADDR485            (1)     // 从机地址
  14. #define RX_LEN_MAX          (255)   // 报文接收缓冲区大小
  15. #define TX_LEN_MAX          (255)   // 报文发送缓冲区大小

  16. /* 01H 读强制单线圈 */
  17. /* 05H 写强制单线圈 */
  18. #define REG_D01     0x1000
  19. #define REG_D02     0x1002
  20. #define REG_D03     0x1003
  21. #define REG_D04     0x1004
  22. #define REG_DXX     REG_D04

  23. /* 02H 读取输入状态 */
  24. #define REG_T01     0x2001
  25. #define REG_T02     0x2002
  26. #define REG_T03     0x2003
  27. #define REG_TXX     REG_T03

  28. /* 03H 读保持寄存器 */
  29. /* 06H 写保持寄存器 */
  30. /* 10H 写多个保存寄存器 */
  31. #define SLAVE_REG_P01       0x3001
  32. #define SLAVE_REG_P02       0x3002

  33. /* 04H 读取输入寄存器(模拟信号) */
  34. #define REG_A01     0x4001
  35. #define REG_AXX     REG_A01


  36. /* RTU 应答代码 */
  37. #define RSP_OK              (0)     // 成功
  38. #define RSP_ERR_CMD         (0x01)  // 不支持的功能码
  39. #define RSP_ERR_REG_ADDR    (0x02)  // 寄存器地址错误
  40. #define RSP_ERR_VALUE       (0x03)  // 数据值域错误
  41. #define RSP_ERR_WRITE       (0x04)  // 写入失败
  42.      
  43. /************************************************************************/
  44. /*  类型定义                                                             */
  45. /************************************************************************/
  46. typedef struct
  47. {
  48.     uBit8 rx_buf[RX_LEN_MAX];       // 报文接收处理缓冲区
  49.     uBit8 tx_buf[TX_LEN_MAX];       // 报文发送处理缓冲区
  50.     uBit16 curr_dma_cnt;            // 表示当前DMA传输计数器中的值
  51.     uBit16 last_dma_cnt;            // 表示上次DMA传输计数器中的值
  52.     uBit16 recv_len;                // 接收的字节数
  53.     uBit16 send_len;                // 发送的字节数
  54.     uBit8 RTU_response_code;        // RTU应答码
  55. }Modbus_Slave_t;



  56. /************************************************************************/
  57. /*  全局变量声明                                                         */
  58. /************************************************************************/
  59. extern Modbus_Slave_t modbus_slave;


  60. /************************************************************************/
  61. /*  函数声明                                                             */
  62. /************************************************************************/
  63. void MODBUS_SLAVE_Pool(void);


  64. #ifdef __cplusplus
  65. }
  66. #endif

  67. #endif

七、效果展示(由于篇幅限制只展示部分功能码)
80246676c1dc0cd658.jpg
01功能码:
12123676c1b5d34dcc.png

05功能码:
99249676c1c1208aa4.png

0F功能码:
38490676c1f851b6f5.png



95765676c17285eb78.png
80885676c1b551b1de.png

AT32L021C8T7_Modbus_slave .zip

991.58 KB, 下载次数: 25

源码

打赏榜单

ArterySW 打赏了 10.00 元 2025-01-07
理由:作品优秀

saservice 发表于 2025-1-3 20:59 | 显示全部楼层
详细介绍了如何在AT-START-L021开发板上实现Modbus RTU从站的功能。
robertesth 发表于 2025-1-3 21:50 | 显示全部楼层
提供的实现方案具有很强的实用性,可以直接应用于实际项目中。
nomomy 发表于 2025-1-3 23:21 | 显示全部楼层
在软件实现方面,详细介绍了 Modbus RTU 协议的具体内容,包括数据帧格式、CRC 校验算法等。作者不仅讲解了如何编写代码实现 Modbus RTU 从站功能,还给出了代码示例,使读者能够更好地理解和应用。
burgessmaggie 发表于 2025-1-4 02:26 | 显示全部楼层
先介绍了Modbus RTU协议的背景和基本概念,然后详细阐述了AT32L021微控制器的特点和优势,最后给出了具体的实现步骤和代码示例。这种结构有助于读者更好地理解和把握整个实现过程。
benjaminka 发表于 2025-1-4 07:22 | 显示全部楼层
描述了Modbus RTU协议的原理,以及如何在AT-START-L021开发板上实现从站功能。
mikewalpole 发表于 2025-1-5 09:41 | 显示全部楼层
Modbus RTU协议的实现,这是一个在工业自动化领域广泛使用的通信协议。
maqianqu 发表于 2025-1-5 10:40 | 显示全部楼层
描述了 Modbus RTU 从站硬件连接的各个方面,包括通信接口的选择(如 RS485 接口)以及相关引脚的连接方式。例如,明确指出了 AT32L021 的串口引脚与 RS485 芯片的连接方式,这对于实际硬件搭建非常关键。
51xlf 发表于 2025-1-5 11:37 | 显示全部楼层
考虑创建一个系列教程,涵盖Modbus RTU从站的不同方面,如性能优化、安全性等。
nomomy 发表于 2025-1-5 12:05 | 显示全部楼层
若包含相关的代码示例,无论是简单的配置代码还是完整的通信处理代码,都能极大地增强帖子的实用性。代码示例可以作为开发者快速上手和调试的参考,减少开发过程中的困惑。
updownq 发表于 2025-1-5 13:50 | 显示全部楼层
对于Modbus RTU协议的帧格式、地址域、功能码等关键概念进行了详细的解释和说明,同时对于实现过程中可能遇到的常见问题也给出了相应的解决方案。
sdCAD 发表于 2025-1-5 14:40 | 显示全部楼层
对于理解Modbus RTU从站的实现非常有帮助。
maudlu 发表于 2025-1-5 16:21 | 显示全部楼层
包括硬件连接、软件配置、代码示例等。
backlugin 发表于 2025-1-5 16:45 | 显示全部楼层
列出了实现的步骤,例如从硬件连接(如串口引脚的配置、与主站的电气连接等)到软件编程(包括初始化串口、设置Modbus从站相关参数如寄存器地址映射、功能码处理等)的每一个环节,这将是非常有价值的。这种详细程度有助于不同水平的开发者理解和操作。
burgessmaggie 发表于 2025-1-5 17:08 | 显示全部楼层
从Modbus RTU协议的基本概念讲起,逐步深入到从站的具体实现,逻辑清晰,易于理解。
usysm 发表于 2025-1-5 17:34 | 显示全部楼层
对 Modbus RTU 从站的功能实现进行了全面的阐述,包括数据的读写操作、异常处理等。例如,通过对 Modbus 寄存器的操作实现了对不同类型数据的读写功能,并且在遇到异常情况时能够及时处理。
ingramward 发表于 2025-1-7 13:33 | 显示全部楼层
提供了完整的代码示例,包括初始化、数据接收和处理、响应发送等关键部分。
beacherblack 发表于 2025-1-7 14:01 | 显示全部楼层
一个很不错的关于AT - START - L021设备Modbus RTU从站实现的参考资料。
qiufengsd 发表于 2025-1-7 15:22 | 显示全部楼层
对于了解Modbus RTU协议和AT32L021微控制器的应用具有较高的参考价值,同时也为读者提供了具体的实现步骤和代码示例
loutin 发表于 2025-1-7 19:22 | 显示全部楼层
使用AT32L021实现Modbus RTU从站功能的过程,涵盖了Modbus RTU协议的基本原理、AT32L021微控制器的相关配置以及具体的实现步骤。内容具有一定的深度和专业性,对于了解Modbus RTU协议和AT32L021微控制器的应用具有一定的参考价值。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2

主题

2

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部