[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网络实时接收主站命令,回传数据。
刚刚我们讲了小模块项目的基本需求,下面我们看看硬件设计吧!
一、硬件设计
- 主控芯片极海APM32F103C8
主控芯片极海APM32F103由于关键技术参数与被替代芯片相同,所以,我们硬件设计不需要更新!嗯,仅需要变更BOM即可。为了方便后续的说明,我们再来详细展示一下硬件原理图与引脚分配的情况。
- SWD与调试串口
极海APM32F103支持Jtag模式与swd模式,但考虑方便度,我们使用SWD模式,即简简单单的二线方式。在jtag调试10pin座上,我们还连接了调试串口方便我们调试与日志输出。调试串口我们使用Usart3外设,波特率使用115200bps。
- RS485连接
我们本次采用自动换向的方案,并使用极海APM32F103的Usart1串口。RS485通讯串口我们使用了9600bps的波特率。
- 干节点
我们采用ULN2003驱动电磁继电器的方案。不过,对于节点数,我们本次限于原型评估板的成本仅使用了2个电磁继电器。小伙伴们可以根据自己的项目需要自行添加干节点的数量。
- PWM输出
我们使用极海APM32F103的Timer1产生互补PWM波,使用Timer3产品PWM波。其参数关联到Modbus的保持寄存器。
- 附属设计
我们还添加了干节点的状态指示灯,RTC等辅助功能。电源也采用了LDO,AMS1117-3.3的设计。
- 硬件设计3D效果图
- 硬件成品图
二、软件架构
本次我们主要采取了“前-后台”的设计实现,没有使用RTOS的主要原因还是维护更方便一些。不过,对于72MHz主频的极海APM32F103来说,运行RTOS是完全胜任的。
- Modbus-RTU协议
Modbus协议在工业控制领域,对于工程师来说,那可是家喻户晓的协议,一方面是其由德国西门子主导产推广应用;另一方面由于其易于实现,易于理解,从8位单片机到32位MCU,再到嵌入式Linux都有其大显身手的地方。
Modbus协议又分为三个主要实现模式:Modbus-RTU,Modbus-ASCII和Modbus-TCP。对于我们今天的主角极海APM32F103来说,Modbus-RTU最适合不过了。接下来,我们先看看我们实现的Modbus-RTU协议的状态机如何在APM32F103上跳转越来的。
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状态机,当本次传输未完成下,直接丢掉数据 ,不再接收数据并处理,从而优化实现过程与效率。
- 软件分层设计
我们的代码架构如下图所示:
我们在极海APM32F103中因为其资源丰富,所以,我们可以不必过于苛刻的考虑SRAM和Flash。所以,我设计了接收与发送的buf_len=256字节,并且设计了双memory的隔离数据分层。Modbus协议层的寄存器数据进行缓存处理,保证通讯的实时性,与独立性,尽量不与App程序耦合。而在应用层(App),我们根据具体应用来存储数据,可保留App层的数据结构通过交换函数周期调用以保持两者的数值一致。我们软件架构框图也可以看出我们的代码API接口要适配RTU,ASCII与TCP三种模式。而对于功能码的支持,我们支持0x03, 0x04, 0x01, 0x05, 0x10等功能码。具体功能码的含义,限于篇幅我就不在这里展开说明了。
在代码实现上,极海官方提供了完善的示例代码与标准库API函数。这里还是强烈建议大家开发前详细阅读用户手册,即便多数情况下,直接调用极海官方的标准库即可。
三、代码实现
下面我就依次介绍一下我的项目代码实现:
- 系统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();
}
- 调试串口配置
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);
}
- 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);
}
- 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];
};
-
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寄存器表
- 保持寄存器列表
保持寄存器主要为配置PWM波输出的参数:
- 线圈寄存器
线圈寄存器主要控制PWM的开关与电磁继电器的开关。
五、改进与总结
电磁继电器强电端未做电气间隙
本次原型开发的时候在电磁继电器下方也做了敷铜,这个是错误的。没有做好电气隔离,所以我的原型开发板上电磁继电器也只能控制12v的电压电源。
未添加GND探钩
自己开发的弱点就在于此,没有人来做检查。再加上自己的主线也不在PCB上,所以……,这导致我在做PWM波的测试时,使用示波器没有地线可以夹,特别不方便。
未添加隔离电源
我们使用了RS485网络,也添加了控制强电的电磁继电器,但我在原型开发板上未使用外置12v电源,更未添加隔离电源,以防止强电电磁干扰。
总结
我主要做的是APM32F103的芯片国产化替代方案,所以大家在自己的应用上面要做足电气隔离,保证应用的稳定性,可靠性。
在本次原型开发过程中,我们充分验证了APM32F103C8可以Pin2Pin替代原芯片。在软件代码的升级过程中,对驱动层代码的重新编写也没有遇到问题,学习成本与调试成本非常低。
到这里基于极海APM32F103的远程IO终端的国产替代化方案的验证也算是圆满完成了。
在本次原型开发板上,我还添加USB,CAN与I2C,SPI等外设与模拟引脚的引出,我将继续为大家分享基于国产芯片极海APM32F103的项目实战分享。