小华HC32F460KETA 移植FreeModbus

本次移植主要参考了如下内容:

https://blog.csdn.net/qq153471503/article/details/104840279

https://blog.csdn.net/childbor/article/details/124690719?spm=1001.2014.3001.5502

https://blog.csdn.net/childbor/article/details/123848831


由于工作原因接触了这款芯片,为了通信方便查阅了几种modbus开源的资料,freemodbus资料多一些,于是开始动手分别在 HC32F460KETA STM32F030C8T6 做移植。

1.创建小华项目

工具及资料下载

这里使用华大的工具生成工程项目,选择对号的芯片,这里我顺便生成了串口2及一路GPIO。

小华HC32F460KETA 移植FreeModbus_第1张图片

 值得注意的是:TIMEOUT中断这个功能,我没有测试成功,所以项目中仅用了发送数据寄存器空中中断使能,及接收数据寄存器满中断使能;分频选择的DIV64,这里默认是DIV1但是串口不工作,选择了DIV16与DIV64都正常。

小华HC32F460KETA 移植FreeModbus_第2张图片

 初始化定时器:对于不是十分了解freemodbus的伙伴们,最好看下上面链接中的文章,以便了解定时器的作用,避免走弯路。

小华HC32F460KETA 移植FreeModbus_第3张图片

这里使用的时钟是32.768,不分频;还开启了硬件清零事件使能,这个功能理解是匹配中断后,将计数器清零,测试时发现不开启该功能,同时也不手动进行清零,定时器容易重复,所以打开更省事。

时钟配置

小华HC32F460KETA 移植FreeModbus_第4张图片

 这里我什么也没改,其中串口2使用的是PCLK1时钟,timer0使用的是XTAL32

项目生成

小华HC32F460KETA 移植FreeModbus_第5张图片

 这里这个驱动库(DDL)位置是前文中官网下载,配置路径即可

小华HC32F460KETA 移植FreeModbus_第6张图片

 生成代码

2.FreeModbus加入到项目中

freemodbus下载地址:GitHub - cwalter-at/freemodbus: BSD licensed MODBUS RTU/ASCII and TCP slave

小华HC32F460KETA 移植FreeModbus_第7张图片

小华HC32F460KETA 移植FreeModbus_第8张图片

为了方便快速,将以上文件合并到一个modbus文件夹内了

小华HC32F460KETA 移植FreeModbus_第9张图片

KEIL5导入

 其中pack文件在前文提供的官网链接中也是有的,下载安装即可

打开生成好的项目

小华HC32F460KETA 移植FreeModbus_第10张图片

 配置JLINK

小华HC32F460KETA 移植FreeModbus_第11张图片

芯片是M4系列的,这里找不到HC开头的就选M4即可,其他细节就不粘贴图了

小华HC32F460KETA 移植FreeModbus_第12张图片

将之前准备好的modbus文件夹放在本项目MDK目录下,并将目录加入项目

 小华HC32F460KETA 移植FreeModbus_第13张图片

 配置modbus文件夹的头文件

小华HC32F460KETA 移植FreeModbus_第14张图片

 此时编译会出错,因为freemodbus中的断言引用问题,这里暂时去掉断言,在HC生成代码中,看到DDL_ASSERT();,还没有深入研究,网上粗略查了下,说建议用DDL_ASSERT();,暂未深究

小华HC32F460KETA 移植FreeModbus_第15张图片

修改定时器porttimer.c



/* ----------------------- Platform includes --------------------------------*/
#include "main.h"       //引入main.h
#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* INT_SRC_TMR0_1_CMP_B Callback. */
static void INT_SRC_TMR0_1_CMP_B_IrqCallback(void);
/* ----------------------- static functions ---------------------------------*/
static void prvvTIMERExpiredISR( void );

/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
		stc_irq_signin_config_t stcIrq;
		stcIrq.enIntSrc = INT_SRC_TMR0_1_CMP_B;
    stcIrq.enIRQn = INT044_IRQn;
    stcIrq.pfnCallback = &INT_SRC_TMR0_1_CMP_B_IrqCallback;
    (void)INTC_IrqSignIn(&stcIrq);
    NVIC_ClearPendingIRQ(INT044_IRQn);
    NVIC_SetPriority(INT044_IRQn, DDL_IRQ_PRIO_15);
    NVIC_EnableIRQ(INT044_IRQn);
	
	
		 stc_tmr0_init_t stcTmr0Init;

    /* Enable AOS clock */
    FCG_Fcg0PeriphClockCmd(FCG0_PERIPH_AOS, ENABLE);
    /* Timer0 trigger event set */
    AOS_SetTriggerEventSrc(AOS_TMR0, EVT_SRC_PORT_EIRQ0);

    /* Enable timer0_1 clock */
    FCG_Fcg2PeriphClockCmd(FCG2_PERIPH_TMR0_1, ENABLE);

    /************************* Configure TMR0_1_B***************************/
    (void)TMR0_StructInit(&stcTmr0Init);
    stcTmr0Init.u32ClockSrc = TMR0_CLK_SRC_XTAL32;
    stcTmr0Init.u32ClockDiv = TMR0_CLK_DIV1;
    stcTmr0Init.u32Func = TMR0_FUNC_CMP;
    stcTmr0Init.u16CompareValue = 0x0100U;
    (void)TMR0_Init(CM_TMR0_1, TMR0_CH_B, &stcTmr0Init);
    DDL_DelayMS(1U);
    TMR0_HWClearCondCmd(CM_TMR0_1, TMR0_CH_B, ENABLE);
    DDL_DelayMS(1U);
    TMR0_IntCmd(CM_TMR0_1, TMR0_INT_CMP_B, ENABLE);
    DDL_DelayMS(1U);
		
    return TRUE;
}


inline void
vMBPortTimersEnable(  )
{
    /* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
		/* TMR0 start counting */
		TMR0_IntCmd(CM_TMR0_1, TMR0_INT_CMP_B, ENABLE);
		TMR0_SetCountValue(CM_TMR0_1, TMR0_CH_B,0);
    TMR0_Start(CM_TMR0_1, TMR0_CH_B);
}

inline void
vMBPortTimersDisable(  )
{
    /* Disable any pending timers. */
		TMR0_IntCmd(CM_TMR0_1, TMR0_INT_CMP_B, DISABLE);
		TMR0_Stop(CM_TMR0_1, TMR0_CH_B);
}

/* Create an ISR which is called whenever the timer has expired. This function
 * must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
 * the timer has expired.
 */
static void prvvTIMERExpiredISR( void )
{
    ( void )pxMBPortCBTimerExpired(  );
}

/* INT_SRC_TMR0_1_CMP_B Callback. */
static void INT_SRC_TMR0_1_CMP_B_IrqCallback(void)
{
    //add your codes here
		prvvTIMERExpiredISR(  );
}


修改串口portserial.c


#include "main.h"   //引入main.h
#include "port.h"

/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"

/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR( void );
static void prvvUARTRxISR( void );
static void INT_SRC_USART2_RI_IrqCallback(void);
static void INT_SRC_USART2_TI_IrqCallback(void);
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
    /* If xRXEnable enable serial receive interrupts. If xTxENable enable
     * transmitter empty interrupts.
     */
		if(xRxEnable){
			USART_FuncCmd(CM_USART2, (USART_RX | USART_INT_RX), ENABLE);
		}else{
			while(SET == USART_GetStatus(CM_USART2,USART_FLAG_RX_FULL)){};
			USART_FuncCmd(CM_USART2, (USART_RX | USART_INT_RX), DISABLE);
		}
		
		if(xTxEnable){
			
			USART_FuncCmd(CM_USART2, (USART_TX | USART_INT_TX_EMPTY), ENABLE);
		}else{
			while(RESET == USART_GetStatus(CM_USART2,USART_FLAG_TX_CPLT)){};
			USART_FuncCmd(CM_USART2, (USART_TX | USART_INT_TX_EMPTY), DISABLE);
		}
}

BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
	stc_irq_signin_config_t stcIrq;

    stcIrq.enIntSrc = INT_SRC_USART2_RI;
    stcIrq.enIRQn = INT081_IRQn;
    stcIrq.pfnCallback = &INT_SRC_USART2_RI_IrqCallback;
    (void)INTC_IrqSignIn(&stcIrq);
    NVIC_ClearPendingIRQ(INT081_IRQn);
    NVIC_SetPriority(INT081_IRQn, DDL_IRQ_PRIO_15);
    NVIC_EnableIRQ(INT081_IRQn);

    stcIrq.enIntSrc = INT_SRC_USART2_TI;
    stcIrq.enIRQn = INT080_IRQn;
    stcIrq.pfnCallback = &INT_SRC_USART2_TI_IrqCallback;
    (void)INTC_IrqSignIn(&stcIrq);
    NVIC_ClearPendingIRQ(INT080_IRQn);
    NVIC_SetPriority(INT080_IRQn, DDL_IRQ_PRIO_15);
    NVIC_EnableIRQ(INT080_IRQn);
	
	stc_usart_uart_init_t stcUartInit;

    /* Enable USART2 clock */
    FCG_Fcg1PeriphClockCmd(FCG1_PERIPH_USART2, ENABLE);
    /************************* Configure USART2***************************/
    USART_DeInit(CM_USART2);
    (void)USART_UART_StructInit(&stcUartInit);
    stcUartInit.u32ClockSrc = USART_CLK_SRC_INTERNCLK;
    stcUartInit.u32ClockDiv = USART_CLK_DIV64;
    stcUartInit.u32CKOutput = USART_CK_OUTPUT_DISABLE;
    stcUartInit.u32Baudrate = 9600UL;
    stcUartInit.u32DataWidth = USART_DATA_WIDTH_8BIT;
    stcUartInit.u32StopBit = USART_STOPBIT_1BIT;
    stcUartInit.u32Parity = USART_PARITY_NONE;
    stcUartInit.u32OverSampleBit = USART_OVER_SAMPLE_16BIT;
    stcUartInit.u32FirstBit = USART_FIRST_BIT_LSB;
    stcUartInit.u32StartBitPolarity = USART_START_BIT_FALLING;
    stcUartInit.u32HWFlowControl = USART_HW_FLOWCTRL_RTS;
    USART_UART_Init(CM_USART2, &stcUartInit, NULL);
		
    return TRUE;
}

BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
    /* Put a byte in the UARTs transmit buffer. This function is called
     * by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
     * called. */
	USART_WriteData(CM_USART2,ucByte);
    return TRUE;
}

BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
    /* Return the byte in the UARTs receive buffer. This function is called
     * by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
			 返回UARTs接收缓冲区中的字节。这个函数在pxmbframecbbyterreceived()被调用后由协议栈调用。
     */
	*pucByte = USART_ReadData(CM_USART2);
    return TRUE;
}

/* Create an interrupt handler for the transmit buffer empty interrupt
 * (or an equivalent) for your target processor. This function should then
 * call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
 * a new character can be sent. The protocol stack will then call 
 * xMBPortSerialPutByte( ) to send the character.
 */
static void prvvUARTTxReadyISR( void )
{
    pxMBFrameCBTransmitterEmpty(  );
}

/* Create an interrupt handler for the receive interrupt for your target
 * processor. This function should then call pxMBFrameCBByteReceived( ). The
 * protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
 * character.
	 为目标处理器的接收中断创建一个中断处理程序。然后这个函数应该调用pxmbframecbbyterreceived()
   的协议栈将调用xMBPortSerialGetByte()来检索的性格。
 */
static void prvvUARTRxISR( void )
{
		
    pxMBFrameCBByteReceived(  );
}


/* INT_SRC_USART2_RI Callback. */
static void INT_SRC_USART2_RI_IrqCallback(void)
{
    //add your codes here
	prvvUARTRxISR();
}

static void INT_SRC_USART2_TI_IrqCallback(void)
{
    //add your codes here
	prvvUARTTxReadyISR();
}

还需要创建一个port.c文件,这里面主要放的是modbus标准协议返回的内容,是从本文开头第一个博客中抄写的。

#include "mb.h"
#include "mbport.h"


// 十路输入寄存器
#define REG_INPUT_SIZE  10
uint16_t REG_INPUT_BUF[REG_INPUT_SIZE];


// 十路保持寄存器
#define REG_HOLD_SIZE   10
uint16_t REG_HOLD_BUF[REG_HOLD_SIZE];


// 十路线圈
#define REG_COILS_SIZE 10
uint8_t REG_COILS_BUF[REG_COILS_SIZE] = {1, 1, 1, 1, 0, 0, 0, 0, 1, 1};


// 十路离散量
#define REG_DISC_SIZE  10
uint8_t REG_DISC_BUF[REG_DISC_SIZE] = {1,1,1,1,0,0,0,0,1,1};


/// CMD4命令处理回调函数
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
    USHORT usRegIndex = usAddress - 1;

    // 非法检测
    if((usRegIndex + usNRegs) > REG_INPUT_SIZE)
    {
        return MB_ENOREG;
    }

    // 循环读取
    while( usNRegs > 0 )
    {
        *pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] >> 8 );
        *pucRegBuffer++ = ( unsigned char )( REG_INPUT_BUF[usRegIndex] & 0xFF );
        usRegIndex++;
        usNRegs--;
    }

    // 模拟输入寄存器被改变
    for(usRegIndex = 0; usRegIndex < REG_INPUT_SIZE; usRegIndex++)
    {
        REG_INPUT_BUF[usRegIndex]++;
    }

    return MB_ENOERR;
}

/// CMD6、3、16命令处理回调函数
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
    USHORT usRegIndex = usAddress - 1;

    // 非法检测
    if((usRegIndex + usNRegs) > REG_HOLD_SIZE)
    {
        return MB_ENOREG;
    }

    // 写寄存器
    if(eMode == MB_REG_WRITE)
    {
        while( usNRegs > 0 )
        {
            REG_HOLD_BUF[usRegIndex] = (pucRegBuffer[0] << 8) | pucRegBuffer[1];
            pucRegBuffer += 2;
            usRegIndex++;
            usNRegs--;
        }
    }

    // 读寄存器
    else
    {
        while( usNRegs > 0 )
        {
            *pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] >> 8 );
            *pucRegBuffer++ = ( unsigned char )( REG_HOLD_BUF[usRegIndex] & 0xFF );
            usRegIndex++;
            usNRegs--;
        }
    }

    return MB_ENOERR;
}

/// CMD1、5、15命令处理回调函数
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{
    USHORT usRegIndex   = usAddress - 1;
    UCHAR  ucBits       = 0;
    UCHAR  ucState      = 0;
    UCHAR  ucLoops      = 0;

    // 非法检测
    if((usRegIndex + usNCoils) > REG_COILS_SIZE)
    {
        return MB_ENOREG;
    }

    if(eMode == MB_REG_WRITE)
    {
        ucLoops = (usNCoils - 1) / 8 + 1;
        while(ucLoops != 0)
        {
            ucState = *pucRegBuffer++;
            ucBits  = 0;
            while(usNCoils != 0 && ucBits < 8)
            {
                REG_COILS_BUF[usRegIndex++] = (ucState >> ucBits) & 0X01;
                usNCoils--;
                ucBits++;
            }
            ucLoops--;
        }
    }
    else
    {
        ucLoops = (usNCoils - 1) / 8 + 1;
        while(ucLoops != 0)
        {
            ucState = 0;
            ucBits  = 0;
            while(usNCoils != 0 && ucBits < 8)
            {
                if(REG_COILS_BUF[usRegIndex])
                {
                    ucState |= (1 << ucBits);
                }
                usNCoils--;
                usRegIndex++;
                ucBits++;
            }
            *pucRegBuffer++ = ucState;
            ucLoops--;
        }
    }

    return MB_ENOERR;
}

/// CMD2命令处理回调函数
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
    USHORT usRegIndex   = usAddress - 1;
    UCHAR  ucBits       = 0;
    UCHAR  ucState      = 0;
    UCHAR  ucLoops      = 0;

    // 非法检测
    if((usRegIndex + usNDiscrete) > REG_DISC_SIZE)
    {
        return MB_ENOREG;
    }

    ucLoops = (usNDiscrete - 1) / 8 + 1;
    while(ucLoops != 0)
    {
        ucState = 0;
        ucBits  = 0;
        while(usNDiscrete != 0 && ucBits < 8)
        {
            if(REG_DISC_BUF[usRegIndex])
            {
                ucState |= (1 << ucBits);
            }
            usNDiscrete--;
            usRegIndex++;
            ucBits++;
        }
        *pucRegBuffer++ = ucState;
        ucLoops--;
    }

    // 模拟离散量输入被改变
    for(usRegIndex = 0; usRegIndex < REG_DISC_SIZE; usRegIndex++)
    {
        REG_DISC_BUF[usRegIndex] = !REG_DISC_BUF[usRegIndex];
    }

    return MB_ENOERR;
}

最后是main.c的内容,由于工具生成的初始化,都在main.c中,上面两段代码中的初始化就是在main.c中迁移过去的。


#include "main.h"
#include "mb.h"
#include "mbport.h"

/* ----------------------- Defines ------------------------------------------*/

/* ----------------------- Static variables ---------------------------------*/

/*******************************************************************************
 * Global variable definitions (declared in header file with 'extern')
 ******************************************************************************/

/*******************************************************************************
 * Local function prototypes ('static')
 ******************************************************************************/

/*******************************************************************************
 * Local variable definitions ('static')
 ******************************************************************************/

/*******************************************************************************
 * Function implementation - global ('extern') and local ('static')
 ******************************************************************************/
//Clock Config
static void App_ClkCfg(void)
{
    /* Set bus clock div. */
    CLK_SetClockDiv(CLK_BUS_CLK_ALL, (CLK_HCLK_DIV1 | CLK_EXCLK_DIV2 | CLK_PCLK0_DIV1 | CLK_PCLK1_DIV2 | \
                                   CLK_PCLK2_DIV4 | CLK_PCLK3_DIV4 | CLK_PCLK4_DIV2));
    /* sram init include read/write wait cycle setting */
    SRAM_SetWaitCycle(SRAM_SRAM_ALL, SRAM_WAIT_CYCLE1, SRAM_WAIT_CYCLE1);
    SRAM_SetWaitCycle(SRAM_SRAMH, SRAM_WAIT_CYCLE0, SRAM_WAIT_CYCLE0);
    /* flash read wait cycle setting */
    EFM_SetWaitCycle(EFM_WAIT_CYCLE5);
    /* XTAL config */
    stc_clock_xtal_init_t stcXtalInit;
    (void)CLK_XtalStructInit(&stcXtalInit);
    stcXtalInit.u8State = CLK_XTAL_ON;
    stcXtalInit.u8Drv = CLK_XTAL_DRV_HIGH;
    stcXtalInit.u8Mode = CLK_XTAL_MD_OSC;
    stcXtalInit.u8StableTime = CLK_XTAL_STB_2MS;
    (void)CLK_XtalInit(&stcXtalInit);
    /* MPLL config */
    stc_clock_pll_init_t stcMPLLInit;
    (void)CLK_PLLStructInit(&stcMPLLInit);
    stcMPLLInit.PLLCFGR = 0UL;
    stcMPLLInit.PLLCFGR_f.PLLM = (1UL - 1UL);
    stcMPLLInit.PLLCFGR_f.PLLN = (50UL - 1UL);
    stcMPLLInit.PLLCFGR_f.PLLP = (2UL - 1UL);
    stcMPLLInit.PLLCFGR_f.PLLQ = (2UL - 1UL);
    stcMPLLInit.PLLCFGR_f.PLLR = (2UL - 1UL);
    stcMPLLInit.u8PLLState = CLK_PLL_ON;
    stcMPLLInit.PLLCFGR_f.PLLSRC = CLK_PLL_SRC_XTAL;
    (void)CLK_PLLInit(&stcMPLLInit);
    /* UPLL config */
    stc_clock_pllx_init_t stcUPLLInit;
    (void)CLK_PLLxStructInit(&stcUPLLInit);
    stcUPLLInit.PLLCFGR = 0UL;
    stcUPLLInit.PLLCFGR_f.PLLM = (2UL - 1UL);
    stcUPLLInit.PLLCFGR_f.PLLN = (60UL - 1UL);
    stcUPLLInit.PLLCFGR_f.PLLP = (2UL - 1UL);
    stcUPLLInit.PLLCFGR_f.PLLQ = (2UL - 1UL);
    stcUPLLInit.PLLCFGR_f.PLLR = (5UL - 1UL);
    stcUPLLInit.u8PLLState = CLK_PLLX_ON;
    (void)CLK_PLLxInit(&stcUPLLInit);
    /* 3 cycles for 126MHz ~ 200MHz */
    GPIO_SetReadWaitCycle(GPIO_RD_WAIT3);
    /* Switch driver ability */
    PWC_HighSpeedToHighPerformance();
    /* Set the system clock source */
    CLK_SetSysClockSrc(CLK_SYSCLK_SRC_PLL);
}

//Port Config
static void App_PortCfg(void)
{
    /* GPIO initialize */
    stc_gpio_init_t stcGpioInit;
    /* PB0 set to GPIO-Output */
    (void)GPIO_StructInit(&stcGpioInit);
    stcGpioInit.u16PinDir = PIN_DIR_OUT;
    stcGpioInit.u16PinAttr = PIN_ATTR_DIGITAL;
    (void)GPIO_Init(GPIO_PORT_B, GPIO_PIN_00, &stcGpioInit);

    GPIO_SetFunc(GPIO_PORT_C,GPIO_PIN_03,GPIO_FUNC_36);//USART2-TX
    
    GPIO_SetFunc(GPIO_PORT_A,GPIO_PIN_00,GPIO_FUNC_37);//USART2-RX
}



/**
 * @brief  Main function of the project
 * @param  None
 * @retval int32_t return value, if needed
 */
int32_t main(void)
{
    /* Register write unprotected for some required peripherals. */
    LL_PERIPH_WE(LL_PERIPH_ALL);
    //Clock Config
    App_ClkCfg();
    //Port Config
    App_PortCfg();
		
		eMBErrorCode    eStatus;
		
    eStatus = eMBInit( MB_RTU, 0x01, 0, 9600, MB_PAR_NONE );
		
    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );
		
		GPIO_SetPins(GPIO_PORT_B, GPIO_PIN_00);
    /* Register write protected for some required peripherals. */
    LL_PERIPH_WP(LL_PERIPH_ALL);
    for (;;) {
				
				eMBPoll(  );
    }
}

main.c中main函数加入的freemodbus内容

    eMBErrorCode    eStatus;
		
    eStatus = eMBInit( MB_RTU, 0x01, 0, 9600, MB_PAR_NONE );
		
    /* Enable the Modbus Protocol Stack. */
    eStatus = eMBEnable(  );


    for (;;) {
				
				eMBPoll(  );
    }

最后测试成功

小华HC32F460KETA 移植FreeModbus_第16张图片

 以上移植过程,皆是摸索中前进的结果,如有错误,请伙伴们纠正。

你可能感兴趣的:(单片机,嵌入式硬件)