STM32系列驱动介绍
在RTT实时操作系统中,各种各样的设备驱动是通过一套I/O设备管理框架来管理的。
设备管理框架给上层应用提供了一套标准的设备操作API,开发者通过这些标准设备操作API,可以高效地完成和底层硬件外设的交互。
使用I/O设备管理框架开发应用程序,有如下优点:
- 使用同一套标准的API开发应用程序,使应用程序具有更好的移植性。
- 底层驱动程序的升级和修改不会影响到上层代码。
- 驱动和应用程序相互独立,方便多个开发者协同开发。
对一个BSP而言,有如下三类驱动:
- 片上外设驱动:指MCU芯片上的外设,如硬件定时器、ADC和看门狗等。
- 板载外设驱动:开发板上外设,例如TF卡,以太网和LCD等。
- 扩展模块驱动:指可以通过扩展接口或者杜邦线连接的开发板的模板,例如ESP8266。
BSP框架
每一个STM32系列的BSP由三部分组成,分别是通用库、BSP模板和特定开发板BSP。
MPU
内存保护单元,在armv7-m架构下,Cortex-M3和Cortex-M4处理器对MPU都是选配的,不是必须的。
MPU是一个可编程设备,可以用来定义内存空间的属性,比如特权之类和非特权指令以及cache是否可以访问。
armv7-m通常支持8个region。一个region就代表一段连续的区域。
MPU可以让嵌入式系统更加健壮,以及保护一些加密区域,防止黑客攻击。
MPU有以下能力可以增加系统的健壮性:
- 可以阻止用户去破坏操作系统要使用的数据。
- 可以阻止一个任务非法访问其它任务的数据,将任务完全隔离开
- 可以把关键数据区设置为只读,从而不被破坏
- 检测其它意外访问,比如堆栈溢出,数组越界等。
MPU是操作系统提供的服务。在嵌入式调试时,经常会遇到hardFault,这个时候一般情况可能是某个指针指向未知的地方,然后对该地址进行修改赋值,就会触发hardFault。
- MPU可以定义某些特定的地址区域的属性,这个属性可以定义成很多类型,比如定义非特权状态下不可以赋值。
- 如果非特权指令不小心访问到这个地址区域并尝试给该区域赋值修改,这个时候就会触发MemManage fault或者hardfault中断,代表程序不允许修改该区域。
比如,RTOS一些特殊变量,用户线程是不被允许访问和修改的,这个时候如果启用了MPU,并且保护了这些变量,那用户即使知道这里的实际的物理地址,也是不被允许访问和修改的。
MPU寄存器模组
MPU类型寄存器主要表示这个MCU有几个region。
MPU控制寄存器主要使能MPU控制。
BootLoader
BootLoader的主要功能是更新app分区中的固件。
- app存储于片内Flash,主要用于存储app固件
- download存储于片内Flash或者SPI Flash,存储待升级固件
当系统需要升级固件时,BootLoader将从download分区将固件搬运到app分区。
- BootLoader启动时检查download分区和app分区中的固件版本。
- 如果两个固件版本相同,就跳转到app分区,BootLoader运行结束。
- 搬运过程中BootLoader可以对固件进行校验、解密、解压缩等操作。
- 搬运完毕后,删除download分区中存储的固件。
- 重启系统跳转到app分区中的固件运行,BootLoader运行结束。
多线程非阻塞网络编程
随着物联网的发展,越来越多产品需要基于网络进行数据传输。
在RTT使用socket网络编程时,当一个任务调用socket的recv()函数接收数据时,如果 socket 上并没有接收到数据,这个任务将阻塞在这个 recv() 函数里。
这个时候,这个任务想要处理一些其他事情,例如进行一些数据采集,发送一些额外数据到网络上等,将变得不可能了。与此同时,其他线程也需要将数据上传同一个服务器,如果直接多个线程共同使用一个 socket 操作,这将会破坏底层 lwip 的消息事件模型。
socket编程模型
客户端使用流程:
- socket()创建一个socket,返回套接字的描述符,并为其分配资源。
- connect()向服务器发出连接请求。
- send()/recv()与服务器进行通信。
- closesocket() 关闭 socket,回收资源。
服务器使用流程:
- socket() 创建一个 socket,返回套接字的描述符,并为其分配系统资源。
- bind()将套接字绑定到一个本地地址和端口上。
- listen()将套接字设为监听模式并设置监听数量,准备接收客户端请求。
- accept()等待监听的客户端发起连接,并返回已经接受连接的新套接字描述符。
- send()/recv()与服务器进行通信。
- closesocket() 关闭 socket,回收资源。
非阻塞socket编程
在 RT-Thread 中,自 v3.0.0 以来更标准化,支持更多的 POSIX API。这其中就包括 poll / select 接口实现,并且可以进行 socket 和设备文件的联合 poll / select。select、poll的内部实现机制相似,由于本文选用 select 方式,故在此不对 poll 展开介绍。
下面结合框图进一步说明如何使用 select 和 pipe 来解决这类问题。
图中存在三个线程:应用线程thread1、thread2和客户端线程thread client,其中thread client完成select功能。
- 数据发送过程:应用程序通过pipe往thread_client发送数据data1,select探测到pipe有数据可读,thread client被唤醒,然后读取pipe中的并通过TCP socket发送到server。
- 数据接收过程:server 通过 TCP socket 发送数据 data2 到 thread client,select 探测到 socket 有数据可读,thread client 被唤醒,thread client 可以获得接收到的数据。
select
select()可以阻塞地同时探测一组支持非阻塞的I/O设备是否有事件发生,直到某一个设备触发了事件或者超过了指定的等待时间,此时我们可以把需要的数据源通道放到select的探测范围内,只要相应的数据源准备好 select 就会返回,这时就能无阻塞地读取到数据。
select()主要处理I/O多路复用的情况,适用于:
- 客户端要处理多个描述符时(一般是交互式输入和网络套接字)。
- 服务器要处理监听套接字,又要处理已连接套接字。
- 服务器要处理TCP,又要处理UDP。
- 服务器要处理多个服务或协议。
pipe
pipe是一个基于文件描述符的单向数据通道,用于线程间的同学。
我们可以将 pipe 理解为水管,水通过水管从一端流向另一端,就像我们的数据从一个线程流向另一个线程,以此来达到线程间通信的目的。
管道本质上也是一个文件,因此它支持文件描述符的形式操作,也就是说我们可以通过open,read、write等函数对管道进行操作。
在使用管道之前,需要先创建它,通过调用rt_pipe_create函数来创建管道:
rt_pipe_t *rt_pipe_create(const char *name, int bufsz);
应用AT组件连接ESP8266模块
为了方便用户使用AT命令,简单的适配不同的AT模块,RTT提供了AT组件用于AT设备的连接和数据通信。
AT 组件的实现包括客户端的和服务器两部分。对于嵌入式设备而言,更多的情况下设备使用 AT 组件作为客户端连接服务器设备,所以本文将为大家重点介绍 AT 组件中客户端的主要功能、移植方式和实现原理,并介绍在客户端的基础上实现标准 BSD Socket API,使用 AT 命令完成复杂网络通讯。
NTP
NTP(Network Time Protocol)是网络时间协议,它是用来同步网络中各个计算机时间的协议。
RTT实现了NTP客户端,可以通过网络获取本地时间,并同步板子的RTC时间。
在msh中输入ntp_sync即可从默认的NTP服务器获取本地时间。
MQTT
Paho MQTT 是 Eclipse 实现的 MQTT 协议的客户端,本软件包是在 Eclipse paho-mqtt 源码包的基础上设计的一套 MQTT 客户端程序。
MQTT 使用发布 / 订阅消息模式,发送消息时要指定发给哪个主题名(Topic Name),接收消息前要订阅一个主题名,然后才能接收到发送给这个主题名的消息内容。
RT-Thread MQTT 客户端功能特点:
- 断线自动重连
- pipe模型,非阻塞API
- 事件回调机制
- TLS加密传输
电源管理
随着物联网(IoT)的兴起,产品对功耗的需求越来越强烈。
作为数据采集的传感器节点通常需要在电池供电时长期工作,而作为联网的SOC也需要有快速的响应功能和较低的功耗。
在产品开发的起始阶段,首先考虑是尽快完成产品的功能开发。在产品功能逐步完善之后,就需要加入电源管理功能。
为了适应IoT这种需求,RTT提供了电源管理框架。电源管理框架的理念是尽量透明,使得产品加入低功耗功能更加轻松。
MCU通常提供了多种时钟源供用户选择。
例如潘多拉开发板上板载的 STM32L475 就可以选择 LSI/MSI/HSI 等内部时钟,还可以选择 HSE/LSE 等外部时钟。
MCU内通常也集成了PLL,基于不同的时钟源,向MCU的其它模块提供更高频率的时钟。
为了支持低功耗功能,MCU 里也会提供不同的休眠模式。例如 STM32L475 里,可以分成 SLEEP模式、STOP模式、STANDBY模式。这些模式还可以有进一步的细分,以适应不同的场合。
配置工程
开启Env工具,进入潘多拉开发板的BSP目录,在Env命令行里输入menuconfig进入配置界面配置工程。
配置内核选项:使用 PM 组件需要更大的 IDLE 线程的栈,这里使用了1024 字节。例程里还使用 Software timer,所以我们还需要开启相应的配置
配置完成,保存并退出配置选项,输入命令scons --target=mdk5生成 mdk5 工程;
打开mdk5 工程可以看到相应的源码以及被添加进来:
定时应用
在定时应用里,我们创建了一个周期性的软件定时器,定时器任务里周期性输出OS Tick。如果创建软件定时器成功之后,使用rt_pm_request(PM_SLEEP_MODE_DEEP)请求深度睡眠模式。
#include <board.h>
#include <rtthread.h>
#include <rtdevice.h>
#ifndef RT_USING_TIMER_SOFT
#error "Please enable soft timer feature!"
#endif
#define TIMER_APP_DEFAULT_TICK (RT_TICK_PER_SECOND * 2)
#ifdef RT_USING_PM
static rt_timer_t timer1;
static void _timeout_entry(void *parameter)
{
rt_kprintf("current tick: %ld\n", rt_tick_get());
}
static int timer_app_init(void)
{
rt_pm_request(PM_SLEEP_MODE_IDLE);
rt_pm_request(PM_SLEEP_MODE_LIGHT);
timer1 = rt_timer_create("timer_app",
_timeout_entry,
RT_NULL,
TIMER_APP_DEFAULT_TICK,
RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
if (timer1 != RT_NULL)
{
rt_timer_start(timer1);
/* keep in timer mode */
rt_pm_request(PM_SLEEP_MODE_DEEP);
return 0;
}
else
{
return -1;
}
}
INIT_APP_EXPORT(timer_app_init);
#endif /* RT_USING_PM */
按下复位按键重启开发板,打开终端软件,我们可以看到有定时输出日志:
\ | /
- RT - Thread Operating System
/ | \ 4.0.1 build May 9 2019
2006 - 2019 Copyright by rt-thread team
msh >current tick: 2001
current tick: 4002
current tick: 6003
current tick: 8004
我们可以在msh里输入pm_dump观察PM组件的模式状态:
pm_dump
| Power Management Mode | Counter | Timer |
+-----------------------+---------+-------+
| None Mode | 0 | 0 |
| Idle Mode | 1 | 0 |
| LightSleep Mode | 1 | 0 |
| DeepSleep Mode | 1 | 1 |
| Standby Mode | 0 | 0 |
| Shutdown Mode | 0 | 0 |
+-----------------------+---------+-------+
pm current sleep mode: Idle Mode
pm current run mode: Normal Speed
msh >
以上的输出说明,PM组件里Idle、Light Sleep、Deep Sleep都被请求了一次,现在正处于空闲模式(Idle Mode)。
我们依次输入命令pm_release 1和pm_release 2手动释放Idle和Light Sleep模式后,将进入Deep Sleep Mode。进入Deep Sleep Mode之后会定时唤醒,shell 还是一直在输出。
按键唤醒应用
在唤醒按键应用里,使用wakeup按键来唤醒处于休眠模式的MCU。一般情况下,在MCU处于比较深度的休眠模式,只能通过特定的方式唤醒。
MCU唤醒之后,会触发相应的中断。
以下例程是从Deep Sleep模式唤醒MCU并闪烁LED之后,再次进入休眠的例程。
#include <board.h>
#include <rtthread.h>
#include <rtdevice.h>
#ifdef RT_USING_PM
#define WAKEUP_EVENT_BUTTON (1 << 0)
#define PIN_LED_R GET_PIN(E, 7)
#define WAKEUP_PIN GET_PIN(C, 13)
#define WAKEUP_APP_THREAD_STACK_SIZE 1024
static rt_event_t wakeup_event;
static void wakeup_callback(void *args)
{
rt_event_send(wakeup_event, WAKEUP_EVENT_BUTTON);
}
static void wakeup_init(void)
{
rt_pin_mode(WAKEUP_PIN, PIN_MODE_INPUT_PULLUP);
rt_pin_attach_irq(WAKEUP_PIN, PIN_IRQ_MODE_FALLING, wakeup_callback, RT_NULL);
rt_pin_irq_enable(WAKEUP_PIN, 1);
}
static void wakeup_app_entry(void *parameter)
{
wakeup_init();
rt_pm_request(PM_SLEEP_MODE_DEEP);
while (1)
{
if (rt_event_recv(wakeup_event,
WAKEUP_EVENT_BUTTON,
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, RT_NULL) == RT_EOK)
{
rt_pm_request(PM_SLEEP_MODE_NONE);
rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT);
rt_pin_write(PIN_LED_R, 0);
rt_thread_delay(rt_tick_from_millisecond(500));
rt_pin_write(PIN_LED_R, 1);
rt_pm_release(PM_SLEEP_MODE_NONE);
}
}
}
static int wakeup_app(void)
{
rt_thread_t tid;
wakeup_event = rt_event_create("wakup", RT_IPC_FLAG_PRIO);
RT_ASSERT(wakeup_event != RT_NULL);
tid = rt_thread_create("wakeup_app", wakeup_app_entry, RT_NULL,
WAKEUP_APP_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(tid != RT_NULL);
rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(wakeup_app);
#endif
创建一个线程,这个线程里注册了按键中断唤醒回调函数,接着请求深度睡眠,每当唤醒中断之后就会触发回调。回调函数里会发送WAKEUP_EVENT_BUTTON。这样我们的线程里接收到这个事件之后,首先请求在 None 模式,然后完成 LED 闪烁功能之后,再去释放 None 。
STM32L476
STM32L476是ST公司推出的一款超低功耗的Cortex-M4内核的MCU,支持多个电源管理模式,其中最低功耗为shutdown模式,待机仅30nA。
ST 公司 把 L476 的电管管理分为很多种,但各个模式的并非功耗逐级递减的特点,下面是各个模式之间的状态转换图:
尽管 STM32L476 的低功耗模式很多,但本质上并不复杂,理解它的原理有助于我们移植驱动,同时更好的在产品中选择合适的模式。
最终决定STM32L476系统功耗的主要是三个因素:稳压器(voltage regulator)、CPU工作频率、芯片自身低功耗的处理。
稳压器
L4使用两个嵌入式线性稳压器为所有数字电路、待机电路以及备份时钟域供电,分别是主稳压器(MR)和低功耗稳压器(LPR)。
稳压器在复位后处于使能状态,根据应用模式,选择不同的稳压器对Vcore域供电。
其中,MR的输出电压可以由软件配置为不同的范围(Range1 和 Range2)。
CPU工作频率
通过降低CPU的主频达到降低功耗的目的:MR在Range1正常模式时,SYSCLK最高可以工作在80M;MR工作在Range 2时,SYSCLK最高不能超过26M;低功耗运行模式和低功耗休眠模式,即Vcore域由LPR供电,SYSCLK必须小于2M。
芯片本身的低功耗处理
芯片本身 定义了一系列休眠模式:Sleep、Stop、Standby和Shutdown。前面的四种模式功耗逐级递减,实质是芯片内部通过关闭外设和时钟来实现。
RTT低功耗管理系统从设计上分离运行模式和休眠模式,独立管理,运行模式用于变频和变电压,休眠调用芯片的休眠特性。
对于多数芯片和开发来说,可能并不需要考虑变频和变电压,仅需关注休眠模式。
STM32 L4系列的芯片有运行模式和低功耗运行模式的概念,同时MR还有Range 2模式,可用于变频场景。
PM组件的底层功能都是通过struct rt_pm_ops结构体里的函数完成:
/**
* low power mode operations
*/
struct rt_pm_ops
{
void (*sleep)(struct rt_pm *pm, uint8_t mode);
void (*run)(struct rt_pm *pm, uint8_t mode);
void (*timer_start)(struct rt_pm *pm, rt_uint32_t timeout);
void (*timer_stop)(struct rt_pm *pm);
rt_tick_t (*timer_get_tick)(struct rt_pm *pm);
};
移植休眠模式仅需关注sleep接口,首先将RTT休眠模式和STM32模式做一个转换。
#include <board.h>
#include <rtthread.h>
#include <rtdevice.h>
static void sleep(struct rt_pm *pm, uint8_t mode)
{
switch (mode)
{
case PM_SLEEP_MODE_NONE:
break;
case PM_SLEEP_MODE_IDLE:
// __WFI();
break;
case PM_SLEEP_MODE_LIGHT:
/* Enter SLEEP Mode, Main regulator is ON */
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
break;
case PM_SLEEP_MODE_DEEP:
/* Enter STOP 2 mode */
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
break;
case PM_SLEEP_MODE_STANDBY:
/* Enter STANDBY mode */
HAL_PWR_EnterSTANDBYMode();
break;
case PM_SLEEP_MODE_SHUTDOWN:
/* Enter SHUTDOWNN mode */
HAL_PWREx_EnterSHUTDOWNMode();
break;
default:
RT_ASSERT(0);
break;
}
}
int rt_hw_pm_init(void)
{
static const struct rt_pm_ops _ops =
{
sleep,
RT_NULL,
RT_NULL,
RT_NULL,
RT_NULL
};
rt_uint8_t timer_mask = 0;
/* Enable Power Clock */
__HAL_RCC_PWR_CLK_ENABLE();
/* initialize system pm module */
rt_system_pm_init(&_ops, timer_mask, RT_NULL);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_pm_init);
移植时间补偿接口:
某些情况下,我们可能需要系统在空闲时进入 Stop 模式,以达到更低的将功耗效果。L476 Stop 2 模式下的电流可以达到 1.6 uA 左右,ST 手册上对 Stop2 模式的描述如下:
Stop 2 模式基于 Cortex-M4 深度睡眠模式与外设时钟门控。在 Stop 2 模式下, Vcore 域中的所有时钟都会停止, PLL、 MSI、 HSI16 和 HSE 振荡器也被禁止。一些带有唤醒功能(I2C3 和 LPUART)的外设可以开启 HSI16 以获取帧,如果该帧不是唤醒帧,也可以在接收到帧后关闭 HSI16。SRAM1、 SRAM2、 SRAM3 和寄存器内容将保留,所有 I/O 引脚的状态与运行模式下相同。
根据手册可知,Stop2模式会关闭系统时钟,当前的OS Tick基于内核的SysTick定时器。
那么在系统时钟停止后,OS Tick 也会停止,对于某些依赖 OS Tick 的应用,在进入 Stop 2 模式,又被中断唤醒后,就会出现问题,需要在系统唤醒后,对OS Tick进行补偿。
在Stop2模式下,绝大多数外设都停止工作,仅低功耗定时器1选择LSI作为时钟源后,仍然正常运行,所以选择LP_TIM1作为STOP2模式的时间补偿定时器。
休眠的时间补偿需要实现三个接口,分别用于启动低功耗定时器、停止、唤醒后获取休眠的Tick。
static void pm_timer_start(struct rt_pm *pm, rt_uint32_t timeout)
{
RT_ASSERT(pm != RT_NULL);
RT_ASSERT(timeout > 0);
/**
* 当超时为 RT_TICK_MAX 时,表明系统此时没有依赖 OS Tick 的应用,
* 因此不启动低功耗定时器,避免超时唤醒而增加系统功耗
*/
if (timeout != RT_TICK_MAX)
{
/* Convert OS Tick to pmtimer timeout value */
timeout = stm32l4_pm_tick_from_os_tick(timeout);
if (timeout > stm32l4_lptim_get_tick_max())
{
timeout = stm32l4_lptim_get_tick_max();
}
/* Enter PM_TIMER_MODE */
stm32l4_lptim_start(timeout);
}
}
static void pm_timer_stop(struct rt_pm *pm)
{
RT_ASSERT(pm != RT_NULL);
/* Reset pmtimer status */
stm32l4_lptim_stop();
}
static rt_tick_t pm_timer_get_tick(struct rt_pm *pm)
{
rt_uint32_t timer_tick;
RT_ASSERT(pm != RT_NULL);
timer_tick = stm32l4_lptim_get_current_tick();
return stm32l4_os_tick_from_pm_tick(timer_tick);
}
int rt_hw_pm_init(void)
{
static const struct rt_pm_ops _ops =
{
sleep,
RT_NULL,
pm_timer_start,
pm_timer_stop,
pm_timer_get_tick
};
rt_uint8_t timer_mask = 0;
/* Enable Power Clock */
__HAL_RCC_PWR_CLK_ENABLE();
/* initialize timer mask */
timer_mask = 1UL << PM_SLEEP_MODE_DEEP;
/* initialize system pm module */
rt_system_pm_init(&_ops, timer_mask, RT_NULL);
return 0;
}
休眠时间补偿的移植相对并不复杂,根据 Tick 配置低功耗定时器超时,唤醒后获取实际休眠时间并转换为OS Tick,告知 PM 组件即可。另外,从 Stop 2 模式唤醒后,默认会切换到内部的 MSI 时钟,通常需要重新配置时钟树。