火爆全网开源额温枪同平台之华大HC32L136 SDK开发入门
到淘宝以及相关平台上不少商家为了蹭疫情额温枪的热度把额温枪开发方案的价格定得非常高,少则几千,多则上万,实在是让国人寒心!关键时刻,大家应该同心协力,即将没法向白衣天使一样上前线去抗疫,也应该以别的方式贡献自己的一份力量才对。本着支持国产支持开源项目的原则,而且我也是玩板爱好者,所以就毫不犹豫就入手了华大半导体推出的HDSC Demo板,当做支持一下,顺便了解下这个平台,也算是给自己积累多平台的开发经验!后面还会根据这个持续输出高质量的项目并且开源。上一篇我们也分享了近期华大开源的额温枪方案,链接:分享一个近期开源火爆全网的额温枪方案(硬件+源码),该额温枪方案的主控MCU就是基于华大半导体自主研发的芯片HC32L136。
1、了解HC32L136
1.1、HC32L136特性
1.2、HC32L136命名规则
1.3、HC32L136引脚配置图
1.4、HC32L136功能模块
1.5、HC32L136存储器映射图
由此可见,国产MCU做得还是相当不错的!性价比非常高!
Demo板长下面这个样子:
Demo板的相关资料可以通过华大官方的ftp服务器下载,链接见文章最后,回复关键字获取资料下载地址。
玩一个板子,最重要的还是看看这个板子上有哪些东西吧,打开SDK包里的Demo板硬件原理图,大致浏览一下有哪些模块:
Demo板最有特色的部分是:CMSIS-DAP compatible JTAG + Virtual Com Port
,这个部分集成了DAP-LINK下载方式以及虚拟串口。
DAP-LINK是ARM官方开源的一款调试烧录器,在刚开始的时候叫CMISI DAP,现在官方对这个调试烧录器进行了更新换代,改名叫DAPLink,DAPLink可以调试arm contex全系列的MCU,所以相比较常规的Jlink和stlink更有优势,因为这也是官方一直在维护的,未来也会增添更多新的强大功能来给用户调试和烧录使用。
DAPLINK主要的功能如下:
- Arm-contex全系列新品的调试和烧录(HID)
- 自带虚拟串口,可以省去USB转串口(CDC)
- 拖拽式编程,通过模拟出U盘,然后将程序编译生成的.hex文件或者.bin文件拷贝进去完成程序烧录(MSC)
- WEBUSB功能(固件版本需要升级到2050)
DAPLink使用的是标准的CMISI-DAP协议,所以常见的IDE开发工具都可以完美的支持,比如常见的有:
- KEIL MDK
- IAR
- PyOCD
- 以及其它支持CMSIS-DAP协议的IDE开发平台
在Demo板上,具体的引脚连接如下:
由于开发板的USB口已经集成了这个功能,所以我们直接插USB连接开发板和PC就可以了,连接成功后可以看到电脑的设备管理器会有串行设备的标识,如果没有,则需要安装相应的串口驱动。
下载链接如下:
http://os.mbed.com/media/downloads/drivers/mbedWinSerial_16466.exe
由于我习惯使用的是KEIL MDK的环境进行学习开发,所以需要了解对环境做好设置,安装芯片开发包以及其它的一些设置,保证我可以正常使用。
2、KEIL MDK设置
2.1、安装集成开发环境支持包
打开官方提供的demo板样例中gpio的工程,俗话说,玩板先点灯,灯点起来了,就没有其它难事了。
打开原理图,对着demo程序看,是否和原理图上IO一致,该例程是让接在PD05的LED灯以一定的频率进行闪烁。
2.2、KEIL MDK环境设置
接下来开始配置KEIL-MDK,看看是否能下载这个Demo程序。
最后兴高采烈准备下载程序的时候发现:
结果发现没有该MCU的FLASH下载算法,刚刚不是已经安装了开发环境支持包吗???为什么没有自动适配?
专业的问题要让专业的人来帮忙解决,于是请教对应的FAE工程师,FAE工程师心平气和的教会了我添加FLASH算法的流程,给FAE工程师点赞!
于是开始找到刚刚安装的集成开发环境支持包的路径,然后把下载算法拷贝到MDK的FLASH目录下:
接下来将MDK工程编译后,点击下载程序:
2.3、GPIO输出Demo运行效果
根据程序编程思路,LED灯开始闪烁!到此,这个板子怎么配置环境和下载程序已经完成,接下来就到分析程序的时刻。
3、GPIO输出Demo程序编写分析
主程序如下:
**int32_t main(void){ stc_gpio_config_t pstcGpioCfg; ///< 打开GPIO外设时钟门控 Sysctrl_SetPeripheralGate(SysctrlPeripheralGpio, TRUE); ///< 端口方向配置->输出 pstcGpioCfg.enDir = GpioDirOut; ///< 端口驱动能力配置->高驱动能力 pstcGpioCfg.enDrv = GpioDrvH; ///< 端口上下拉配置->无上下拉 pstcGpioCfg.enPuPd = GpioNoPuPd; ///< 端口开漏输出配置->开漏输出关闭 pstcGpioCfg.enOD = GpioOdDisable; ///< 端口输入/输出值寄存器总线控制模式配置->AHB pstcGpioCfg.enCtrlMode = GpioAHB; ///< GPIO IO PD05初始化(PD05在STK上外接LED) Gpio_Init(GpioPortD, GpioPin5, &pstcGpioCfg); while(1) { ///< 端口PD05设置为高电平(LED点亮) Gpio_SetIO(GpioPortD, GpioPin5); delay1ms(1000); ///< 端口PD05设置为低电平(LED关闭) Gpio_ClrIO(GpioPortD, GpioPin5); delay1ms(1000); }}**
从这个程序我们了解到,完成这一个简单的GPIO输出需要做这几步:
- 1 配置外设时钟
- 2 GPIO基本配置及初始化
- 3 GPIO设置端口电平
看了上面这个风格,和STM32的编程方式还是非常的相似。
3.1 Sysctrl_SetPeripheralGate函数功能分析
/** ******************************************************************************* ** \brief 设置外设时钟门控开关 ** \param [in] enPeripheral 目标外设 ** \param [in] bFlag 使能开关 ** \retval Ok 设定成功 ** 其他 设定失败 ******************************************************************************/en_result_t Sysctrl_SetPeripheralGate(en_sysctrl_peripheral_gate_t enPeripheral, boolean_t bFlag){ en_result_t enRet = Ok; bFlag = !!bFlag; setBit(&(M0P_SYSCTRL->PERI_CLKEN), enPeripheral, bFlag); return enRet;}
哇!不愧是国产芯,连函数的代码注释都写上中文,不得不说真赞!接下来一步一步,首先分析函数形参en_sysctrl_peripheral_gate_t
,跳过去看,这是一个枚举:
/** ******************************************************************************* ** \brief 外设时钟门控开关类型枚举 ******************************************************************************/typedef enum en_sysctrl_peripheral_gate{ SysctrlPeripheralUart0 = 0u, ///< 串口0 SysctrlPeripheralUart1 = 1u, ///< 串口1 SysctrlPeripheralLpUart0 = 2u, ///< 低功耗串口0 SysctrlPeripheralLpUart1 = 3u, ///< 低功耗串口1 SysctrlPeripheralI2c0 = 4u, ///< I2C0 SysctrlPeripheralI2c1 = 5u, ///< I2C1 SysctrlPeripheralSpi0 = 6u, ///< SPI0 SysctrlPeripheralSpi1 = 7u, ///< SPI1 SysctrlPeripheralBTim = 8u, ///< 基础定时器 SysctrlPeripheralLpTim = 9u, ///< 低功耗定时器 SysctrlPeripheralAdvTim = 10u, ///< 高级定时器 SysctrlPeripheralTim3 = 11u, ///< 定时器3 SysctrlPeripheralOpa = 13u, ///< OPA SysctrlPeripheralPca = 14u, ///< 可编程计数阵列 SysctrlPeripheralWdt = 15u, ///< 看门狗 SysctrlPeripheralAdcBgr = 16u, ///< ADC&BGR SysctrlPeripheralVcLvd = 17u, ///< 电压比较和低电压检测 SysctrlPeripheralRng = 18u, ///< RNG SysctrlPeripheralPcnt = 19u, ///< PCNT SysctrlPeripheralRtc = 20u, ///< RTC SysctrlPeripheralTrim = 21u, ///< 时钟校准 SysctrlPeripheralLcd = 22u, ///< LCD SysctrlPeripheralTick = 24u, ///< 系统定时器 SysctrlPeripheralSwd = 25u, ///< SWD SysctrlPeripheralCrc = 26u, ///< CRC SysctrlPeripheralAes = 27u, ///< AES SysctrlPeripheralGpio = 28u, ///< GPIO SysctrlPeripheralDma = 29u, ///< DMA SysctrlPeripheralDiv = 30u, ///< 除法器 SysctrlPeripheralFlash = 31u, ///< Flash}en_sysctrl_peripheral_gate_t;
这里将MCU的时钟控制外设对应的位以枚举的形式定义出来了。
boolean_t bFlag
参数,跳过去看,其实是uint8_t类型typedef uint8_t boolean_t;
,也就是typedef unsigned char uint8_t;
,再继续向下看代码: en_result_t
也是一个枚举类型,它定义了返回状态:
/** generic error codes */typedef enum en_result{ Ok = 0u, ///< No error 没有错误 Error = 1u, ///< Non-specific error code 非特定的错误代码 ErrorAddressAlignment = 2u, ///< Address alignment does not match ErrorAccessRights = 3u, ///< Wrong mode (e.g. user/system) mode is set地址对齐方式不匹配 ErrorInvalidParameter = 4u, ///< Provided parameter is not valid提供的参数无效 ErrorOperationInProgress = 5u, ///< A conflicting or requested operation is still in progress 冲突或请求的操作仍在进行中 ErrorInvalidMode = 6u, ///< Operation not allowed in current mode 当前模式下不允许操作 ErrorUninitialized = 7u, ///< Module (or part of it) was not initialized properly 模块(或其一部分)未正确初始化 ErrorBufferFull = 8u, ///< Circular buffer can not be written because the buffer is full 由于缓冲区已满,无法写入循环缓冲区 ErrorTimeout = 9u, ///< Time Out error occurred (e.g. I2C arbitration lost, Flash time-out, etc.) 发生超时错误(例如,I2C仲裁丢失,闪存超时等) ErrorNotReady = 10u, ///< A requested final state is not reached 未达到请求的最终状态 OperationInProgress = 11u ///< Indicator for operation in progress 进行中的指示器} en_result_t;
函数代码的第一行en_result_t enRet = Ok;
默认将变量指定为返回没有错误。
函数代码的第二行bFlag = !!bFlag;
,当接收到bFlag参数不为0值,取反一次为0,再取反一次即为1,这就是!!的含义,这个参数设置得非常巧妙,我们继续往下看:
函数代码的第三行setBit(&(M0P_SYSCTRL->PERI_CLKEN), enPeripheral, bFlag);
,先看看M0P_SYSCTRL->PERI_CLKEN
是什么东西:
#define M0P_SYSCTRL ((M0P_SYSCTRL_TypeDef *)0x40002000UL)
这里的操作和stm32类似,就是将一个物理地址0x40002000UL
强转为一个结构体M0P_SYSCTRL_TypeDef
通过映射表我们也知道这个地址的含义是什么。
我们具体来看看这个结构体长什么样,有哪些参数:
typedef struct{ union { __IO uint32_t SYSCTRL0; stc_sysctrl_sysctrl0_field_t SYSCTRL0_f; }; union { __IO uint32_t SYSCTRL1; stc_sysctrl_sysctrl1_field_t SYSCTRL1_f; }; union { __IO uint32_t SYSCTRL2; stc_sysctrl_sysctrl2_field_t SYSCTRL2_f; }; union { __IO uint32_t RCH_CR; stc_sysctrl_rch_cr_field_t RCH_CR_f; }; union { __IO uint32_t XTH_CR; stc_sysctrl_xth_cr_field_t XTH_CR_f; }; union { __IO uint32_t RCL_CR; stc_sysctrl_rcl_cr_field_t RCL_CR_f; }; union { __IO uint32_t XTL_CR; stc_sysctrl_xtl_cr_field_t XTL_CR_f; }; uint8_t RESERVED7[4]; union { __IO uint32_t PERI_CLKEN; stc_sysctrl_peri_clken_field_t PERI_CLKEN_f; }; uint8_t RESERVED8[24]; union { __IO uint32_t PLL_CR; stc_sysctrl_pll_cr_field_t PLL_CR_f; };}M0P_SYSCTRL_TypeDef;
这里采用的联合体对相关寄存器进行定义,是为了让同一个联合体的操作都处于同一段内存,但是这样的话只要修改一个联合体中成员则会影响该联合体的其余所有成员,从代码看,这个结构体一共设置了9个联合体,如果需要详细了解结构体里每个参数具体是什么含义,那我们就需要去看Datasheet的系统控制器(SYSCTRL)章节的寄存器部分,由于篇幅限制,这里就不详细写出来了。
继续往下看:setBit
源码
#define setBit(addr,offset,flag) { if( (flag) > 0u){ *((volatile uint32_t *)(addr)) |= ((1UL)<<(offset)); }else{ *((volatile uint32_t *)(addr)) &= (~(1UL<<(offset))); } }
这是一个宏,当flag不为0时,则清除地址中对应的位,否则设置对应的位。
那么回来:
setBit(&(M0P_SYSCTRL->PERI_CLKEN), enPeripheral, bFlag);
这句话的意思就是,给&(M0P_SYSCTRL->PERI_CLKEN)
这个寄存器的enPeripheral
这一位设置bFlag
值,那我们就来了解PERI_CLKEN
寄存器是什么东西,打开Datasheet的外围模块时钟控制寄存器(PERI_CLKEN )
章节,往下看:
看到这里我们就明白了,这里就是要把外设的时钟给使能,这里我们操作的是GPIO,所以就要把GPIO模块的时钟给使能,GPIO模块位于该寄存器的第28位,通过读代码,我们了解到这个枚举定义的SysctrlPeripheralGpio = 28u, ///< GPIO
这个值就是指操作PERI_CLKEN
这个寄存器的第28位。
至此,我们已经弄明白Sysctrl_SetPeripheralGate
该库函数的实现原理及用法。
3.2 GPIO配置功能分析
和stm32一样,对GPIO进行操作,HC32L136一样也是通过一个结构体stc_gpio_config_t
来进行维护,只不过它和我们之前接触的32有所区别:
typedef struct{ en_gpio_dir_t enDir; ///< 端口方向配置 en_gpio_drv_t enDrv; ///< 端口驱动能力配置 en_gpio_pupd_t enPuPd; ///< 端口上下拉配置 en_gpio_od_t enOD; ///< 端口开漏输出配置 en_gpio_ctrl_mode_t enCtrlMode; ///< 端口输入/输出值寄存器总线控制模式配置}stc_gpio_config_t;
GPIO输入输出配置数据类型定义
typedef enum en_gpio_dir{ GpioDirOut = 0u, ///< GPIO 输出 GpioDirIn = 1u, ///< GPIO 输入}en_gpio_dir_t;
GPIO端口上拉、下拉配置数据类型定义
typedef enum en_gpio_pupd{ GpioNoPuPd = 0u, ///< GPIO无上拉下拉 GpioPu = 1u, ///< GPIO上拉 GpioPd = 2u, ///< GPIO下拉}en_gpio_pupd_t;
GPIO端口输出驱动能力配置数据类型定义
typedef enum en_gpio_drv{ GpioDrvH = 0u, ///< GPIO高驱动能力 GpioDrvL = 1u, ///< GPIO低驱动能力}en_gpio_drv_t;
GPIO端口开漏输出控制数据类型定义
typedef enum en_gpio_od{ GpioOdDisable = 0u, ///< GPIO开漏输出关闭 GpioOdEnable = 1u, ///< GPIO开漏输出使能}en_gpio_od_t;
GPIO端口输入/输出值寄存器总线控制模式选择
typedef enum en_gpio_ctrl_mode{ GpioFastIO = 0u, ///< FAST IO 总线控制模式 GpioAHB = 1u, ///< AHB 总线控制模式}en_gpio_ctrl_mode_t;
与系统时钟一样的分析方法,如果需要深入了解寄存器每一位以及为什么这么设置,需要自己去查看Datasheet相应的章节,根据该结构体的描述,我们能够快速的写出我们想要的让GPIO作为通过输出的功能:
/*1、定义一个结构体变量*/stc_gpio_config_t pstcGpioCfg;//2、对结构体参数的赋值操作///< 端口方向配置->输出pstcGpioCfg.enDir = GpioDirOut;///< 端口驱动能力配置->高驱动能力pstcGpioCfg.enDrv = GpioDrvH;///< 端口上下拉配置->无上下拉pstcGpioCfg.enPuPd = GpioNoPuPd;///< 端口开漏输出配置->开漏输出关闭pstcGpioCfg.enOD = GpioOdDisable;///< 端口输入/输出值寄存器总线控制模式配置->AHBpstcGpioCfg.enCtrlMode = GpioAHB;//3、调用GPIO完成初始化///< GPIO IO PD05初始化(PD05在STK上外接LED)Gpio_Init(GpioPortD, GpioPin5, &pstcGpioCfg);
关于Gpio_Init函数,里面做的事情和设置时钟的方法差不多,这里就不再进行详细分析,留给大家有兴趣自己去看手册对着寄存器进行分析,函数源码如下:
/** ******************************************************************************* ** \brief GPIO 初始化 ** ** \param [in] enPort IO Port口 ** \param [in] enPin IO Pin脚 ** \param [in] pstcGpioCfg IO 配置结构体指针 ** ** \retval Ok 设置成功 ** 其他值 设置失败 ******************************************************************************/en_result_t Gpio_Init(en_gpio_port_t enPort, en_gpio_pin_t enPin, stc_gpio_config_t *pstcGpioCfg){ //配置为默认值,GPIO功能 setBit((uint32_t)&M0P_GPIO->PAADS + enPort, enPin, FALSE); *((uint32_t*)(((uint32_t)(&(M0P_GPIO->PA00_SEL)) + enPort) + (((uint32_t)enPin)<<2))) = GpioAf0; //方向配置 if(GpioDirIn == pstcGpioCfg->enDir) { setBit(((uint32_t)&M0P_GPIO->PADIR + enPort), enPin, TRUE); } else { setBit(((uint32_t)&M0P_GPIO->PADIR + enPort), enPin, FALSE); } //驱动能力配置 if(GpioDrvH == pstcGpioCfg->enDrv) { setBit(((uint32_t)&M0P_GPIO->PADR + enPort), enPin, FALSE); } else { setBit(((uint32_t)&M0P_GPIO->PADR + enPort), enPin, TRUE); } //上拉下拉配置 if(GpioPu == pstcGpioCfg->enPuPd) { setBit(((uint32_t)&M0P_GPIO->PAPU + enPort), enPin, TRUE); setBit(((uint32_t)&M0P_GPIO->PAPD + enPort), enPin, FALSE); } else if(GpioPd == pstcGpioCfg->enPuPd) { setBit(((uint32_t)&M0P_GPIO->PAPU + enPort), enPin, FALSE); setBit(((uint32_t)&M0P_GPIO->PAPD + enPort), enPin, TRUE); } else { setBit(((uint32_t)&M0P_GPIO->PAPU + enPort), enPin, FALSE); setBit(((uint32_t)&M0P_GPIO->PAPD + enPort), enPin, FALSE); } //开漏输出功能 if(GpioOdDisable == pstcGpioCfg->enOD) { setBit(((uint32_t)&M0P_GPIO->PAOD + enPort), enPin, FALSE); } else { setBit(((uint32_t)&M0P_GPIO->PAOD + enPort), enPin, TRUE); } M0P_GPIO->CTRL2_f.AHB_SEL = pstcGpioCfg->enCtrlMode; return Ok;}
关于后面的Gpio_SetIO函数,也是差不多的,源码如下:
/** ******************************************************************************* ** \brief GPIO IO设置 ** ** \param [in] enPort IO Port口 ** \param [in] enPin IO Pin脚 ** ** \retval en_result_t Ok 设置成功 ** 其他值 设置失败 ******************************************************************************/en_result_t Gpio_SetIO(en_gpio_port_t enPort, en_gpio_pin_t enPin){ setBit(((uint32_t)&M0P_GPIO->PABSET + enPort), enPin, TRUE); return Ok;}
3.3 SysTick功能分析
再来看看delay1ms
这个函数,发现其实不是简单的一个for或者一个while对一个变量自加或者自减实现的延时,而是用SysTick定时器
来实现的延时,实现源码如下:
/** * \brief delay1ms * delay approximately 1ms. * \param [in] u32Cnt * \retval void */void delay1ms(uint32_t u32Cnt){ uint32_t u32end; SysTick->LOAD = 0xFFFFFF; SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_CLKSOURCE_Msk; while(u32Cnt-- > 0) { SysTick->VAL = 0; u32end = 0x1000000 - SystemCoreClock/1000; while(SysTick->VAL > u32end) { ; } } SysTick->CTRL = (SysTick->CTRL & (~SysTick_CTRL_ENABLE_Msk));}
SysTick定时器可以用于定时、计时或者为需要周期执行的任务提供中断源,它同样也是一个结构体进行维护:
/** \brief Structure type to access the System Timer (SysTick). */typedef struct{ __IOM uint32_t CTRL; /*!< Offset: 0x000 (R/W) SysTick Control and Status Register */ __IOM uint32_t LOAD; /*!< Offset: 0x004 (R/W) SysTick Reload Value Register */ __IOM uint32_t VAL; /*!< Offset: 0x008 (R/W) SysTick Current Value Register */ __IM uint32_t CALIB; /*!< Offset: 0x00C (R/ ) SysTick Calibration Register */} SysTick_Type;
关于SysTick定时器的介绍,可以详细查看Datasheet的SysTick定时器章节,学习思路一样,这里就不再继续往下追,后续做项目的时候发现疑难点再进行分析。