基本完成操作系统

master
高宏宇 11 months ago
parent 8e93d0bc44
commit 851e9ec6db

@ -1,3 +1,5 @@
[TOC]
# 1. 概述
## 1.1. 软件编程模式
@ -369,14 +371,525 @@ const osThreadAttr_t comTask_attributes = { .name = "comTask", .stack_size = 128
### 3.1.2. 二值信号量的应用
建立一个按键执行任务,用于执行按键按下后的操作:翻转指示灯状态。按键的检测采用外部中断,中断和任务之间通过二值信号量实现同步。
#### 3.1.2.1. 配置
使用PA7作为LEDPB12button1作为按钮注意button1有外部上拉不用设置内部上拉
1. 新建一个工程取名Semaphore注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 设置PA7为输出并设置Label为LED
4. 设置PB12为外部中断Label为BUTTON
5. 在NVIC中配置EXTI 15:10 为启用;
6. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
7. 修改缺省的任务如下:
![image-20241010171519288](./img/image-20241010171519288.png)
8. 在 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
![image-20241010171627600](./img/image-20241010171627600.png)
9. 添加一个信号量:
![image-20241010171714699](./img/image-20241010171714699.png)
信号量的配置如下:
![image-20241010171737309](./img/image-20241010171737309.png)
其中 Initial State 为信号量的初始化状态,这里选择 Available 表示1
10. 保存并生成代码
#### 3.1.2.2. 代码编写
在main.c的 USER CODE BEGIN 0 位置加入按钮中断的回调:
```c
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == BUTTON_Pin)
osSemaphoreRelease(KeyBinarySemHandle);
}
/* USER CODE END 0 */
```
修改 **void** **StartLedTask**(**void** *argument) 函数,大约在 237 行,如下:
```c
void StartLedTask(void *argument) {
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;) {
osSemaphoreAcquire(KeyBinarySemHandle, osWaitForever);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
/* USER CODE END 5 */
}
```
#### 3.1.2.3. 代码分析
在配置中新建一个信号量后,主程序中自动添加了这些代码:
```c
osSemaphoreId_t KeyBinarySemHandle;
const osSemaphoreAttr_t KeyBinarySem_attributes = { .name = "KeyBinarySem" };
KeyBinarySemHandle = osSemaphoreNew(1, 1, &KeyBinarySem_attributes);
```
书上说 osSemaphoreNew 的第三个参数可以是 null在框架中自动生成了一个结构体描述
另外生成了 StartLedTask 任务函数的代码框架,只需要在任务中添加我们的逻辑就好了。
程序流程:
1. 在信号量配置的时候,初始值选择了 Available ,表示信号是可用的;
2. 操作系统在运行到 StartLedTask 的循环中,执行 osSemaphoreAcquire 去取得信号因为信号是1表示可用这时该函数立即返回并同时设置 KeyBinarySemHandle 信号量为0表示已经被占用
3. 随后执行灯的翻转缺省灯是0翻转后是1点亮LED
4. 再次循环,执行 osSemaphoreAcquire 函数获取信号因为上一次信号已经被占用这时的值只0该函数不能获取信号将被阻塞参数 osWaitForever 表示无限制时间);
5. 按钮按下,通过中断回调 HAL_GPIO_EXTI_Callback 函数中执行 osSemaphoreRelease 释放信号信号被设置成1
6. 通过任务调度StartLedTask 任务中的 osSemaphoreAcquire 返回后执行端口反转关闭LED然后再次进入第四步
### 3.1.3. 计数信号量
建立两个任务:发送任务负责连续发送信号量,指示灯任务在接收到信号量后,控制指示灯闪烁一下。具体代码如程序清单 10-6 所示。
#### 3.1.3.1. 配置
1. 新建一个工程取名SemaphoreN注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 设置PA7为输出并设置Label为LED
4. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
5. FreeRTOS 的 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
6. 在FreeRTOS 建立一个新的 Counting Semaphore 信号量:
![image-20241010201708218](./img/image-20241010201708218.png)
按照下图进行添加信号量:
![image-20241010201735093](./img/image-20241010201735093.png)
Max Count 是计数信号量的最大值Initial Count 表示计数信号量的初始值;
7. 添加两个任务:
![image-20241011111210093](./img/image-20241011111210093.png)![image-20241011111307902](./img/image-20241011111307902.png)
#### 3.1.3.2. 代码编写
补充 **StartSendTask** 函数:
```c
/* USER CODE END Header_StartSendTask */
void StartSendTask(void *argument) {
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;) {
osSemaphoreRelease(CountingSemHandle); // 第一次设置一个信号量信号量加1
osDelay(1000);
osSemaphoreRelease(CountingSemHandle); // 第二次设置两个信号量信号量加2
osSemaphoreRelease(CountingSemHandle);
osDelay(1000);
osSemaphoreRelease(CountingSemHandle); // 第三次设置三个信号量信号量加3
osSemaphoreRelease(CountingSemHandle);
osSemaphoreRelease(CountingSemHandle);
osDelay(3000);
}
/* USER CODE END 5 */
}
```
补充 **StartLedTask** 函数:
```c
/* USER CODE END Header_StartLedTask */
void StartLedTask(void *argument) {
/* USER CODE BEGIN StartLedTask */
/* Infinite loop */
for (;;) {
if (osSemaphoreAcquire(CountingSemHandle, osWaitForever) == osOK) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
osDelay(100);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
osDelay(100);
}
}
/* USER CODE END StartLedTask */
}
```
#### 3.1.3.3. 代码解析
1. 计数信号量表示可用资源的个数最大是10初始值是0表示没有可用资源如果计数信号量是0当调用osSemaphoreAcquire函数的时候将会阻塞一直到信号量大于0为止
2. 两个任务的优先级是一样的其实在程序刚刚开始运行的时候我们不确定到底是StartSendTask先运行还是StartLedTask先运行不过不影响整个流程我们能确定的是这两个任务几乎是并行的的
3. 如果 StartSendTask 先运行,调用一次 osSemaphoreRelease 函数后信号量从0变成了1此时StartLedTask 的 osSemaphoreAcquire 函数返回同时信号量减1LED闪烁一次当循环再次运行到 osSemaphoreAcquire 时,因为没有信号量,所有阻塞;
4. 第二秒开始StartSendTask 使用 osSemaphoreRelease 添加了两个信号量,因此 StartLedTask 控制LED闪烁两次
5. 第三秒开始StartSendTask 使用 osSemaphoreRelease 添加了3个信号量因此 StartLedTask 控制LED闪烁3次
6. 延迟3秒后从第三步再次开始。
## 3.2. 事件标志组
在信号量的应用中。我们发现信号量只能实现两个任务之间的同步,如果要实现多个任务之间的同步,则需要使用事件标志组。
事件标志组是多个二值信号的组合,其中每一个二值信号就是一个事件标志位(相当于一个二值信号量)用来表明某一个事件是否发生该标志位由一个相关的任务或ISR 置位。
事件标志组可以实现多个任务(包括ISR)协同控制一个任务,当各个相关任务的对应事件发生时,将设置事件标志组的对应标志位有效,进而触发对应的任务。
使用事件标志组同步的任务可以分为独立性同步(OR)和关联性同步(AND)。独立性同步表示等待的任何一个事件发生时,就可以触发任务。关联性同步表示等待的全部事件都发生时,才可以触发任务。事件标志组的基本原理如图 10-18 所示。
![image-20241011130650558](./img/image-20241011130650558.png)
FreeRTOS提供了事件标志组的功能每一个事件标志组最多具有24个事件标志位。经过 CMSIS-RTOS2封装后提供的常用接口数有三个
### 3.2.1. 函数接口
#### 3.2.1.1. 事件标志组创建函数(osEventFlagsNew)
该函数用于创建一个新的事件标志组具体描述如表10-11所示。
![image-20241011130917909](./img/image-20241011130917909.png)
#### 3.2.1.2. 事件标志组设置函数(osEventFlagsSet)
该函数用于设置指定事件标志组中的一个或多个的事件标志位具体描述如表10-12 所示。
![image-20241011131030296](./img/image-20241011131030296.png)
#### 3.2.1.3. 事件标志组等待函数(osEventFlagsWait)
该函数用于等待指定事件标志组中的一个或多个事件标志位执行时会暂停调用该函数的任务直到等待的事件标志位置位。具体描述如表10-13所示。
![image-20241011131134373](./img/image-20241011131134373.png)![image-20241011131145191](./img/image-20241011131145191.png)
### 3.2.2. 应用示例(和书上不一样)
#### 3.2.2.1. 配置
1. 新建一个工程取名EventFlag注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 设置PA7为输出并设置Label为LED
4. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
5. FreeRTOS 的 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
6. 配置两个任务:
![image-20241011191646983](./img/image-20241011191646983.png)![image-20241011191706915](./img/image-20241011191706915.png)
#### 3.2.2.2. 代码编写
补充 StartEventTask 代码:
```c
void StartEventTask(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;) {
osEventFlagsSet(LedEventFlagHandle, EVENT1);
osDelay(1000);
osEventFlagsSet(LedEventFlagHandle, EVENT2);
osDelay(1000);
osEventFlagsSet(LedEventFlagHandle, EVENT1);
osEventFlagsSet(LedEventFlagHandle, EVENT2);
osDelay(3000);
}
/* USER CODE END 5 */
}
```
补充 StartLedTask 代码:
```c
void StartLedTask(void *argument)
{
/* USER CODE BEGIN StartLedTask */
/* Infinite loop */
for (;;) {
osEventFlagsWait(LedEventFlagHandle, EVENT1 | EVENT2, osFlagsWaitAll, osWaitForever);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
osDelay(200);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
/* USER CODE END StartLedTask */
}
```
#### 3.2.2.3. 运行效果和程序分析
思考:程序运行的效果是什么?
分析两个任务的执行流程。
## 3.3. 线程标志
线程标志,也称为任务通知,它的功能与事件标志组类似,可以用于多个任务的同步。唯一的区别是线程标志不需要用户创建,每一个任务创建后就自动拥有一个线程标志。任务的线程标志只能由任务自身等待,由其他任务设置。而事件标志组则相当于一个公用的资源,任何任务都可以设置或等待。
FreeRTOS提供了线程标志的功能每一个线程标志具有31个线程标志位(每一个线程标志位相当于一个二值信号量 )。经过 CMSIS-RTOS2封装后提供的常用接口函数有两个。
### 3.3.1. 函数接口
#### 3.3.1.1. 线程标志设置函数(osThreadFlagsSet)
![image-20241011192352596](./img/image-20241011192352596.png)
#### 3.3.1.2. 线程标志等待函数(osThreadFlagsWait)
![image-20241011192458298](./img/image-20241011192458298.png)
![image-20241011192528221](./img/image-20241011192528221.png)
### 3.3.2. 应用示例(书上代码不完整)
#### 3.3.2.1. 配置
1. 新建一个工程取名ThreadFlag注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
5. FreeRTOS 的 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
6. 修改缺省任务的名称:
![image-20241011201444075](./img/image-20241011201444075.png)
7. 打开串口2并使能串口2的全局中断
![image-20241011201300601](./img/image-20241011201300601.png)
#### 3.3.2.2. 代码
在 USER CODE BEGIN 0 代码段中加入:
```c
/* USER CODE BEGIN 0 */
uint8_t RxBuffer[10];
// 中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART2) {
osThreadFlagsSet(ComTaskHandle, 0x01);
HAL_UART_Receive_IT(&huart2, RxBuffer, 10); // 再次启动中断接收
}
}
/* USER CODE END 0 */
```
完善 **StartComTask** 任务代码:
```c
/* USER CODE END Header_StartComTask */
void StartComTask(void *argument) {
/* USER CODE BEGIN 5 */
HAL_UART_Receive_IT(&huart2, RxBuffer, 10); // 启动中断接收
/* Infinite loop */
for (;;) {
osThreadFlagsWait(0x01, osFlagsWaitAny, osWaitForever);
HAL_UART_Transmit(&huart2, RxBuffer, 10, 100);
}
/* USER CODE END 5 */
}
```
#### 3.3.2.3. 代码分析和效果
问题:
1. StartComTask 函数中的 HAL_UART_Receive_IT(&huart2, RxBuffer, 10) 的目的是什么?可以写在其他地方吗?
2. HAL_UART_RxCpltCallback 的作用是什么?
3. 程序实现什么效果?
4. 程序的逻辑和流程是什么?
## 3.4. 互斥量
在RTOS中实现临界区的互斥访问一般使用互斥量。互斥量本质上也是一个二值信号量二者的不同之处在于二值信号量主要是实现任务的同步通常是一个任务申请另一个任务释放。而互斥量主要是实现对共享资源的互斥访问通常是同一个任务申请同一个任务释放。
### 3.4.1. 接口函数
#### 3.4.1.1. 互斥量创建函数(osMutexNew)
![image-20241011202359570](./img/image-20241011202359570.png)
#### 3.4.1.2. 互斥量获取函数(osMutexAcquire)
![image-20241011202452649](./img/image-20241011202452649.png)
#### 3.4.1.3. 互斥量释放函数(osMutexRelease
![image-20241011202535381](./img/image-20241011202535381.png)
### 3.4.2. 应用示例
建立两个任务分别通过串口向PC发送数据利用互斥量实现两个任务对串口的互斥访问。具体代码如程序清单10-9所示。
#### 3.4.2.1. 配置
1. 新建一个工程取名Mutex注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 打开串口2不用设置中断模式
4. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
5. FreeRTOS 的 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
6. 建立一个互斥量:
![image-20241011203518319](./img/image-20241011203518319.png)
7. 建立两个任务:
![image-20241011203618054](./img/image-20241011203618054.png)![image-20241011203643871](./img/image-20241011203643871.png)
#### 3.4.2.2. 代码编写
在 USER CODE BEGIN 0 中加入以下代码:
```c
/* USER CODE BEGIN 0 */
char * MSG1="Message from task1\n";
char * MSG2="Message from task2\n";
/* USER CODE END 0 */
```
补充 StartComTask1 代码:
```c
/* USER CODE END Header_StartComTask1 */
void StartComTask1(void *argument) {
/* USER CODE BEGIN 5 */
/* Infinite loop */
for (;;) {
osMutexAcquire(UartMutexHandle, osWaitForever);
HAL_UART_Transmit(&huart2, (uint8_t*) MSG1, 19, 100);
osMutexRelease(UartMutexHandle);
osDelay(1000);
}
/* USER CODE END 5 */
}
```
补充 StartComTask2 代码:
```c
/* USER CODE END Header_StartComTask2 */
void StartComTask2(void *argument) {
/* USER CODE BEGIN StartComTask2 */
/* Infinite loop */
for (;;) {
osMutexAcquire(UartMutexHandle, osWaitForever);
HAL_UART_Transmit(&huart2, (uint8_t*) MSG2, 19, 100);
osMutexRelease(UartMutexHandle);
osDelay(1000);
}
/* USER CODE END StartComTask2 */
}
```
#### 3.4.2.3. 代码分析
1. 运行后是什么效果
2. 互斥量在使用上和信号量有什么区别?
## 3.5. 消息队列
使用信号量、事件标志组和线程标志进行任务同步时,只能提供同步的时刻信息,无法在任务之间进行数据传输。要实现任务间的数据传输,一般使用两种方式:
1. 全局变量在RTOS中使用全局变量时必须保证每个任务对全局变量的互斥访问一般借助互斥量来实现。另一个方法是在任务设计时设计成只有一个任务修改这个全局变量其他任务只是读取这个全局变量而不修改它的值并在全局变量前面加上 volatile 的关键字修饰,以避免编译器的优化。
2. 消息队列消息队列类似于一个数据缓冲区,可以保存有限个、具有确定大小的数据。通常情况下,消息队列按照 FIFO(先进先出)的模式使用,即数据由队尾写人,从队首读出。任务向消息队列中放人消息时,需要判断消息队列是否有多余的空间:如果有空间则放入一个新的消息;如果消息队列已经存满,该任务将进入到阻塞态,直到消息队列中有多余的空间。任务从消息队列中获取消息时,需要判断消息队列是否有消息:如果消息队列中没有消息,该任务将进入到阻塞态。当消息队列中有新的消息时,处于阻塞态的任务将被唤醒并获得该消息。任务在获取消息时,需要提前定义存放消息的缓冲区,这个缓冲区的大小不能小于消息队列中单个消息的大小。
注意:FreeRTOS利用消息队列进行消息传递时放入消息队列的是实际的数据,而不是数据的地址。例如串口一次接收10字节的数据如果使用消息队列来传递串口接收的数据则应该将消息队列的单个消息大小设置为10字节以便一次性存放串口接收的10 字节数据。
消息队列和全局变量相比解决了多任务访问共享资源的冲突问题还提供了任务的同步和超时处理等机制并且可以实现中断服务程序和任务之间的数据传递。例如多个任务都要使用串口进行数据传输时可以采用两种方法一种方法是利用互斥量实现对串口的互斥访问另一种方法是创建一个消息队列和一个负责串口数据收发的任务。任务A发送的数据放入消息队列任务B发送的数据也放入消息队列串口发送任务则按照 FIFO的原则从消息队列中取出消息发送。
在实际应用时,由于消息队列采用数据复制的方式传输数据,而不是传输存放数据的地址。如果任务间传输的数据量较大时,使用消息队列的效率会比较低。这时,可以考虑使用全局变量来实现任务问的通信,只是要注意全局变量的互斥访问(利用互斥量实现)。
### 3.5.1. 接口函数
#### 3.5.1.1. 消息队列创建函数(osMessageQueueNew)
![image-20241011210019721](./img/image-20241011210019721.png)
#### 3.5.1.2. 消息放入函数(osMessageQueuePut)
![image-20241011210108534](./img/image-20241011210108534.png)
![image-20241011210124549](./img/image-20241011210124549.png)
#### 3.5.1.3. 消息获取函数(osMessageQueueGet)
![image-20241011210216122](./img/image-20241011210216122.png)
![image-20241011210233299](./img/image-20241011210233299.png)
### 3.5.2. 应用示例
利用消息队列传输串口接收的数据。串口采用中断方式接收10字节的数据并放入消息队列。数据处理任务从消息队列中取出数据并发送到PC显示。消息队列设置为可以容纳5个消息,每个消息的大小为10字节。
#### 配置
1. 新建一个工程取名MsgQueue注意需要设置SYS中的Debug为 Serial Wire另外固件版本可能需要调整1.8.6的有问题;
2. 同样在 SYS 中配置Timebase Source 为一个没有使用的定时器如TIM4
3. 打开串口2并打开串口全局中断
4. 配置中间件打开FREERTOSInterface设置成 CMSIS_V2
5. FreeRTOS 的 Advanced settings 中打开 NEWLIB 的选项,否则要提示警告;
6. 建立一个任务:
![image-20241011214503936](./img/image-20241011214503936.png)
7. 建立一个消息队列:
![image-20241011214604355](./img/image-20241011214604355.png)
这里的Queue Size 表示队列的长度Item Size 表示队列中每个元素的长度(字节);
#### 代码
在 USER CODE BEGIN 0 段加入代码:
```c
/* USER CODE BEGIN 0 */
uint8_t RxBuffer[10];
uint8_t TxBuffer[10];
// 中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
osMessageQueuePut(ComQueueHandle, RxBuffer, 0, osWaitForever);
HAL_UART_Receive_IT(&huart2, RxBuffer, 10);
}
/* USER CODE END 0 */
```
完善 **StartProcessTask** 任务函数:
```c
/* USER CODE END Header_StartProcessTask */
void StartProcessTask(void *argument) {
/* USER CODE BEGIN 5 */
HAL_UART_Receive_IT(&huart2, RxBuffer, 10);
/* Infinite loop */
for (;;) {
if (osMessageQueueGet(ComQueueHandle, TxBuffer, NULL, osWaitForever)
== osOK) {
HAL_UART_Transmit(&huart2, TxBuffer, 10, 100);
}
}
/* USER CODE END 5 */
}
```
#### 运行效果和代码分析
不出意外,出意外了;把:
```c
osMessageQueuePut(ComQueueHandle, RxBuffer, 0, osWaitForever);
```
修改成:
```c
osMessageQueuePut(ComQueueHandle, RxBuffer, 0, 0);
```
成功。
为什么?无解。
## 3.6. 软件定时器

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Loading…
Cancel
Save