单片机

随着微电子技术和计算机技术的发展,原来以强电和电器为主、功能简单的电气设备发展成为强、弱电结合,具有数字化特点、功能完善的新型微电子设备。在很多场合,已经出现了越来越多的单片机产品代替传统的电气控制产品。属于存储程序控制的单片机,其控制功能通过软件指令来实现,其硬件配置也可变、易变。因此,一旦生产过程有所变动,就不必重新设计线路连线安装,有利于产品的更新换代和订单式生产。

传统电气设备采用的各种控制信号,必须转换到与单片机输入/输出口相匹配的数字信号。用户设备须输入到单片机的各种控制信号,如限位开关、操作按钮、选择开关、行程开关以及其他一些传感器输出的开关量等,通过输入电路转换成单片机能够接收和处理的信号。输出电路则应将单片机送出的弱电控制信号转换、放大到现场需要的强输出信号,以驱动功率管、电磁阀和继电器、接触器、电动机等被控制设备的执行元件,能方便实际控制系统使用。针对电气控制产品的特点,本文讨论了几种单片机I/O的常用驱动和隔离电路的设计方法,对合理地设计电气控制系统,提高电路的接口能力,增强系统稳定性和抗干扰能力有实际指导意义。

1、输入电路设计

1.png

图1 开关信号输入

一般输入信号最终会以开关形式输入到单片机中,以工程经验来看,开关输入的控制指令有效状态采用低电平比采用高电平效果要好得多,如图1如示。当按下开关S1时,发出的指令信号为低电平,而平时不按下开关S1时,输出到单片机上的电平则为高电平。该方式具有较强的耐噪声能力。

若考虑到由于TTL电平电压较低,在长线传输中容易受到外界干扰,可以将输入信号提高到+24 V,在单片机入口处将高电压信号转换成TTL信号。这种高电压传送方式不仅提高了耐噪声能力,而且使开关的触点接触良好,运行可靠,如图2所示。其中,D1为保护二极管,反向电压≥50 V。

2.png

图2 提高输入信号电平

3.png

图3 输入端保护电路

为了防止外界尖峰干扰和静电影响损坏输入引脚,可以在输入端增加防脉冲的二极管,形成电阻双向保护电路,如图3所示。二极管D1、D2、D3的正向导通压降UF≈0.7 V,反向击穿电压UBR≈30 V,无论输入端出现何种极性的破坏电压,保护电路都能把该电压的幅度限制在输入端所能承受的范围之内。即:VI~VCC出现正脉冲时,D1正向导通;VI~VCC出现负脉冲时,D2反向击穿;VI与地之间出现正脉冲时,D3反向击穿;VI与地之间出现负脉冲时,D3正向导通,二极管起钳位保护作用。缓冲电阻RS约为1.5~2.5 kΩ,与输入电容C构成积分电路,对外界感应电压延迟一段时间。若干扰电压的存在时间小于τ,则输入端承受的有效电压将远低于其幅度;若时间较长,则D1导通,电流在RS上形成一定的压降,从而减小输入电压值。

 此外,一种常用的输入方式是采用光耦隔离电路。如图4所示,R为输入限流电阻,使光耦中的发光二极管电流限制在10~20 mA。输入端靠光信号耦合,在电气上做到了完全隔离。同时,发光二极管的正向阻抗值较低,而外界干扰源的内阻一般较高,根据分压原理,干扰源能馈送到输入端的干扰噪声很小,不会产生地线干扰或其他串扰,增强了电路的抗干扰能力。

4.png

图4 输入端光耦隔离

在满足功能的前提下,提高单片机输入端可靠性最简单的方案是:在输入端与地之间并联一只电容来吸收干扰脉冲,或串联一只金属薄膜电阻来限制流入端口的峰值电流。

2、输出电路设计

单片机输出端口受驱动能力的限制,一般情况下均需专用的接口芯片。其输出虽因控制对象的不同而千差万别,但一般情况下均满足对输出电压、电流、开关频率、波形上升下降速率和隔离抗干扰的要求。在此讨论几种典型的单片机输出端到功率端的电路实现方法。

2.1 直接耦合

在采用直接耦合的输出电路中,要避免出现图5所示的电路。

5.png

图5 错误的输出电路

T1截止、T2导通期间,为了对T2提供足够的基极电流,R2的阻值必须很小。因为T2处于射极跟随器方式工作,因此为了减少T2损耗,必须将集射间电压降控制在较小范围内。这样集基间电压也很小,电阻R2阻值很小才能提供足够的基极电流。R2阻值过大,会大幅度增加T2压降,引起T2发热严重。而在T2截止期间,T1必须导通,高压+15 V全部降在电阻R2上,产生很大的电流,显然是不合理的。另外,T1的导通将使单片机高电平输出被拉低至接近地电位,引起输出端不稳定。T2基极被T1拉到地电位,若其后接的是感性负载,由于绕组反电势的作用,T2的发射极可能存在高电平,容易引起T2管基射结反向击穿。

图6为一直接耦合输出电路,由T1和T2组成耦合电路来推动T3。T1导通时,在R3、R4的串联电路中产生电流,在R3上的分压大于T2晶体管的基射结压降,促使T2导通,T2提供了功率管T3的基极电流,使T3变为导通状态。当T1输入为低电平时,T1截止,R3上压降为零,T2截止,最终T3截止。R5的作用在于:一方面作为T2集电极的一个负载,另一方面T2截止时,T3基极所储存的电荷可以通过电阻R3迅速释放,加快T3的截止速度,有利于减小损耗。

6.png

图6 直接耦合输出电路

2.2 TTL或CMOS器件耦合

若单片机通过TTL或CMOS芯片输出,一般均采用集电极开路的器件,如图7(a)所示。集电极开路器件通过集电极负载电阻R1接至+15 V电源,提升了驱动电压。但要注意的是,这种电路的开关速度低,若用其直接驱动功率管,则当后续电路具有电感性负载时,由于功率管的相位关系,会影响波形上升时间,造成功率管动态损耗增大。

为了改善开关速度,可采用2种改进形式输出电路,如图7(b)和图7(c)所示。图7(b)是能快速开通的改进电路,当TTL输出高电平时,输出点通过晶体管T1获得电压和电流,充电能力提高,从而加快开通速度,同时也降低了集电极开路TTL器件上的功耗。图7(c)为推挽式的改进电路,采用这种电路不但可提高开通时的速度,而且也可提高关断时的速度。输出晶体管T1是作为射极跟随器工作的,不会出现饱和,因而不影响输出开关频率。

7.png

图7 TTL或CMOS器件输出电路

2.3 脉冲变压器耦合

脉冲变压器是典型的电磁隔离元件,单片机输出的开关信号转换成一种频率很高的载波信号,经脉冲变压器耦合到输出级。由于脉冲变压器原、副边线圈间没有电路连接,所以输出是电平浮动的信号,可以直接与功率管等强电元件耦合,如图8所示。

8.png

图8 脉冲变压器输出电路

这种电路必须有一个脉冲源,脉冲源的频率是载波频率,应至少比单片机输出频率高10倍以上。脉冲源的输出脉冲送入控制门G,单片机输出信号由另一端输入G门。当单片机输出高电平时,G门打开,输出脉冲进入变压器,变压器的副线圈输出与原边相同频率的脉冲,通过二极管D1、D2检波后经滤波还原成开关信号,送入功率管。当单片机输出低电平时,G门关闭,脉冲源不能通过G门进入变压器,变压器无输出。

这里,变压器既传递信号,又传送能量,提高了脉冲源的频率,有利于减轻变压器的体重。由于变压器可通过调整电感量、原副边匝数等来适应不同推动功率的要求,所以应用起来比较灵活。更重要的是,变压器原副边线圈之间没有电的联系,副线圈输出信号可以跟随功率元件的电压而浮动,不受其电源大小的影响。

当单片机输出较高频率的脉冲信号时,可以不采用脉冲源和G门,对变压器原副边电路作适当调整即可。

2.4 光电耦合

光电耦合可以传输线性信号,也可以传输开关信号,在输出级应用时主要用来传递开关信号。如图9所示,单片机输出控制信号经缓冲器7407放大后送入光耦。R2为光耦输出晶体管的负载电阻,它的选取应保证:在光耦导通时,其输出晶体管可靠饱和;而在光耦截止时,T1可靠饱和。但由于光耦响应速度慢使开关延迟时间加长,限制了其使用频率。

9.png

图9 光耦输出电路

3 结语

单片机接口技术在很多文献中均有详细的介绍,但在对大量电气控制产品的改造和设计中,经常会碰到用接口芯片所无法解决的问题(如驱动电流大、开关速度慢、抗干扰差等),因此必须寻求另一种电路解决方案。上述几种输入/输出电路通过广泛的应用表明,其对合理、可靠地实现单片机电气控制系统具有较高的工程实用价值。

来源:STM32嵌入式开发

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 139

市面上很多基于单片机的产品都具有在线或离线升级功能,为了防止升级过程出现意外,一般我们都会对Flash程序数据进行校验,常见的就是添加 CRC 校验信息。

本文给大家讲述一下Keil和IAR中计算CRC值的方法。

Flash自检的流程

Flash的自检一般分为启动时自检和程序运行时自检两个阶段。不管是哪种自检,其思路都是:

在程序编译完成后,计算整个程序的CRC值,然后将这个CRC值添加到可执行文件末尾,再将带有CRC校验值的可执行文件烧录到MCU中。程序启动后,由程序中的自检代码重新根据当前Flash内容(不包括预存的CRC校验值)计算一次CRC值,再与之前预先计算并烧录到Flash中的CRC校验值进行比较,如果一致就通过检测。

这两个自检阶段的区别就是:

程序启动自检是一次性对整个实际Flash代码范围计算出最终的CRC值;而运行时的自检,为了不影响其他程序模块的运行,计算CRC的过程是分步进行的,每次计算一部分,分多次计算出最终的CRC值。围绕Flash的自检所发生的问题可以归为两大类,一类是预先计算CRC值时和上电后计算CRC值的Flash范围设置是否一致;第二类就是预先计算CRC时和上电后计算CRC采用的CRC算法是否一致。

如何添加CRC值

下面我们主要介绍如何添加CRC校验值到可执行文件。

1、基于IAR环境

如果你使用IAR,那么添加CRC值的配置相对比较简单。通过配置IAR的CRC计算参数,自动对整个FLASH空间进行CRC计算,并将计算结果放到FLASH的末尾。

1)修改Link文件,指定CRC值的存放位置

在Link文件中增加下面语句,指定checksum在FLASH中的存储位置。

1.jpg

该语句指定将CRC的值放在FLASH的末尾位置,是整个FLASH空间的末尾,不是应用程序的代码末尾。这样,CRC值的位置就是固定的。不会随代码大小而变化。

在自检代码中,可以通过__checksum读取Flash中保存的CRC校验值来与重新计算的CRC值进行比较。

2)配置Checksum页面的参数

在link文件中指定了checksum的存储位置后,还要在工程配置菜单中,配置计算CRC值的范围和参数。见下图:

2.jpg

IAR的checksum页面分为两个部分。

第一部分,也就是红线圈出的部分。定义了FLASH中需要计算CRC的范围和空闲字节填充值。这里注意要留出flash末尾存储CRC值的位置。

剩下的部分,就是对checksum计算参数的设定部分。

Checksum size:选择checksum的大小(字节数)Alignment:指定checksum的对齐方式。一般,处理器不支持非对齐访问时有用,不填的话默认1字节对齐。

Algorithm:选择checksum的算法。

Complement:是否需要进行补码计算。选择“As is”就是不进行补码计算。

Bit order:位输出的顺序。

MSB first,每个字节的高位在前。LSB first,每个字节的低位在前。

Reverse byte order within word: 对于输入数据,在一个字内反转各个字节的顺序。

Initial value: checksum计算的初始化值。

Checksum unit size:选择进行迭代的单元大小,按8-bit,16-bit还是32-bit进行迭代。

3)STM32CRC外设的配置

与上图IARchecksum的配置对应,STM32 CRC外设可以按以下配置:

POLY= 0x4C11DB7(CRC32)

Initial_Crc = 0Xffffffff

输入/输出数据不反转

输入数据:根据实际Flash范围设定,留出CRC校验值的位置

CRC外设初始化及计算代码:

3.jpg

2、基于Keil环境

KEIL没有提供直接生成CRC值的功能,所以需要借助外部的工具计算CRC值,然后添加到可执行文件的末尾。在X-CUBE-CLASSB软件中提供了bat文件,它会利用外部工具Srecord来生成整个Flash的CRC校验码并放在文件末尾。这个工具同样也可以和标准外设库的ClassB库一起用。下面我们就来看看如何在KEIL工程中利用Srecord工具来添加CRC值。

1)安装Srecord工具

下载Srecord 工具(http://srecord.sourceforge.net )。将srec_cat.exe,srec_cmp.exe,srec_info.exe拷贝到C:\SREC(自己新建)目录下。

2)添加crc_gen_keil.bat及crc_load.ini文件到KEIL工程同级目录下

打开X-CUBE-CLASSB软件包中的任意KEIL工程目录,将其中crc_gen_keil.bat及crc_load.ini文件拷贝到自己的KEIL工程目录下。

crc_gen_keil.bat:利用外部工具Srecord来生成整个Flash的CRC校验码并放在文件末尾。

crc_load.ini:这个文件调试时有用,用来配置调试时导入带CRC校验码的HEX,避免对FLASH检测失败导致程序无法正常运行。

4.jpg

这两个文件中的内容也需要根据新工程路径进行修改:

  • 将crc_gen_keil.bat中的TARGET_NAME和TARGET_PATH改成跟新工程一致。否则不能成功的自动生成带CRC校验值的HEX文件。

5.jpg

  • Crc_load.ini文件中的路径和文件也要和实际的一致

6.jpg


3)添加定义CRC校验码存储区域

7.jpg

在分散加载文件中将CHECKSUM指定在代码的末尾。和IAR不同的是,通过在分散加载文件中+last指定checksum的位置,它不是将其固定放在整个flash地址的末尾,而是放在实际代码的末尾。

8.jpg

4)添加外部命令让KEIL在编译结束后,自动生成一个带CRC校验值的HEX文件

9.jpg

定义debug和flash download使用的HEX文件路径,使用带CRC校验值的HEX文件。

10.jpg

11.jpg

5)STM32CRC外设的配置

这里需要注意,从X-CUBE-CLASSB的软件包里拷贝出的crc_gen_keil.bat文件,里面的BYTE_SWAP设为1,也就是它在计算CRC值的时候,输入的数据,在一个字内按字节颠倒顺序。

12.jpg

所以直接用HAL_CRC_Calculate函数进行计算结果是不对的。可以参考下面的代码来初始化及进行计算:

13.jpg

或者,将crc_gen_keil.bat文件,里面的BYTE_SWAP改为0, 就可以直接调用HAL_CRC_Calculate函数进行计算了。

总结

本文介绍了基于IAR及ARM KEIL中如何添加CRC校验值的过程。在X-CUBE-CLASSB软件包中,也都可以找到对应的例程。如果在调试中,遇到FLASH CRC校验出错,也不用急。

可以从计算CRC值的范围设置是否一致和采用的CRC算法是否一致这两个方面去找原因。一定要调试看看代码实际执行的情况,比如要测试的地址范围和实际代码执行时计算的地址范围是否一样,防止因为coding错误造成检测不通过。

直接来源:strongerHuang

素材来源 | 网络

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 223

Holtek着眼于中高阶应用市场,宣布新推出HT32F49365/HT32F49395高性能32-bit单片机。采用高效能Arm® Cortex®-M4核心,提供单精度浮点运算单元(FPU),支持所有Arm®单精度数据处理指令和数据类型,内置完整DSP指令和内存保护单元(MPU),增强数值运算效能与应用安全性。高集成度与高运算效能并提供多种节能模式,可满足各种应用场景,例如智能家居、工业控制、嵌入式系统、电机控制等。

1.jpg

HT32F49365/HT32F49395系列最高运行速度可达240MHz,工作电压范围为2.6V~3.6V,符合-40℃~105℃工业温度范围。Flash容量为256KB/1024KB,SRAM容量最高为224KB,可支持外部SPI Flash扩展 (最高16 MBytes寻址能力),并提供耐受5V电压输入的GPIO。丰富的外围资源包括:12-bit 2 Msps ADC ×3,12-bit DAC ×2,16-bit通用定时器 ×8,32-bit通用定时器 ×2,高级控制定时器 ×2,I²C ×3,SPI/I²S ×4,SDIO ×2,USART/UART ×8,USB FS ×1和CAN ×2。提供封装48QFN、48/64/100LQFP,依据封装类型GPIO引脚可达37~80个。

HT32F49365/HT32F49395系列支持业界主流Keil MDK及IAR EWARM开发环境,并提供硬件开发工具包、周边驱动函数库(Firmware Library)及应用范例等,操作系统支持FreeRTOS及RT-Thread OS。

来源:Holtek

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 28

GD32F30x 和 GD32F403 系列硬件为参考。

一. 硬件设计 

1.电源

1.png




2.png

3.png

2.复位

4.png


注意:

1. 内部上拉电阻40kΩ,建议外部上拉电阻建议10kΩ,以使得电压干扰不会导致芯片工作异常;

2. 若考虑静电等影响,可在NRST管脚处放置ESD保护二极管;

3. 尽管MCU内部有硬件POR电路,仍推荐外部加NRST复位阻容电路;

4. 如果MCU启动异常(由于电压波动等),可适当增加NRST对地电容值,拉长MCU复位完成时间,避开上电异常时序区。

3.时钟

GD32F30x/GD32F403系列内部有完备的时钟系统,可以根据不同的应用场合,选择合适的时钟源,时钟主要特征:

4-32MHz外部高速晶体振荡器(HXTAL);

8MHz内部高速RC振荡器(IRC8M);

32.768KHz外部低速晶体振荡器(LXTAL);

48 MHz内部高速RC振荡器(IRC48M);

40kHz内部低速RC振荡器(IRC40K);

PLL时钟源可选HXTAL、IRC8M或IRC48M;

HXTAL时钟可监控;

时钟树如下:

5.png







6.png

7.png

8.png

9.png

4.启动配置

GD32F30x/GD32F403系列提供三种启动方式,可以通过BOOT0和BOOT1来进行相关的配置。

用户可以配置BOOT0和BOOT1,进行上电复位或系统复位,从而确定启动选项。电路设计时,运行用户程序,BOOT0不能悬空,建议通过一个10kΩ电阻到GND;运行System Memory进行程序更新,需要将BOOT0接高,BOOT1接低,更新完成后,再将BOOT0接低上电才能运行用户程序;SRAM执行程序多用于调试状态下。

嵌入式的 Bootloader 存放在系统存储空间,用于对 FLASH 存储器进行重新编程。在GD32F305xx/ GD32F307xx/ GD32F403xx设备中,Bootloader可以通过USART0 (PA9 and PA10),USART1 (PD5 and PD6),USBFS (PA9, PA11 and PA12)和外界交互。在GD32F303xx(Flash<512kB)设备中,Bootloader可以通过USART0 (PA9 and PA10) 和外界交互, 在GD32F303xx(Flash>512kB)设备中,Bootloader可以通过USART0 (PA9 and PA10) USART1 (PA2 and PA3)和外界交互。




10.png

11.png

5.下载调试

GD32F30x/GD32F403系列内核支持JTAG调试接口和SWD接口。JTAG接口标准为20针接口,其中5根信号接口,SWD接口标准为5针接口,其中2根信号接口。

注意:复位后,调试相关端口为输入PU/PD模式,其中:

PA15:JTDI为上拉模式;

PA14:JTCK / SWCLK为下拉模式;

PA13:JTMS / SWDIO为上拉模式;

PB4:NJTRST为上拉模式;

PB3:JTDO为浮空模式。

12.png





13.png

14.png

15.png

有以下几种方式可以提高SWD下载调试通信的可靠性,增强下载调试的抗干扰能力。

1. 缩短SWD两个信号线长度,最好15cm以内;

2. 将SWD两根线和GND线编个麻花,缠在一起;

3. 在SWD两根信号线对地各并几十pF小电容;

4. SWD两根信号线任意IO串入100Ω~1KΩ电阻。

6.典型外设

ubs外设电路

16.png




17.png

18.png

二. PCB Layout

1.电源去耦电容

GD32F30x/GD32F403系列电源有VDD、VDDA、VREF+和VBAT四个供电脚,100nF去耦电容采用陶瓷即可,且需要保证位置尽可能地靠近电源引脚。电源走线要尽量使得经过电容后再到达MCU电源引脚,建议可通过靠近电容PAD处打Via的形式Layout。

19.png


2.时钟电路

GD32F30x/GD32F403系列时钟有HXTAL和LXTAL,要求时钟电路(包括晶体或晶振及电容等)靠近MCU时钟引脚放置,且尽量时钟走线由GND包裹起来。

20.png


注意:

1. 晶体尽量靠近MCU时钟Pin,匹配电容等尽量靠近晶体;

2. 整个电路尽量与MCU在同层,走线尽量不要穿层;

3. 时钟电路PCB区域尽量禁空,不走任何与时钟无关走线;

4. 大功率、强干扰风险器件及高速走线尽量远离时钟晶体电路;

5. 时钟线进行包地处理,以起到屏蔽效果。

3.复位电路

21.png


注意:复位电路阻容等尽可能地靠近MCU NRST引脚,且NRST走线尽量远离强干扰风险器件及高速走线等,条件允许的话,最好将NRST走线做包地处理,以起到更好的屏蔽效果。

4.USB 电路

USB模块有DM、DP两根差分信号线,建议PCB走线要求做特性阻抗90ohm,差分走线严格按照等长等距规则来走,且尽量使走线最短,如果两条差分线不等长,可在终端用蛇形线补偿短线。

由于阻抗匹配考虑,串联匹配电阻建议50Ω左右即可。当USB终端接口离MCU较远的时候,需要适当增大该串联电阻值。

22.png


注意:

1. 布局时摆放合理,以缩短差分走线距离;

2. 优先绘制差分线,一对差分线上尽量不要超过两对过孔,且需要对称放置;

3. 对称平行走线,保证两根线紧密耦合,避免90°、弧形或45°走线方式;

4. 差分走线上所接阻容、EMC等器件,或测试点,也要做到对称原则。

对于USB HS模块,MCU与外部HS PHY之间的数据线与信号控制线也尽量走短,需要用蛇形线做等长处理,注意事项如下:

1. 布局时摆放合理,USB HS-PHY芯片与MCU之间尽量紧凑;

2. 布线时,以信号线中最长的一根线长度为目标,将其他信号线通过蛇形走线补偿即可。

5.BGA 走线

GD32F403x 系列中包含 BGA100 的封装,对应的型号为 GD32F403VxH6,该芯片走线和其它 BGA 芯片类似,先对各个球型焊盘进行扇出,再进行布线操作。对于 0.5 mm Pitch 的 BGA封装,若将 BGA 焊盘大小设置为 0.25/0.35,过孔距焊盘以及线宽线距为 3 mil 时,可以使用Dog bone 型扇出,扇出后如图 3-5. BGA100 封装的扇出方式所示,过孔距焊盘距离为 4.5mil;但此种布线对 PCB 制造商工艺要求较高,需与 PCB 制造商沟通后再进行布线,若制造商工艺达不到要求,可对此 BGA 封装打盘中孔以及盲埋孔

23.png



参考:

https://gd32mcu.com/cn/download/10?kw=

来源: 嵌入式学习与实践

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 95

CW32的LVD低电压检测器适用于监测VDDA电源电压或外部引脚输入电压,当被监测电压与LVD阈值的比较结果满足触发的条件时,LVD将会产生中断或者复位信号,通常用来处理一些紧急任务。LVD产生的中断或复位标志,只能通过软件程序清零,并且只有当中断或复位标志被清零后,在再次达到触发条件时,LVD才能再次产生中断或复位信号。在本文中以CW32L083系列为例,介绍LVD的基本功能和使用例程。

LVD的基本功能介绍:

1、4路监测电压源

VDDA电源电压,PA00引脚输入,PB00引脚输入,PB11引脚输入

2、16阶阈值电压,范围2.02V-3.76V

3、3种触发条件,可以组合使用

电平触发:电压低于阈值

下降沿触发:电压跌落到阈值以下的下降沿

上升沿触发:电压回升到阈值以上的上升沿

4、可触发产生中断或复位信号,二者不能同时产生

5、8阶滤波可配置

6、支持迟滞功能

7、支持低功耗模式下运行,中断唤醒MCU

1.png

通过LVD的控制寄存器LVD_CR0的SOURCE位域来选择LVD模块监控的电压(VDDA电源/ PA00引脚/PB00引脚/PB11引脚),在监测外部引脚电压时,需将对应的GPIO端口配置为模拟输入模式(GPIOx_ANALOG.PINy = 1)。

LVD的比较结果可以从PA01/PA08/PC12/PE02/PF02脚输出,在此之前,需将对应的GPIO口配置为数字输出模式,同时选择端口位LVDOUT复用功能。

LVD 内置的电压比较器具有迟滞功能,只有当被监测电压高于或低于阈值电压达到 20mV 时,比较器输出信号才会发生翻转,可避免当 LVD 的监测电压在阈值电压附近时,电压比较器的输出结果发生频繁翻转,增强系统抗干扰能力。具体波形如下图所示:

2.png

LVD的阈值电压根据LVD控制寄存器LVD_CR0的VTH位控制。

3.png

LVD支持数字滤波功能,可以增强系统的鲁棒性(系统在一定的参数抖动下,维持起某些性能的特性),可以将LVD电压比较的输出结果信号进行数字滤波,小于滤波宽度的信号被滤除,不会被触发中断或复位,如下图所示,图中两处噪音或其他信号就被滤除了。

4.png

通过设置控制寄存器LVD_CR1的FLTEN位域,可以使能数字滤波模块,当将该位设置为1的时候,会使能数字滤波模块。

通过设置控制寄存器 LVD_CR1 的 FLTCLK 位域可以选择数字滤波的时钟:

• FLTCLK 位为 1,选择 HSIOSC 作为滤波时钟 

• FLTCLK 位为 0,选择内置 RC 振荡器时钟作为滤波时钟,其频率约 150kHz

控制寄存器 LVD_CR1 的 FLTTIME 位域用于选择数字滤波的时钟个数,如下表所示:

5.png

从 LVD 状态寄存器 LVD_SR 的 FLTV 位域,可以读出经 LVD 数字滤波后的信号电平;当 GPIO 的功能复用为 LVD_OUT 时,数字滤波后的信号就可以从 GPIO 输出,以方便观察测量。

LVD 支持在低功耗模式下工作,中断输出可将芯片从低功耗模式下唤醒。当被监测电压与 LVD 阈值的比较结果满足触发条件时,可产生中断或复位信号。产生中断还是复位信号由控制寄存器 LVD_CR0 的 ACTION 位域控制:

 • ACTION 为 1,LVD 触发产生复位 #define LVD_Action_Reset  ((uint32_t)0x00000002)

 • ACTION 为 0,LVD 触发产生中断 #define LVD_Action_Irq   ((uint32_t)0x00000000)

LVD可以通过设置控制寄存器 LVD_CR0 的 IE 位域为 1,使能 LVD 中断,满足触发条件时将产生 LVD 中断,中断标志位 LVD_SR.INTF 会被硬件置 1,用户可以向 INTF 位写 0,清除中断标志。设置控制寄存器 LVD_CR1 的 LEVEL、FALL、RISE 位域,可选择不同的中断或复位触发方式,三者可组合使用:

• LEVEL 为 1,被监测电压低于阈值时触发中断或产生复位 

• FALL 为 1,被监测电压跌落到阈值以下的下降沿触发中断或产生复位 

• RISE 为 1,被监测电压回升到阈值以上的上升沿触发中断或产生复位

LVD使用例程介绍:

根据上述内容,可以配置一个关于CW32L083的电压监测例程,LVD的输入通道设置为PA00,输出端口为PA08,门限电压为2.02V,利用LVD的中断实现当LVD输入通道电压低于或者高于门限电压时刻(利用上升沿和下降沿),PC03输出电平翻转一次。

void LVD_PortInit(void) 
{ 
    GPIO_InitTypeDef GPIO_InitStructure = {0}; 
    
    //打开GPIOA时钟 
    __RCC_GPIOA_CLK_ENABLE(); 
    
    //将PA08设置为LVD比较结果输出 
    GPIO_InitStructure.Pins = GPIO_PIN_8; 
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; 
    GPIO_Init(CW_GPIOA, &GPIO_InitStructure); 
    
    //将PA08复用为LVD比较结果输出     
    PA08_AFx_LVDOUT();
     //将PA00设置为LVD的输入口 
    PA00_ANALOG_ENABLE(); 
} 

int main(void) 
{ 
    LVD_InitTypeDef LVD_InitStruct = {0}; 
    
    //LED初始化 
    LED_Init(); 
    
    //配置测试IO口 
    LVD_PortInit(); 
    
    LVD_InitStruct.LVD_Action = LVD_Action_Irq;  //配置中断功能 
    LVD_InitStruct.LVD_Source = LVD_Source_PA00; //配置LVD输入口为PA00 
    LVD_InitStruct.LVD_Threshold = LVD_Threshold_2p02V; //配置LVD基准电压为2.02v 
    LVD_InitStruct.LVD_FilterEn = LVD_Filter_Enable;//LVD滤波模块开启 
    LVD_InitStruct.LVD_FilterClk = LVD_FilterClk_RC150K;//LVD滤波时钟为150KHz 
    LVD_InitStruct.LVD_FilterTime = LVD_FilterTime_4095Clk; 
    LVD_Init(&LVD_InitStruct);
    
    LVD_TrigConfig(LVD_TRIG_FALL | LVD_TRIG_RISE, ENABLE);//LVD中断为上升沿和下降沿触发 
    LVD_EnableIrq(LVD_INT_PRIORITY); 
    LVD_ClearIrq(); 
    FirmwareDelay(4800); 
    LVD_Enable(); //LVD使能 
    
    while (1) 
    { 
        if (gFlagIrq) 
        { 
            PC03_TOG(); 
            gFlagIrq = FALSE; 
        } 
    } 
} 

/** 
* @brief LED I/O初始化 
* 
*/ 
void LED_Init(void) 
{ 
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    
    //打开GPIOC时钟 
    REGBITS_SET(CW_SYSCTRL->AHBEN, SYSCTRL_AHBEN_GPIOC_Msk); 
    
    /* Configure the GPIO_LED pin */ 
    GPIO_InitStructure.Pins = GPIO_PIN_2 | GPIO_PIN_3; 
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; 
    GPIO_Init(CW_GPIOC, &GPIO_InitStructure); 
    
    //LEDs are off. 
    PC02_SETLOW(); 
    PC03_SETLOW(); 
} 

//LVD中断服务函数 
void LVD_IRQHandler(void) 
{ 
    LVD_ClearIrq();      //清除中断标志 
    gFlagIrq = TRUE;     //将gFlagIrq赋值为TURE 
}

根据上述例程可以得到在PA00的输入电压值低于2.02v或高于2.02v的瞬间时刻,LVD会产生中断,PC03的输出电平会产生翻转,可利用CW32L083的开发板和一根杜邦线,将PA00和DVCC连接,在连接上的时刻以及拔掉杜邦线的时刻,LED1的状态会发生翻转。

来源:武汉芯源半导体

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 153

程序架构重要性

很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。

最终代码跑起来看似没有问题(有可能也真的没有问题),但是系统的可扩展性很差,添加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。

所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。

在学习实时操作系统的过程中,发现实时操作系统框架与个人的业务代码之间的耦合性就非常低,都是只需要将业务代码通过一定的接口函数注册好后就交给操作系统托管了,十分方便。

但是操作系统的调度过于复杂,这里就使用操作系统的思维方式来重构这个时间片轮询框架。实现该框架的完全解耦,用户只需要包含头文件,并且在使用过程中不需要改动已经写好的库文件。

Demo

首先来个demo,该demo是使用电脑开两个线程:一个线程模拟单片机的定时器中断产生时间片轮询个时钟,另一个线程则模拟主函数中一直运行的时间片轮询调度程序。

#include <thread>
#include <stdio.h>
#include <windows.h>
#include "timeslice.h"

// 创建5个任务对象
TimesilceTaskObj task_1, task_2, task_3, task_4, task_5;

// 具体的任务函数
void task1_hdl()
{        
    printf(">> task 1 is running ...\n");
}

void task2_hdl()
{    
    printf(">> task 2 is running ...\n");
}

void task3_hdl()
{    
    printf(">> task 3 is running ...\n");
}

void task4_hdl()
{    
    printf(">> task 4 is running ...\n");
}

void task5_hdl()
{    
    printf(">> task 5 is running ...\n");
}

// 初始化任务对象,并且将任务添加到时间片轮询调度中
void task_init()
{    
    timeslice_task_init(&task_1, task1_hdl, 1, 10);    
    timeslice_task_init(&task_2, task2_hdl, 2, 20);    
    timeslice_task_init(&task_3, task3_hdl, 3, 30);    
    timeslice_task_init(&task_4, task4_hdl, 4, 40);    
    timeslice_task_init(&task_5, task5_hdl, 5, 50);    
    timeslice_task_add(&task_1);    
    timeslice_task_add(&task_2);    
    timeslice_task_add(&task_3);    
    timeslice_task_add(&task_4);    
    timeslice_task_add(&task_5);
}

// 开两个线程模拟在单片机上的运行过程
void timeslice_exec_thread()
{    
    while (true)    
    {        
        timeslice_exec();    
    }
}

void timeslice_tick_thread()
{    
    while (true)    
    {        
        timeslice_tick();        
        Sleep(10);    
    }
}

int main()
{    
    task_init();
    
   printf(">> task num: %d\n", timeslice_get_task_num());    
   printf(">> task len: %d\n", timeslice_get_task_timeslice_len(&task_3));
   
   timeslice_task_del(&task_2);    
   printf(">> delet task 2\n");    
   printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));
    
   printf(">> task num: %d\n", timeslice_get_task_num());    
   timeslice_task_del(&task_5);    printf(">> delet task 5\n");
   printf(">> task num: %d\n", timeslice_get_task_num());    
   printf(">> task 3 is exist: %d\n", timeslice_task_isexist(&task_3));   
   
   timeslice_task_add(&task_2);    printf(">> add task 2\n");    
   printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2));
    
   timeslice_task_add(&task_5);    printf(">> add task 5\n");   
   printf(">> task num: %d\n", timeslice_get_task_num());
    
   printf("\n\n========timeslice running===========\n");
    
   std::thread thread_1(timeslice_exec_thread);    
   std::thread thread_2(timeslice_tick_thread);
    
   thread_1.join();    
   thread_2.join();

   return 0;
}

运行结果如下:

1.jpg

由以上例子可见,这个框架使用十分方便,甚至可以完全不知道其原理,仅仅通过几个简单的接口就可以迅速创建任务并加入到时间片轮询的框架中,十分好用。

时间片轮询架构

其实该部分主要使用了面向对象的思维,使用结构体作为对象,并使用结构体指针作为参数传递,这样作可以节省资源,并且有着极高的运行效率。

其中最难的部分是侵入式链表的使用,这种链表在一些操作系统内核中使用十分广泛,这里是参考RT-Thread实时操作系统中的侵入式链表实现。

h文件:

#ifndef _TIMESLICE_H
#define _TIMESLICE_H

#include "./list.h"

typedef enum 
{    
    TASK_STOP,    
    TASK_RUN
} IsTaskRun;

typedef struct timesilce
{    
    unsigned int id;    
    void (*task_hdl)(void);    
    IsTaskRun is_run;    
    unsigned int timer;    
    unsigned int timeslice_len;    
    ListObj timeslice_task_list;
} TimesilceTaskObj;

void timeslice_exec(void);
void timeslice_tick(void);
void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len);
void timeslice_task_add(TimesilceTaskObj* obj);
void timeslice_task_del(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj);
unsigned int timeslice_get_task_num(void);
unsigned char timeslice_task_isexist(TimesilceTaskObj* obj);

#endifc文件:
#include "./timeslice.h"

static LIST_HEAD(timeslice_task_list);

void timeslice_exec()
{    
    ListObj* node;    
    TimesilceTaskObj* task;
   
   list_for_each(node, &timeslice_task_list)    
   {
        
        task = list_entry(node, TimesilceTaskObj, timeslice_task_list);        
        if (task->is_run == TASK_RUN)        
        {            
            task->task_hdl();            
            task->is_run = TASK_STOP;        
        }    
    }
}

void timeslice_tick()
{    
    ListObj* node;    
    TimesilceTaskObj* task;
   
   list_for_each(node, &timeslice_task_list)    
   {        
       task = list_entry(node, TimesilceTaskObj, timeslice_task_list);        
       if (task->timer != 0)        
       {            
           task->timer--;            
           if (task->timer == 0)            
           {                
               task->is_run = TASK_RUN;                
               task->timer = task->timeslice_len;            
           }        
       }    
   }
}

unsigned int timeslice_get_task_num()
{    
    return list_len(&timeslice_task_list);
}

void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len)
{    
    obj->id = id;    
    obj->is_run = TASK_STOP;    
    obj->task_hdl = task_hdl;    
    obj->timer = timeslice_len;    
    obj->timeslice_len = timeslice_len;
}

void timeslice_task_add(TimesilceTaskObj* obj)
{    
    list_insert_before(&timeslice_task_list, &obj->timeslice_task_list);
}

void timeslice_task_del(TimesilceTaskObj* obj)
{    
    if (timeslice_task_isexist(obj))        
        list_remove(&obj->timeslice_task_list);    
    else        
        return;
}

unsigned char timeslice_task_isexist(TimesilceTaskObj* obj)
{    
    unsigned char isexist = 0;    
    ListObj* node;    
    TimesilceTaskObj* task;
    
   list_for_each(node, &timeslice_task_list)    
   {        
       task = list_entry(node, TimesilceTaskObj, timeslice_task_list);        
       if (obj->id == task->id)            
           isexist = 1;    
   }
    
   return isexist;
}

unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj)
{    
    return obj->timeslice_len;
}

底层侵入式双向链表

该链表是linux内核中使用十分广泛,也十分经典,其原理具体可以参考文章:

https://www.cnblogs.com/skywang12345/p/3562146.html

h文件:

#ifndef _LIST_H
#define _LIST_H    
#define offset_of(type, member)             (unsigned long) &((type*)0)->member
#define container_of(ptr, type, member)     ((type *)((char *)(ptr) - offset_of(type, member)))

typedef struct list_structure
{    
    struct list_structure* next;    
    struct list_structure* prev;
} ListObj;

#define LIST_HEAD_INIT(name)    {&(name), &(name)}
#define LIST_HEAD(name)         ListObj name = LIST_HEAD_INIT(name)

void list_init(ListObj* list);
void list_insert_after(ListObj* list, ListObj* node);
void list_insert_before(ListObj* list, ListObj* node);
void list_remove(ListObj* node);
int list_isempty(const ListObj* list);
unsigned int list_len(const ListObj* list);

#define list_entry(node, type, member) \    
    container_of(node, type, member)
    
#define list_for_each(pos, head) \    
    for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_safe(pos, n, head) \  
    for (pos = (head)->next, n = pos->next; pos != (head); \    
        pos = n, n = pos->next)
        
#endif

c文件:

#include "list.h"

void list_init(ListObj* list)
{    
    list->next = list->prev = list;
}

void list_insert_after(ListObj* list, ListObj* node)
{    
    list->next->prev = node;    
    node->next = list->next;
   
   list->next = node;    
   node->prev = list;
}

void list_insert_before(ListObj* list, ListObj* node)
{    
    list->prev->next = node;    
    node->prev = list->prev;
    
   list->prev = node;    
   node->next = list;
}

void list_remove(ListObj* node)
{    
    node->next->prev = node->prev;    
    node->prev->next = node->next;
   
   node->next = node->prev = node;
}

int list_isempty(const ListObj* list)
{    
    return list->next == list;
}

unsigned int list_len(const ListObj* list)
{    
    unsigned int len = 0;    
    const ListObj* p = list;    
    while (p->next != list)    
    {        
        p = p->next;        
        len++;    
    }
    
    return len;
}

到此,一个全新的,完全解耦的,十分方便易用时间片轮询框架完成。

来源:小麦大叔

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 77

就8051 MCU(单片机)而言,烧录器的种类繁多,兹此我们为您介绍烧录工具的”在线升级”。过去有普通烧写器,这个工具大多用并口烧录,使用引脚较多。以笙泉科技的8051 MCU为例:MPC82系列、MPC89系列几乎为20或20多个引脚。这些带来诸多不便,假如程序有BUG,软件需要升级等,就会很麻烦,得要先把IC从系统上卸下来,然后烧录新的程序,最后再焊回去,……於是痛苦就这么产生了。

基于解决上述的痛点,方便的ISP和ICP方式应运而生。

方式1: ISP(在系统编程)

所谓的ISP即在系统编程: In-System Programming, 缩写为ISP), 参见下图之范例:

1.png

这样,使用者可以在系统上预留3个点:VCC、DTA(P3.1)、GND,通过笙泉专利默认的应用程序软件重新配置在线升级,由于烧录接口减少,第一段所述之痛苦也可逐步减轻。

2.png

8051 ISP烧录器(TH079)特色:

  • USB即插即用

  • 支持联机及脱机二种烧录模式

  • 支持滚动码烧录,此滚动码可当序列号或唯一ID号使用

  • 可设置最大烧录次数限制

  • 自动软件在线更新

  • 3线传输模式,接口简单好连接

3.png

虽然以ISP方式,其使用的接口少(VCC, P3.1, GND) ,但它必须在IC内先植入ISP CODE,也就是说必须用掉部分程序空间,且只能修改部分硬件选项。

方式2: 专业再升级-ICP(在电路编程)

1)  ICP (在电路编程)为一种电路,具有5线烧录模式,完全可代替前述的ISP方式之在线烧录工具,这个在笙泉科技的对应规格书上都有强烈建议,常搭配采用的仿真、烧录工具包括有OCD_ICE和M-Link。

4.png

2) ICP (在电路编程)特色:

  • 无须在目标芯片上预编程一个引导程序

  • 专用串行接口: 不占用 IO口

  • 目标芯片无须在运行状态: 仅需电源

  • 便携、独立的工作,而无须主机的干预

采ICP方式的普通烧写器,其关键是只要用到5个接口,一般来讲4个就足够 ( OCD_SCL, VDD, OCD_SDA, GND )。亦即该烧写器通过ICP方式(电路)来实现在线升级/下载更新程序的功能。

最后归纳: 在您设计的系统中,一定要预留ICP的接口(4个点 OCD_SCL, VDD, OCD_SDA, GND ),为您的产品保驾护航,从此前述之”痛苦”将消失,轻松实现在线升级更新。

来源:megawin笙泉科技

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 83

页面

订阅 RSS - 单片机