jobszheng 发表于 2024-12-29 21:21

[实战分享]基于极海APM32F103的远程IO终端

本帖最后由 jobszheng 于 2024-12-29 21:28 编辑

# [实战分享]基于极海APM32F103的远程IO终端

  远程IO模块应用非常广泛,比如远程控制电机启动与停止,采集远端的温度数据,湿度数据等,返回远程设备的工作状态等等。在当前国际芯片的大形势下,国产化芯片的替代方案势在必行,今天给大家带来的就是我们项目组的《基于极海APM32F103的远程IO终端》实现方案,供大家参考。

  我们最初在设计上也是使用了某欧洲品牌的Cortex-M3系列的MCU,由于国产化率的原因,我们也面临了必须选择国内品牌主控芯片的境地。在更换主控任务上,牵扯的人员还是蛮多的,硬件、软件、测试等。不过,极海的APM32F103在兼容性上面已经完全适配,而且是Pin2Pin兼容——这样,我们的PCB设计人员就无须再投入人力成本了。软件功能与实现上,也完全兼容,我们的嵌入式工程师的学习成本再次降低几个数量级。下面,我就详细介绍一下我们的小模块产品——远程IO终端。

  我们使用极海公司Cortex-M3系列产品APM32F103C8型号为主控芯片。APM32F103系列MCU最高支持96MHz的主频,3个Usart,2个I2C,4个Timer,37个GPIO,以及2个看门狗。——嘿嘿,说实话,因为是全兼容,要不是写帖子,我也没有再关心具体的参数。虽然极海APM32F103支持96MHz的主频,但我们仍然为了产品线的兼容,保持了72MHz的主频应用。

  我们的方案中,我们使用Usart1做为RS485通讯的外设接口,使用Timer1和Timer3来产生互补PWM信号与PWM信号。对于电磁继电器与LED灯则直接使用了GPIO来控制,嗯,还有ULN2003这个驱动芯片。在软件上面,我们部署了Modbus-RTU协议的从站,通过RS485网络实时接收主站命令,回传数据。

  刚刚我们讲了小模块项目的基本需求,下面我们看看硬件设计吧!

## 一、硬件设计

1. 主控芯片极海APM32F103C8

!(data/attachment/forum/202412/29/205730uo1izkii16z21zm1.png "APM32F103_RemoteIO_02.png")
  主控芯片极海APM32F103由于关键技术参数与被替代芯片相同,所以,我们硬件设计不需要更新!嗯,仅需要变更BOM即可。为了方便后续的说明,我们再来详细展示一下硬件原理图与引脚分配的情况。

2. SWD与调试串口

  极海APM32F103支持Jtag模式与swd模式,但考虑方便度,我们使用SWD模式,即简简单单的二线方式。在jtag调试10pin座上,我们还连接了调试串口方便我们调试与日志输出。调试串口我们使用Usart3外设,波特率使用115200bps。

!(data/attachment/forum/202412/29/205821bp4049pj8b911r40.png "APM32F103_RemoteIO_01.png")

3. RS485连接

  我们本次采用自动换向的方案,并使用极海APM32F103的Usart1串口。RS485通讯串口我们使用了9600bps的波特率。

!(data/attachment/forum/202412/29/205904wq9pvu2iv33rievr.png "APM32F103_RemoteIO_03.png")

4. 干节点

  我们采用ULN2003驱动电磁继电器的方案。不过,对于节点数,我们本次限于原型评估板的成本仅使用了2个电磁继电器。小伙伴们可以根据自己的项目需要自行添加干节点的数量。

!(data/attachment/forum/202412/29/205937mpslssca6ijl1ssz.png "APM32F103_RemoteIO_04.png")

5. PWM输出

  我们使用极海APM32F103的Timer1产生互补PWM波,使用Timer3产品PWM波。其参数关联到Modbus的保持寄存器。

6. 附属设计

  我们还添加了干节点的状态指示灯,RTC等辅助功能。电源也采用了LDO,AMS1117-3.3的设计。

!(data/attachment/forum/202412/29/210034k8glflsg8z6fzp7p.png "APM32F103_RemoteIO_05.png")

7. 硬件设计3D效果图

!(data/attachment/forum/202412/29/210105b2kkpqzqzt65c8kh.png "3D_REMOTE_IO_MODULE_2024-12-28.png")

8. 硬件成品图

!(data/attachment/forum/202412/29/210150ijobx6ndjjdstntx.jpg "APM32F103_hw_02.jpg")

!(data/attachment/forum/202412/29/210151zq297m7y5m257oya.jpg "APM32F103_hw_01.jpg")

## 二、软件架构

  本次我们主要采取了“前-后台”的设计实现,没有使用RTOS的主要原因还是维护更方便一些。不过,对于72MHz主频的极海APM32F103来说,运行RTOS是完全胜任的。

1. Modbus-RTU协议

  Modbus协议在工业控制领域,对于工程师来说,那可是家喻户晓的协议,一方面是其由德国西门子主导产推广应用;另一方面由于其易于实现,易于理解,从8位单片机到32位MCU,再到嵌入式Linux都有其大显身手的地方。

  Modbus协议又分为三个主要实现模式:Modbus-RTU,Modbus-ASCII和Modbus-TCP。对于我们今天的主角极海APM32F103来说,Modbus-RTU最适合不过了。接下来,我们先看看我们实现的Modbus-RTU协议的状态机如何在APM32F103上跳转越来的。

!(data/attachment/forum/202412/29/210236h6z8zlexp4x65e2i.png "modbus-modbus.png")

1.1 帧尾超时判断

  对于Modbus来说,每发送的一组数据称为一帧数据,对于Modbus-RTU来说,帧尾的判断方式是RS485总线空闲T3.5-T4.0的字节传输空闲时间。本次我们使用的9600bps的波特率。因此,T3.5-T4.0时间,我们就近似取值4ms,并由systick的1ms时基来管理与实现。

1.2 帧完整性CRC16

  对于Modbus-RTU来说,每帧数据必须经过CRC16来实现完整性校验,从而保证每帧传输的命令,配置参数不会出现错误。我在极海APM32F103中使用的是查表方式实现的CRC16计算。毕竟极海APM32F103的Flash空间还是蛮大的,换一些算力出来还是很值的。

1.3 主从模式下,从机地址快速识别

  Modbus协议是典型的主-从应答通讯架构。在Modbus协议的组网中,仅支持单主多从的模式,而主机与从机的通讯身份识别是通过首字节数据,即首字节为从机地址,正因如此,我也在接收中断中添加首字节的判断功能,当非本从机地址数据帧时,直接进入MB\_abandon状态机,当本次传输未完成下,直接丢掉数据 ,不再接收数据并处理,从而优化实现过程与效率。

2. 软件分层设计

  我们的代码架构如下图所示:

!(data/attachment/forum/202412/29/210309etsplts69vxzlsv8.png "modbus-arch.png")

  我们在极海APM32F103中因为其资源丰富,所以,我们可以不必过于苛刻的考虑SRAM和Flash。所以,我设计了接收与发送的buf\_len=256字节,并且设计了双memory的隔离数据分层。Modbus协议层的寄存器数据进行缓存处理,保证通讯的实时性,与独立性,尽量不与App程序耦合。而在应用层(App),我们根据具体应用来存储数据,可保留App层的数据结构通过交换函数周期调用以保持两者的数值一致。我们软件架构框图也可以看出我们的代码API接口要适配RTU,ASCII与TCP三种模式。而对于功能码的支持,我们支持0x03, 0x04, 0x01, 0x05, 0x10等功能码。具体功能码的含义,限于篇幅我就不在这里展开说明了。

  在代码实现上,极海官方提供了完善的示例代码与标准库API函数。这里还是强烈建议大家开发前详细阅读用户手册,即便多数情况下,直接调用极海官方的标准库即可。

## 三、代码实现

  下面我就依次介绍一下我的项目代码实现:

1. 系统HSE配置

```c
/**
* @brief:set System Main Freqence 72MHz and enable CSS
*
* @param:
* @return:
* @note:   
*/
void hal_sysclock_set(void)
{
    RCM_Reset();

    RCM_ConfigHSE(RCM_HSE_OPEN);

    if (RCM_WaitHSEReady() == SUCCESS)
    {
      FMC_EnablePrefetchBuffer();
      FMC_ConfigLatency(FMC_LATENCY_2);

      RCM_ConfigAHB(RCM_AHB_DIV_1);
      RCM_ConfigAPB2(RCM_APB_DIV_1);
      RCM_ConfigAPB1(RCM_APB_DIV_2);

      RCM_ConfigPLL(RCM_PLLSEL_HSE, RCM_PLLMF_9);
      RCM_EnablePLL();
      while (RCM_ReadStatusFlag(RCM_FLAG_PLLRDY) == RESET)
            ;

      RCM_ConfigSYSCLK(RCM_SYSCLK_SEL_PLL);
      while (RCM_ReadSYSCLKSource() != RCM_SYSCLK_SEL_PLL)
            ;
    }
    else
    {
      while (1)
            ;
    }

    RCM_EnableCSS();
    SystemCoreClockUpdate();
}
```

2. 调试串口配置

```c
void dbg_uart_init(uint32_t band)
{
    GPIO_Config_T gpio_inst;
    USART_Config_T uart_inst;
    // RCM_EnableAPB2PeriphClock();
    RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_USART3);
    gpio_inst.mode = GPIO_MODE_AF_PP;
    gpio_inst.pin = DBG_UART_TX_PIN;
    gpio_inst.speed = GPIO_SPEED_2MHz;
    GPIO_Config(DBG_UART_TX_PORT, &gpio_inst);

    gpio_inst.mode = GPIO_MODE_IN_FLOATING;
    gpio_inst.pin = DBG_UART_RX_PIN;
    GPIO_Config(DBG_UART_RX_PORT, &gpio_inst);

    uart_inst.baudRate = band;
    uart_inst.hardwareFlow = USART_HARDWARE_FLOW_NONE;
    uart_inst.mode = USART_MODE_TX;
    uart_inst.parity = USART_PARITY_NONE;
    uart_inst.stopBits = USART_STOP_BIT_1;
    uart_inst.wordLength = USART_WORD_LEN_8B;
    USART_Config(DBG_UART, &uart_inst);

    USART_Enable(DBG_UART);
}
```

3. RS485串口配置

```c
void rs485_uart_init(uint32_t band)
{
    GPIO_Config_T gpio_inst;
    USART_Config_T uart_inst;
    DMA_Config_T dma_inst;

    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_USART1);
    RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);

    gpio_inst.mode = GPIO_MODE_AF_PP;
    gpio_inst.pin = RS485_UART_TX_PIN;
    gpio_inst.speed = GPIO_SPEED_2MHz;
    GPIO_Config(RS485_UART_TX_PORT, &gpio_inst);

    gpio_inst.mode = GPIO_MODE_IN_FLOATING;
    gpio_inst.pin = RS485_UART_RX_PIN;
    GPIO_Config(RS485_UART_RX_PORT, &gpio_inst);

    uart_inst.baudRate = band;
    uart_inst.hardwareFlow = USART_HARDWARE_FLOW_NONE;
    uart_inst.mode = USART_MODE_TX_RX;
    uart_inst.parity = USART_PARITY_NONE;
    uart_inst.stopBits = USART_STOP_BIT_1;
    uart_inst.wordLength = USART_WORD_LEN_8B;
    USART_Config(RS485_UART, &uart_inst);

    dma_inst.peripheralBaseAddr = (uint32_t)(&USART1->DATA);
    dma_inst.memoryBaseAddr = (uint32_t)(&mb_inst.tx_buf);
    dma_inst.dir = DMA_DIR_PERIPHERAL_DST;
    dma_inst.bufferSize = 0;
    dma_inst.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
    dma_inst.memoryInc = DMA_MEMORY_INC_ENABLE;
    dma_inst.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
    dma_inst.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE;
    dma_inst.loopMode = DMA_MODE_NORMAL;
    dma_inst.priority = DMA_PRIORITY_MEDIUM;
    dma_inst.M2M = DMA_M2MEN_DISABLE;

    DMA_Config(DMA1_Channel4, &dma_inst);

#if 0
    /* Move to MB_Poll() */
    USART_EnableInterrupt(RS485_UART, USART_INT_RXBNE);

#endif
    USART_EnableDMA(USART1, USART_DMA_TX);
    USART_Enable(RS485_UART);
}
```

4. Modbus-RTU状态机定义

```c
enum mb_state_e
{
    mb_state_initial = 0,
    mb_state_listening,
    mb_state_frame_received,
    mb_state_execute,
    mb_state_frame_send,
    mb_state_abandon,
    mb_state_ready,
};

struct reg_arrange_class
{
    uint16_t *base;
    uint16_t start;
    uint16_t end;
};

struct mb_class
{
    struct reg_arrange_class hold_regs_zone;
    struct reg_arrange_class input_regs_zone;
    struct reg_arrange_class coils_zone;
    uint8_t rx_buf;
    uint8_t tx_buf;
    uint16_t rx_len;
    uint16_t tx_len;
    uint8_t slave_addr;
    uint8_t tick;
    uint8_t flag;
    enum mb_state_e state;
    // uint8_t reserved;
};
```

5. PWM波输出配置

   ```c
   /**
    * @brief:initial PWM output
    *
    * @param:CH0 => TIM1_CH1N    (PA7)
    *          CH1 => TIM3_CH3   (PB0)
    *          CH2 => TIM1_CH1   (PA8)
    *          CH3 => TIM3_CH4   (PB1)
    * @return:
    * @note:
    */
   void hal_pwm_channel_init(void)
   {
       GPIO_Config_T gpio_inst;
       TMR_BaseConfig_T timer_inst;
       TMR_OCConfig_T timer_oc_inst;

       RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_TMR1);
       RCM_EnableAPB1PeriphClock(RCM_APB1_PERIPH_TMR3);

       gpio_inst.pin = GPIO_PIN_8;
       gpio_inst.mode = GPIO_MODE_AF_PP;
       gpio_inst.speed = GPIO_SPEED_50MHz;
       GPIO_Config(GPIOA, &gpio_inst);

       timer_inst.clockDivision = TMR_CLOCK_DIV_1;
       timer_inst.countMode = TMR_COUNTER_MODE_UP;
       timer_inst.division = 71;
       timer_inst.period = 999;
       TMR_ConfigTimeBase(TMR1, &timer_inst);

       timer_oc_inst.idleState = TMR_OC_IDLE_STATE_RESET;
       timer_oc_inst.mode = TMR_OC_MODE_PWM1;
       timer_oc_inst.nIdleState = TMR_OC_NIDLE_STATE_RESET;
       timer_oc_inst.nPolarity = TMR_OC_NPOLARITY_HIGH;
       timer_oc_inst.outputNState = TMR_OC_NSTATE_ENABLE;
       timer_oc_inst.outputState = TMR_OC_STATE_ENABLE;
       timer_oc_inst.polarity = TMR_OC_POLARITY_HIGH;
       timer_oc_inst.pulse = 300;
       TMR_ConfigOC1(TMR1, &timer_oc_inst);

   #if 0
       /* move to PWM output config */
       TMR_ConfigOC1Preload(TMR1, TMR_OC_PRELOAD_ENABLE);
   #endif
       TMR_ConfigOC1Preload(TMR1, TMR_OC_PRELOAD_DISABLE);
       TMR_EnableAutoReload(TMR1);
       TMR_Enable(TMR1);
       TMR_EnablePWMOutputs(TMR1);
   }

   /**
    * @brief:config PWM channel parameter
    *
    * @param:freq: unit is KHz
    *          duty: 0-100
    *          stat: enable or disable
    * @return:
    * @note:
    */
   int hal_pwm_channel_2_config(uint16_t freq, uint16_t duty, uint8_t stat)
   {
       int ret = 0;
       struct pwm_cfg_class cfg_inst;
       if (stat == DISABLE)
       {
         TMR_ConfigOC1Preload(TMR1, TMR_OC_PRELOAD_DISABLE);
       }
       else
       {
         ret = pwm_parameter_cale(&cfg_inst, freq, duty);
         if (ret != 0)
         {
               return (ret);
         }
         TMR_ConfigPrescaler(TMR1, cfg_inst.div, TMR_PSC_RELOAD_UPDATE);
         TMR_ConfigAutoreload(TMR1, cfg_inst.cnt);
         TMR_ConfigCompare1(TMR1, cfg_inst.cc);
         TMR_ConfigOC1Preload(TMR1, TMR_OC_PRELOAD_ENABLE);
       }
       return (ret);
   }
   ```

## 四、Modbus寄存器表

1. 保持寄存器列表

  保持寄存器主要为配置PWM波输出的参数:

![保持寄存器表.png](data/attachment/forum/202412/29/211141rn4zy1cs4rcy5od4.png "保持寄存器表.png")

2. 线圈寄存器

  线圈寄存器主要控制PWM的开关与电磁继电器的开关。

![线圈寄存器表.png](data/attachment/forum/202412/29/211238w1ou93h44y1od5yn.png "线圈寄存器表.png")

## 五、改进与总结

**电磁继电器强电端未做电气间隙**

  本次原型开发的时候在电磁继电器下方也做了敷铜,这个是错误的。没有做好电气隔离,所以我的原型开发板上电磁继电器也只能控制12v的电压电源。

**未添加GND探钩**

  自己开发的弱点就在于此,没有人来做检查。再加上自己的主线也不在PCB上,所以……,这导致我在做PWM波的测试时,使用示波器没有地线可以夹,特别不方便。

**未添加隔离电源**

  我们使用了RS485网络,也添加了控制强电的电磁继电器,但我在原型开发板上未使用外置12v电源,更未添加隔离电源,以防止强电电磁干扰。

**总结**

  我主要做的是APM32F103的芯片国产化替代方案,所以大家在自己的应用上面要做足电气隔离,保证应用的稳定性,可靠性。

  在本次原型开发过程中,我们充分验证了APM32F103C8可以Pin2Pin替代原芯片。在软件代码的升级过程中,对驱动层代码的重新编写也没有遇到问题,学习成本与调试成本非常低。

  到这里基于极海APM32F103的远程IO终端的国产替代化方案的验证也算是圆满完成了。

  在本次原型开发板上,我还添加USB,CAN与I2C,SPI等外设与模拟引脚的引出,我将继续为大家分享基于国产芯片极海APM32F103的项目实战分享。

17101797897 发表于 2025-1-10 15:14

本帖最后由 17101797897 于 2025-1-10 15:17 编辑

支持一下
页: [1]
查看完整版本: [实战分享]基于极海APM32F103的远程IO终端