打印
[APM32F1]

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

[复制链接]
130|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

[i=s] 本帖最后由 jobszheng 于 2024-12-29 21:28 编辑 [/i]<br /> <br />

[实战分享]基于极海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

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

  1. SWD与调试串口

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

APM32F103_RemoteIO_01.png

  1. RS485连接

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

APM32F103_RemoteIO_03.png

  1. 干节点

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

APM32F103_RemoteIO_04.png

  1. PWM输出

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

  1. 附属设计

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

APM32F103_RemoteIO_05.png

  1. 硬件设计3D效果图

3D_REMOTE_IO_MODULE_2024-12-28.png

  1. 硬件成品图

APM32F103_hw_02.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上跳转越来的。

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状态机,当本次传输未完成下,直接丢掉数据 ,不再接收数据并处理,从而优化实现过程与效率。

  1. 软件分层设计

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

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配置
/** 
 * @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();
}
  1. 调试串口配置
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);
}
  1. RS485串口配置
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[0]);
    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);
}
  1. Modbus-RTU状态机定义
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[MB_BUF_SIZE];
    uint8_t tx_buf[MB_BUF_SIZE];
    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[1];
};
  1. PWM波输出配置

    /**
     * @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

  1. 线圈寄存器

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

线圈寄存器表.png

五、改进与总结

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

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

未添加GND探钩

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

未添加隔离电源

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

总结

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

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

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

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

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:嵌入式技术专家
简介:热爱开源,乐于分享。在嵌入式技术领域里面,主攻通讯协议,Modbus,TCP/IP以及虚拟化和RTOS

18

主题

410

帖子

2

粉丝