RTT操作系统(4)
RTT操作系统(4)
本笔记为作者再学习RTT操作系统的一些心得体会,如有不对的地方,请包含与谅解!
————by wsoz
由于I/O框架的重要性我们对其单开了一章,这一章我们继续RTT的学习
PIN设备
首先与通用的 rt_device_read()
不同,PIN 设备框架为了使用的便捷性,封装了一套更直观的 API,如 rt_pin_read(pin_num)
。
PIN设备就是我们常用的GPIO,对于GPIO的特性由下放展示
- 可编程控制中断:中断触发模式可配置,一般有下图所示 5 种中断触发模式:
=
- 输入输出模式可控制。
- 输出模式一般包括:推挽、开漏、上拉、下拉。引脚为输出模式时,可以通过配置引脚输出的电平状态为高电平或低电平来控制连接的外围设备。
- 输入模式一般包括:浮空、上拉、下拉、模拟。引脚为输入模式时,可以读取引脚的电平状态,即高电平或低电平。
PIN设备操作
由于官方对PIN设备进行了更加直观的封装,我们需要了解的操作接口有下面的几个:
函数 | 描述 |
---|---|
rt_pin_get() | 获取引脚编号 |
rt_pin_mode() | 设置引脚模式 |
rt_pin_write() | 设置引脚电平 |
rt_pin_read() | 读取引脚电平 |
rt_pin_attach_irq() | 绑定引脚中断回调函数 |
rt_pin_irq_enable() | 使能引脚中断 |
rt_pin_detach_irq() | 脱离引脚中断回调函数 |
获取引脚编号
原型:
rt_base_t rt_pin_get(const char *name) //引脚名
GET_PIN(port, pin) //宏定义方法
RT-Thread 提供的引脚编号需要和芯片的引脚号区分开来,它们并不是同一个概念,引脚编号由 PIN 设备驱动程序定义,和具体的芯片相关。该接口返回的是RTT内部定义的引脚号。
示例:
rt_base_t gpio_num1;
gpio_num1=rt_pin_get("PF.9");
//or
gpio_num1=GET_PIN(F,9);
设置引脚模式
原型:
void rt_pin_mode(rt_base_t pin, rt_base_t mode); //引脚与模式
模式选择:
#define PIN_MODE_OUTPUT 0x00 /* 输出 */
#define PIN_MODE_INPUT 0x01 /* 输入 */
#define PIN_MODE_INPUT_PULLUP 0x02 /* 上拉输入 */
#define PIN_MODE_INPUT_PULLDOWN 0x03 /* 下拉输入 */
#define PIN_MODE_OUTPUT_OD 0x04 /* 开漏输出 */
设置引脚电平
原型:
void rt_pin_write(rt_base_t pin, rt_base_t value); //引脚与电平
电平选择:
#define PIN_LOW 0x00
#define PIN_HIGH 0x01
读取引脚电平
原型:
int rt_pin_read(rt_base_t pin); //引脚
返回高电平1–低电平0
绑定引脚中断回调函数
原型:
rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode, //引脚 中断触发模式void (*hdr)(void *args), void *args); //回调函数 参数
中断触发模式选择:
#define PIN_IRQ_MODE_RISING 0x00 /* 上升沿触发 */
#define PIN_IRQ_MODE_FALLING 0x01 /* 下降沿触发 */
#define PIN_IRQ_MODE_RISING_FALLING 0x02 /* 边沿触发(上升沿和下降沿都触发)*/
#define PIN_IRQ_MODE_HIGH_LEVEL 0x03 /* 高电平触发 */
#define PIN_IRQ_MODE_LOW_LEVEL 0x04 /* 低电平触发 */
返回值:
返回 | 描述 |
---|---|
RT_EOK | 绑定成功 |
错误码 | 绑定失败 |
使能引脚中断
原型:
rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled); //引脚 使能标志位
//PIN_IRQ_ENABLE--开启 PIN_IRQ_DISABLE--关闭
返回值:
返回 | 描述 |
---|---|
RT_EOK | 使能成功 |
错误码 | 使能失败 |
脱离引脚中断回调函数
原型:
rt_err_t rt_pin_detach_irq(rt_int32_t pin); //引脚
引脚脱离了中断回调函数以后,中断并没有关闭,还可以调用绑定中断回调函数再次绑定其他回调函数。
返回值:
返回 | 描述 |
---|---|
RT_EOK | 脱离成功 |
错误码 | 脱离失败 |
按键移植实例
key.c
#include <rtthread.h>
#include "key_app.h"
#include <rtdevice.h>
#define DBG_TAG "key_app"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>static rt_base_t key1=0;
uint8_t key_read(void) //按键扫描
{uint8_t temp=0;if(rt_pin_read(key1)==1)temp=1;return temp;
}void key_thread(void*param)
{static uint8_t key_val,key_down,key_up,key_old;while(1){key_val=key_read();key_down=key_val&(key_old^key_val);key_up=~key_val&(key_old^key_val);key_old=key_val;switch (key_down){case 1:LOG_I("Key1 down\r\n");break;}rt_thread_mdelay(10);}
}void key_init(void)
{//按键初始化key1 = rt_pin_get("PA.0");rt_pin_mode(key1, PIN_MODE_INPUT_PULLUP);//线程初始化static rt_thread_t key_thread_t=NULL;key_thread_t=rt_thread_create("key", key_thread, NULL, 1024, 10, 10);rt_thread_startup(key_thread_t);
}
LED移植实例
led.c
#include <rtthread.h>
#include <rtdevice.h> // For rt_pin_get, rt_pin_mode, rt_pin_write
#include <rtdbg.h> // For LOG_I, LOG_E#include "led_app.h" // 包含自身头文件#define DBG_TAG "led_app"
#define DBG_LVL DBG_LOGled_t led1_handle;
// 最大LED数量
#define MAX_LED_DEVICES 8// LED设备状态结构体
struct led_device
{const char *name; // LED名称rt_base_t pin; // 引脚号rt_base_t active_level; // LED的激活电平 (PIN_HIGH 或 PIN_LOW)rt_uint8_t current_mode; // 当前工作模式 (LED_CMD_ON, LED_CMD_OFF, LED_CMD_BLINK)rt_uint8_t current_pin_state; // 当前引脚物理状态 (PIN_HIGH 或 PIN_LOW)// 闪烁模式相关参数rt_uint32_t blink_on_ms; // 亮灯持续时间 (ms)rt_uint32_t blink_off_ms; // 灭灯持续时间 (ms)rt_tick_t last_toggle_tick; // 上次切换状态的系统时钟
};// 全局LED设备数组
static struct led_device led_devices[MAX_LED_DEVICES];
static rt_uint8_t led_count = 0; // 已注册的LED数量// LED管理线程句柄
static rt_thread_t led_thread_t = RT_NULL;// ---------------------- 内部帮助函数 ----------------------// 根据LED的激活电平设置引脚状态
static void _set_led_physical_state(struct led_device *led, rt_uint8_t state)
{if (led->active_level == PIN_HIGH){// 高电平亮rt_pin_write(led->pin, state);}else // LED_ACTIVE_LOW{// 低电平亮rt_pin_write(led->pin, (state == PIN_HIGH) ? PIN_LOW : PIN_HIGH);}led->current_pin_state = state; // 记录的是逻辑状态,PIN_HIGH表示亮,PIN_LOW表示灭
}// ---------------------- LED控制线程 ----------------------static void led_thread_entry(void *parameter)
{rt_tick_t current_tick;while (1){current_tick = rt_tick_get();for (rt_uint8_t i = 0; i < led_count; i++){struct led_device *led = &led_devices[i];if (led->current_mode == LED_CMD_BLINK){// 计算当前状态持续了多长时间rt_uint32_t elapsed_time = (current_tick - led->last_toggle_tick) * 1000 / RT_TICK_PER_SECOND;if (led->current_pin_state == PIN_HIGH) // 当前是亮灯状态{if (elapsed_time >= led->blink_on_ms){// 亮灯时间到,切换到灭灯_set_led_physical_state(led, PIN_LOW);led->last_toggle_tick = current_tick;}}else // 当前是灭灯状态 (PIN_LOW){if (elapsed_time >= led->blink_off_ms){// 灭灯时间到,切换到亮灯_set_led_physical_state(led, PIN_HIGH);led->last_toggle_tick = current_tick;}}}// ON/OFF模式的LED不需要线程处理,直接由_set_led_physical_state控制}rt_thread_mdelay(10); // 周期性扫描,例如10ms一次,用于处理闪烁}
}// ---------------------- 外部API实现 ----------------------led_t led_register(const char *name, const char *pin_name, rt_base_t active_level)
{if (led_count >= MAX_LED_DEVICES){LOG_E("Max LED devices reached. Cannot register %s.", name);return RT_NULL;}struct led_device *new_led = &led_devices[led_count];new_led->pin = rt_pin_get(pin_name);if (new_led->pin == -1){LOG_E("Failed to get pin number for %s (%s).", name, pin_name);return RT_NULL;}new_led->name = name;new_led->active_level = active_level;new_led->current_mode = LED_CMD_OFF; // 默认关闭new_led->blink_on_ms = 0;new_led->blink_off_ms = 0;new_led->last_toggle_tick = 0;rt_pin_mode(new_led->pin, PIN_MODE_OUTPUT);_set_led_physical_state(new_led, PIN_LOW); // 默认设置为灭led_count++;LOG_I("LED '%s' registered on pin %s (pin_id: %d, active_level: %s).",name, pin_name, new_led->pin, (active_level == PIN_HIGH) ? "HIGH" : "LOW");return (led_t)new_led;
}rt_err_t led_control(led_t led_handle, rt_uint8_t cmd, rt_uint32_t on_ms, rt_uint32_t off_ms)
{if (led_handle == RT_NULL){LOG_E("Invalid LED handle passed to led_control.");return -RT_EINVAL;}struct led_device *led = (struct led_device *)led_handle;// 如果模式切换,则更新last_toggle_tick以避免闪烁延迟if (led->current_mode != cmd){led->last_toggle_tick = rt_tick_get();}led->current_mode = cmd; // 更新模式switch (cmd){case LED_CMD_ON:_set_led_physical_state(led, PIN_HIGH); // 逻辑高,即亮LOG_D("LED '%s' set to ON.", led->name);break;case LED_CMD_OFF:_set_led_physical_state(led, PIN_LOW); // 逻辑低,即灭LOG_D("LED '%s' set to OFF.", led->name);break;case LED_CMD_BLINK:if (on_ms == 0 || off_ms == 0){LOG_W("LED '%s' blink parameters on_ms(%d) or off_ms(%d) invalid. Setting to ON.", led->name, on_ms, off_ms);_set_led_physical_state(led, PIN_HIGH); // 如果参数不合法,则直接亮led->current_mode = LED_CMD_ON;return -RT_EINVAL;}led->blink_on_ms = on_ms;led->blink_off_ms = off_ms;// 首次进入闪烁,立即亮灯_set_led_physical_state(led, PIN_HIGH);led->last_toggle_tick = rt_tick_get(); // 重置计时器LOG_D("LED '%s' set to BLINK (on:%dms, off:%dms).", led->name, on_ms, off_ms);break;default:LOG_W("Unknown LED command %d for LED '%s'.", cmd, led->name);return -RT_EINVAL;}return RT_EOK;
}int led_init(void)
{//初始化LEDled1_handle = led_register("board_led", "PB.2", LED_ACTIVE_HIGH);led_control(led1_handle, LED_CMD_OFF, 0, 0);// 创建LED管理线程led_thread_t = rt_thread_create("led_ctrl",led_thread_entry,RT_NULL,512, // 栈大小20, // 优先级,可以较低1); // 时间片rt_thread_startup(led_thread_t);return 0;
}
led.h
#ifndef APPLICATIONS_LED_APP_H_
#define APPLICATIONS_LED_APP_H_#include <rtthread.h> // 需要 rt_base_t 等类型
#include "bsp_sys.h"// LED 命令
#define LED_CMD_ON 1 // 开启LED
#define LED_CMD_OFF 2 // 关闭LED
#define LED_CMD_BLINK 3 // LED闪烁// LED 激活电平(取决于硬件连接)
#define LED_ACTIVE_HIGH PIN_HIGH // 高电平亮
#define LED_ACTIVE_LOW PIN_LOW // 低电平亮// LED 设备句柄(对于外部使用者透明,不需要知道内部结构)
typedef struct led_device *led_t;/*** @brief LED初始化函数,根据传入的LED信息注册并初始化** @param name LED名称(例如 "led1", "led2")* @param pin_name 引脚名称(例如 "PB.2", "PC.3")* @param active_level LED的激活电平(LED_ACTIVE_HIGH 或 LED_ACTIVE_LOW)* @return 成功返回LED句柄,失败返回RT_NULL*/
led_t led_register(const char *name, const char *pin_name, rt_base_t active_level);/*** @brief 控制LED的函数** @param led LED句柄* @param cmd 控制命令 (LED_CMD_ON, LED_CMD_OFF, LED_CMD_BLINK)* @param on_ms 闪烁模式下亮灯时长 (ms), 非闪烁模式忽略* @param off_ms 闪烁模式下灭灯时长 (ms), 非闪烁模式忽略* @return RT_EOK 成功, 其他失败*/
rt_err_t led_control(led_t led, rt_uint8_t cmd, rt_uint32_t on_ms, rt_uint32_t off_ms);/*** @brief 整个LED模块的初始化函数(在系统启动时调用)** @return 成功返回0,失败返回负数*/
int led_init(void);#endif /* APPLICATIONS_LED_APP_H_ */
UART设备
串口设备官方就没有进行特定的封装,直接就通过上一章的I/O管理框架定义的标准接口进行访问使用,具体接口如下:
函数 | 描述 |
---|---|
rt_device_find() | 查找设备 |
rt_device_open() | 打开设备 |
rt_device_read() | 读取数据 |
rt_device_write() | 写入数据 |
rt_device_control() | 控制设备 |
rt_device_set_rx_indicate() | 设置接收回调函数 |
rt_device_set_tx_complete() | 设置发送完成回调函数 |
rt_device_close() | 关闭设备 |
使用的串口引脚应该在board.h
中去修改我们的串口宏定义,我们的shell使用的是串口1在后面的代码演示中我们使用串口2来进行代码的编写。
讲解一下整个框架的实现:
首先在board.h
中去添加宏定义如下
/** After configuring corresponding UART or UART DMA, you can use it.** STEP 1, define macro define related to the serial port opening based on the serial port number* such as #define BSP_USING_UART1** STEP 2, according to the corresponding pin of serial port, define the related serial port information macro* such as #define BSP_UART1_TX_PIN "PA9"* #define BSP_UART1_RX_PIN "PA10"** STEP 3, if you want using SERIAL DMA, you must open it in the RT-Thread Settings.* RT-Thread Setting -> Components -> Device Drivers -> Serial Device Drivers -> Enable Serial DMA Mode** STEP 4, according to serial port number to define serial port tx/rx DMA function in the board.h file* such as #define BSP_UART1_RX_USING_DMA**/#define BSP_USING_UART1
#define BSP_UART1_TX_PIN "PA9"
#define BSP_UART1_RX_PIN "PA10"#define BSP_USING_UART2
#define BSP_UART2_TX_PIN "PA2"
#define BSP_UART2_RX_PIN "PA3"
之后这个宏定义就会开启我们uart.config
对应串口的配置项,进一步影响到我们底层drv_uart.c
文件中对于串口的时钟初始化等。
串口移植实例
常用的串口使用方式:1.中断接收加轮询发送 2. DMA接收加轮询发送
默认底层的配置是
static struct serial_configure uart_config =
{BAUD_RATE_115200, // 默认波特率DATA_BITS_8, // 默认数据位STOP_BITS_1, // 默认停止位PARITY_NONE, // 默认无校验BIT_ORDER_LSB,NRZ_NORMAL,RT_SERIAL_RB_BUFSZ, // 接收缓冲区大小,通常由 rtconfig.h 中的 RT_SERIAL_RB_BUFSZ 定义0
};
中断接收加轮询发送
#include <rtthread.h>
#include <rtdevice.h> // For rt_pin_get, rt_pin_mode, rt_pin_write
#include <rtdbg.h> // For LOG_I, LOG_E#include "uart_app.h" // 包含自身头文件#define DBG_TAG "uart_app"
#define DBG_LVL DBG_LOG//中断接收加轮询发送static rt_device_t uart2=NULL;
//static rt_device_t uart1=NULL;
static rt_sem_t uart2_sem=NULL;
static rt_thread_t uart2_thread=NULL;
#define UART_RX_BUFFER_SIZE 64rt_err_t uart2_handler(rt_device_t dev,rt_size_t size)
{rt_sem_release(uart2_sem); //释放信号量,用于统计接收字符数return RT_EOK;
}//void uart2_read_thread(void*param)
//{
// static char ch;
// while(1)
// {
// rt_sem_take(uart2_sem, RT_WAITING_FOREVER);
// rt_device_read(uart2, 0, &ch, 1);
// rt_device_write(uart2, 0, &ch, 1);
// }
//}void uart2_read_thread(void* param)
{rt_uint8_t rx_buffer[UART_RX_BUFFER_SIZE]; // 使用一个数组作为缓冲区rt_size_t len;while(1){rt_sem_take(uart2_sem, RT_WAITING_FOREVER);while (1){// 尝试一次性读取最多 UART_RX_BUFFER_SIZE 个字节len = rt_device_read(uart2, 0, rx_buffer, sizeof(rx_buffer)); // 假设设备句柄名为 uart2if (len > 0){rt_device_write(uart2, 0, rx_buffer, len);// rt_device_write(uart1, 0, rx_buffer, len); //测试回显到串口1// TODO: 在这里处理接收到的数据,例如解析协议,转发等}}}
}int uart2_init(void)
{//串口初始化uart2=rt_device_find("uart2");//uart1=rt_device_find("uart1"); //测试发送到串口1rt_device_open(uart2, RT_DEVICE_FLAG_INT_RX); //以中断模式接收rt_device_set_rx_indicate(uart2, uart2_handler);//信号量创建uart2_sem=rt_sem_create("uart2_sem", 0, RT_IPC_FLAG_PRIO);//线程创建uart2_thread=rt_thread_create("uart2_thd", uart2_read_thread, NULL, 1024, 12, 100);rt_thread_startup(uart2_thread);return 0;
}
注意:我们注释掉的线程可能会出现丢包的情况同时效率地下,无论有多少个字节到达,这个回调函数都只导致信号量计数增加 1。rx_indicate
是 RT-Thread 串口驱动层向应用层发送的**“有数据到达”的通知**,而不是“每个字符到达”的事件。同时每次进入线程只读取一个缓存数据效率低,我们该用一次性直接读取我们的缓存区数据。
DMA接收加轮询发送
首先要使用DMA进行传输就要在 RT-Thread Setting
中将那个串口的DMA打开,然后在 board.h
中进行下面的宏定义
#define BSP_UART2_RX_USING_DMA
#define BSP_USING_UART2
#define BSP_UART2_TX_PIN "PA2"
#define BSP_UART2_RX_PIN "PA3"
最后我们底层的代码其实只需要修改一下open函数的一个标志即可,当然在官方的文档中使用的是消息队列传输方式
static rt_device_t uart2=NULL;
//static rt_device_t uart1=NULL;
static rt_sem_t uart2_sem=NULL;
static rt_thread_t uart2_thread=NULL;
#define UART_RX_BUFFER_SIZE 64rt_err_t uart2_handler(rt_device_t dev,rt_size_t size)
{rt_sem_release(uart2_sem); //释放信号量,用于统计接收字符数return RT_EOK;
}void uart2_read_thread(void* param)
{rt_uint8_t rx_buffer[UART_RX_BUFFER_SIZE]; // 使用一个数组作为缓冲区rt_size_t len;while(1){rt_sem_take(uart2_sem, RT_WAITING_FOREVER);while (1){// 尝试一次性读取最多 UART_RX_BUFFER_SIZE 个字节len = rt_device_read(uart2, 0, rx_buffer, sizeof(rx_buffer)); // 假设设备句柄名为 uart2if (len > 0){rt_device_write(uart2, 0, rx_buffer, len);// rt_device_write(uart1, 0, rx_buffer, len); //测试回显到串口1// TODO: 在这里处理接收到的数据,例如解析协议,转发等}}}
}int uart2_init(void)
{//串口初始化uart2=rt_device_find("uart2");//uart1=rt_device_find("uart1"); //测试发送到串口1rt_device_open(uart2, RT_DEVICE_FLAG_DMA_RX); //以DMA接收rt_device_set_rx_indicate(uart2, uart2_handler);//信号量创建uart2_sem=rt_sem_create("uart2_sem", 0, RT_IPC_FLAG_PRIO);//线程创建uart2_thread=rt_thread_create("uart2_thd", uart2_read_thread, NULL, 1024, 12, 100);rt_thread_startup(uart2_thread);return 0;
}
DMA加环形缓存区加超时解析
下面的代码是直接使用超时解析,让线程定时启动。
static rt_device_t uart2=NULL;
//static rt_device_t uart1=NULL;
static rt_thread_t uart2_thread=NULL;
#define UART_RX_BUFFER_SIZE 256
static struct rt_ringbuffer uart2_ringbuffer; //环形缓存区结构体
static uint8_t uart2_rx_ringbuffer[UART_RX_BUFFER_SIZE]; //环形缓存区rt_err_t uart2_handler(rt_device_t dev,rt_size_t size)
{rt_size_t len;rt_uint8_t rx_buffer[UART_RX_BUFFER_SIZE]; // 使用一个数组作为缓冲区len = rt_device_read(uart2, 0, rx_buffer, sizeof(rx_buffer)); // 将接收缓存区数据移到暂时缓存rt_ringbuffer_put(&uart2_ringbuffer, rx_buffer, len); //将暂时缓冲区的数据移到环形缓存区return RT_EOK;
}void uart2_read_thread(void* param)
{rt_uint8_t readbuffer[UART_RX_BUFFER_SIZE]; // 读取数组避免直接操作环形缓存区static uint16_t data_size=0;//数据大小static uint32_t uart2_tick=0; //当前时间static uint32_t uart2_last_tick=0; //上次接收时间while(1){uart2_tick=rt_tick_get(); //获取当前时间data_size=rt_ringbuffer_data_len(&uart2_ringbuffer);if(uart2_tick-uart2_last_tick>10&&data_size>0){rt_ringbuffer_get(&uart2_ringbuffer, readbuffer, data_size);rt_device_write(uart2, 0, readbuffer, data_size); //回显数据//此处进行解析操作uart2_last_tick=uart2_tick; //更新时间}rt_thread_mdelay(10);}
}int uart2_init(void)
{//串口初始化uart2=rt_device_find("uart2");//uart1=rt_device_find("uart1"); //测试发送到串口1rt_device_open(uart2, RT_DEVICE_FLAG_DMA_RX); //以DMA接收rt_device_set_rx_indicate(uart2, uart2_handler);//环形缓存区创建rt_ringbuffer_init(&uart2_ringbuffer, uart2_rx_ringbuffer, UART_RX_BUFFER_SIZE);//线程创建uart2_thread=rt_thread_create("uart2_thd", uart2_read_thread, NULL, 1024, 12, 100);rt_thread_startup(uart2_thread);return 0;
}
我们继续优化一下同样使用信号量环形加超时解析的方案,这样可以提高我们系统的实时性同时减少不必要的CPU资源消耗
static rt_device_t uart2=NULL;
static rt_sem_t uart2_sem=NULL;
//static rt_device_t uart1=NULL;
static rt_thread_t uart2_thread=NULL;
#define UART_RX_BUFFER_SIZE 256
static struct rt_ringbuffer uart2_ringbuffer; //环形缓存区结构体
static uint8_t uart2_rx_ringbuffer[UART_RX_BUFFER_SIZE]; //环形缓存区rt_err_t uart2_handler(rt_device_t dev,rt_size_t size)
{rt_size_t len;rt_uint8_t rx_buffer[UART_RX_BUFFER_SIZE]; // 使用一个数组作为缓冲区len = rt_device_read(uart2, 0, rx_buffer, sizeof(rx_buffer)); // 将接收缓存区数据移到暂时缓存rt_ringbuffer_put(&uart2_ringbuffer, rx_buffer, len); //将暂时缓冲区的数据移到环形缓存区rt_sem_release(uart2_sem); //信号量释放return RT_EOK;
}void uart2_read_thread(void* param)
{rt_uint8_t readbuffer[UART_RX_BUFFER_SIZE]; // 读取数组避免直接操作环形缓存区static uint16_t data_size=0;//数据大小static uint32_t uart2_tick=0; //当前时间static uint32_t uart2_last_tick=0; //上次接收时间while(1){rt_sem_take(uart2_sem, RT_WAITING_FOREVER); //获取信号量uart2_tick=rt_tick_get(); //获取当前时间data_size=rt_ringbuffer_data_len(&uart2_ringbuffer);if(uart2_tick-uart2_last_tick>10&&data_size>0){rt_ringbuffer_get(&uart2_ringbuffer, readbuffer, data_size);rt_device_write(uart2, 0, readbuffer, data_size); //回显数据//此处进行解析操作uart2_last_tick=uart2_tick; //更新时间}}
}int uart2_init(void)
{//串口初始化uart2=rt_device_find("uart2");//uart1=rt_device_find("uart1"); //测试发送到串口1rt_device_open(uart2, RT_DEVICE_FLAG_DMA_RX); //以DMA接收rt_device_set_rx_indicate(uart2, uart2_handler);//环形缓存区创建rt_ringbuffer_init(&uart2_ringbuffer, uart2_rx_ringbuffer, UART_RX_BUFFER_SIZE);//信号量创建uart2_sem=rt_sem_create("uart2_sem", 0, RT_IPC_FLAG_PRIO);//线程创建uart2_thread=rt_thread_create("uart2_thd", uart2_read_thread, NULL, 1024, 12, 100);rt_thread_startup(uart2_thread);return 0;
}
ADC设备
ADC(Analog-to-Digital Converter) 指模数转换器。是指将连续变化的模拟信号转换为离散的数字信号的器件。
下面为一些简单的概念:
分辨率
分辨率以二进制(或十进制)数的位数来表示,一般有8位、10位、12位、16位等,它说明模数转换器对输入信号的分辨能力,位数越多,表示分辨率越高,恢复模拟信号时会更精确。
精度
精度表示 ADC 器件在所有的数值点上对应的模拟值和真实值之间的最大误差值,也就是输出数值偏离线性最大的距离。
注:精度与分辨率是两个不一样的概念,请注意区分。
转换速率
转换速率是指 A/D 转换器完成一次从模拟到数字的 AD 转换所需时间的倒数。例如,某 A/D 转换器的转换速率为 1MHz,则表示完成一次 AD 转换时间为 1 微秒。
ADC设备操作
ADC设备操作的接口有下面的几个:
函数 | 描述 |
---|---|
rt_device_find() | 根据 ADC 设备名称查找设备获取设备句柄 |
rt_adc_enable() | 使能 ADC 设备 |
rt_adc_read() | 读取 ADC 设备数据 |
rt_adc_disable() | 关闭 ADC 设备 |
下面我们对没有遇见的,官方再次封装的API进行说明
使能ADC
原型:
rt_err_t rt_adc_enable(rt_adc_device_t dev, rt_uint32_t channel); //设备句柄 ADC通道
返回值:
返回 | 描述 |
---|---|
RT_EOK | 成功 |
-RT_ENOSYS | 失败,设备操作方法为空 |
其他错误码 | 失败 |
读取ADC数据
原型:
rt_uint32_t rt_adc_read(rt_adc_device_t dev, rt_uint32_t channel); //设备句柄 ADC通道
返回值:
返回 | 描述 |
---|---|
读取的数值 |
关闭ADC通道
原型:
rt_err_t rt_adc_disable(rt_adc_device_t dev, rt_uint32_t channel); //设备句柄 ADC通道
返回值:
返回 | 描述 |
---|---|
RT_EOK | 成功 |
-RT_ENOSYS | 失败,设备操作方法为空 |
其他错误码 | 失败 |
ADC移植实例
对于ADC的移植,我们需要了解两种方式一种是利用官方的框架,另一种是我们直接使用生成的HAL库
对于ADC的使用在board.h
可以看见:
/** if you want to use adc you can use the following instructions.** STEP 1, open adc driver framework support in the RT-Thread Settings file** STEP 2, define macro related to the adc* such as #define BSP_USING_ADC1** STEP 3, copy your adc init function from stm32xxxx_hal_msp.c generated by stm32cubemx to the end of board.c file* such as void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)** STEP 4, modify your stm32xxxx_hal_config.h file to support adc peripherals. define macro related to the peripherals* such as #define HAL_ADC_MODULE_ENABLED**/
官方框架移植使用
官方框架暂时没有支持DMA搬运功能,所以我们这里演示的就是最简单的使用方法,使用官方的最重要的是完成底层指出的配置
首先,先在RT_Tread Setings
中将使用ADC的驱动框架打开,然后,点开CubeMX Setings
将我们需要的ADC配置好生成,接着直接将我们生成的文件中两个函数void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
和void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
都复制到我们的board.c
文件的最末尾,即可完成我们基础的设置
下面为我们使用框架的简单示例:
#include <rtthread.h>
#include "adc_app.h"
#include <rtdevice.h>
#define DBG_TAG "adc_app"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>#define ADC1_CHANNEL4 4 //通道
static rt_thread_t adc1_4_thread=NULL;
static rt_adc_device_t adc1_4_device=NULL;void adc1_4_thread_entry(void*param)
{static uint16_t adc_value=0;static float vlotage=0;while(1){adc_value=rt_adc_read(adc1_4_device, ADC1_CHANNEL4);rt_kprintf("adc_value:%d\r\n",adc_value);rt_thread_mdelay(1000);}
}int adc_init(void)
{//线程创建adc1_4_thread=rt_thread_create("adc1_4_thread", adc1_4_thread_entry, NULL, 1024, 13, 200);rt_thread_startup(adc1_4_thread);//adc设备创建adc1_4_device=(rt_adc_device_t)rt_device_find("adc1");rt_adc_enable(adc1_4_device, ADC1_CHANNEL4);return 0;
}
CubeMX框架移植
对于CubeMX
生成的HAL库我们就可以直接进行比如DMA搬运、DMA多通道,其实最主要的是如果要使用DMA搬运的方式就必须使用自己HAL库的框架移植
- 首先,我们需要在
CubeMX
中把ADC和DMA相关配置好,同时记得开启DMA中断。 - 然后,我们将生成的
CubeMX
文件中函数**MX_DMA_Init
和MX_ADC1_Init
以及HAL_ADC_MspInit
**,然后默认情况下HAL_ADC_ConvCpltCallback
,转换完成这个 事件发生后,什么都不会发生。我们在此处自己定义一手完成转换后释放信号量来唤醒线程 - 最后就是进行我们的初始化以及应用,我们需要在初始化中对**
MX_DMA_Init
和MX_ADC1_Init
**调用,注意(若提示重复定义可以修改这两个函数的函数名)
#ifdef ADC_DMA
#include "stm32f4xx_hal.h" // 包含HAL库通用定义,如ENABLE/DISABLE, HAL_OK, HAL_ERROR等
#define ADC1_CHANNEL4 4 //通道
static rt_thread_t adc1_4_thread=NULL;
#define SIZE 16
uint32_t adc_buffer[SIZE];
static rt_sem_t adc_dma_sem=NULL;ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;void DMA2_Stream0_IRQHandler(void)
{/* USER CODE BEGIN DMA2_Stream0_IRQn 0 *//* USER CODE END DMA2_Stream0_IRQn 0 */HAL_DMA_IRQHandler(&hdma_adc1);/* USER CODE BEGIN DMA2_Stream0_IRQn 1 *//* USER CODE END DMA2_Stream0_IRQn 1 */
}void MY_MX_DMA_Init(void)
{/* DMA controller clock enable */__HAL_RCC_DMA2_CLK_ENABLE();/* DMA interrupt init *//* DMA2_Stream0_IRQn interrupt configuration */HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);/* DMA2_Stream2_IRQn interrupt configuration */HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);/* DMA2_Stream7_IRQn interrupt configuration */HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0);HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);}void MX_ADC1_Init(void)
{/* USER CODE BEGIN ADC1_Init 0 *//* USER CODE END ADC1_Init 0 */ADC_ChannelConfTypeDef sConfig = {0};/* USER CODE BEGIN ADC1_Init 1 *//* USER CODE END ADC1_Init 1 *//** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)*/hadc1.Instance = ADC1;hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;hadc1.Init.Resolution = ADC_RESOLUTION_12B;hadc1.Init.ScanConvMode = DISABLE;hadc1.Init.ContinuousConvMode = ENABLE;hadc1.Init.DiscontinuousConvMode = DISABLE;hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;hadc1.Init.NbrOfConversion = 1;hadc1.Init.DMAContinuousRequests = ENABLE;hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;if (HAL_ADC_Init(&hadc1) != HAL_OK){Error_Handler();}/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.*/sConfig.Channel = ADC_CHANNEL_4;sConfig.Rank = 1;sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK){Error_Handler();}/* USER CODE BEGIN ADC1_Init 2 *//* USER CODE END ADC1_Init 2 */}void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{GPIO_InitTypeDef GPIO_InitStruct = {0};if(adcHandle->Instance==ADC1){/* USER CODE BEGIN ADC1_MspInit 0 *//* USER CODE END ADC1_MspInit 0 *//* ADC1 clock enable */__HAL_RCC_ADC1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**ADC1 GPIO ConfigurationPA4 ------> ADC1_IN4*/GPIO_InitStruct.Pin = GPIO_PIN_4;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* ADC1 DMA Init *//* ADC1 Init */hdma_adc1.Instance = DMA2_Stream0;hdma_adc1.Init.Channel = DMA_CHANNEL_0;hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;hdma_adc1.Init.Mode = DMA_CIRCULAR;hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;if (HAL_DMA_Init(&hdma_adc1) != HAL_OK){Error_Handler();}__HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);/* USER CODE BEGIN ADC1_MspInit 1 *//* USER CODE END ADC1_MspInit 1 */}
}/*** @brief ADC 转换完成中断回调函数* @note 这个函数在 ADC DMA 传输完成时由中断服务程序调用。* !!! 不可在此函数中执行耗时操作,如打印、延时等 !!!*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{// 检查是否是我们关心的 ADC1if(hadc->Instance == ADC1){// 释放信号量,通知处理线程数据已准备好rt_sem_release(adc_dma_sem);}
}void adc1_4_thread_entry(void*param)
{while (1){// 等待信号量,线程会在此阻塞,直到中断回调函数释放信号量// 超时设置为永久等待rt_err_t result = rt_sem_take(adc_dma_sem, RT_WAITING_FOREVER);if (result == RT_EOK){// 成功获取到信号量,说明 adc_buffer 已填满uint32_t adc_sum = 0;// 安全地处理数据for (int i = 0; i < SIZE; i++){adc_sum += adc_buffer[i];}uint16_t adc_avg = adc_sum / SIZE;// 可以在这里进行滤波等其他处理LOG_D("ADC average value: %d", adc_avg);}}
}int adc_init(void)
{//信号量创建adc_dma_sem=rt_sem_create("adc_dma_sem", 0, RT_IPC_FLAG_PRIO);//线程创建adc1_4_thread=rt_thread_create("adc1_4_thread", adc1_4_thread_entry, NULL, 1024, 13, 200);rt_thread_startup(adc1_4_thread);//初始化ADC和DMAMY_MX_DMA_Init();MX_ADC1_Init();HAL_ADC_Start_DMA(&hadc1,adc_buffer,sizeof(adc_buffer)/sizeof(uint32_t));return 0;
}
我们使用这个DMA+ADC
的方式来进行测试,你会发现和我们使用IO
框架的方式的结果不一样,主要原因是因为**sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
**这个采样周期的问题,下面我们来进行分析:
3CYCLES
是 最短采样时间。这意味着ADC硬件在开始“转换”之前,只用了极短的时间去“品尝”你 PA4 引脚上的电压。这个时间太短,导致它内部的电容没有被完全充满。
对比项 | 方法一 (rt_adc_read ) | 方法二 (HAL+DMA) |
---|---|---|
采样时间 | 长且安全 (由驱动默认设置,如 480 周期) | 极短且危险 (由你代码指定为 3 周期) |
物理过程 | 内部电容有充足时间被完全充电 | 内部电容充电不完全,采样提前结束 |
测量结果 | 准确,接近真实值 (~4090) | 系统性偏低,显著低于真实值 (~3910) |
I2C设备
I2C(Inter Integrated Circuit)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data
),另一根是双向时钟线 SCL(serial clock)
。
使用I2C最主要的就是去了解I2C协议的时序:
起始信号:在 SCL 保持高电平期间,SDA 从高电平跳变到低电平。
终止信号:在 SCL 保持高电平期间,SDA 从低电平跳变到高电平。
信号传输:在 SCL 为高电平期间,SDA 的电平必须保持稳定。SDA 上的电平变化只能在 SCL 为低电平期间发生。
应答/非应答:每当主设备发送完一个字节(8位)的数据后,紧接着的第9个 SCL 时钟周期是用来进行应答或非应答的。
当在SCL高电平期间,如果SDA变为低电平则为应答,反之为非应答。
I2C设备操作
I2C设备的接口如下:
函数 | 描述 |
---|---|
rt_device_find() | 根据 I2C 总线设备名称查找设备获取设备句柄 |
rt_i2c_transfer() | 传输数据 |
数据交换API
原型:
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus, //i2c句柄struct rt_i2c_msg msgs[], //待传输的消息数组指针rt_uint32_t num); //消息数组的元素个数
注:此函数会调用 rt_mutex_take()
, 不能在中断服务程序里面调用,会导致断言报错。
返回值:
返回 | 描述 |
---|---|
消息数组的元素个数 | 成功 |
错误码 | 失败 |
消息结构体rt_i2c_msg
struct rt_i2c_msg
{rt_uint16_t addr; /* 从机地址 */rt_uint16_t flags; /* 读、写标志等 */rt_uint16_t len; /* 读写数据字节数 */rt_uint8_t *buf; /* 读写数据缓冲区指针 */
}
标志位flags
#define RT_I2C_WR 0x0000 /* 写标志,不可以和读标志进行“|”操作 */
#define RT_I2C_RD (1u << 0) /* 读标志,不可以和写标志进行“|”操作 */
#define RT_I2C_ADDR_10BIT (1u << 2) /* 10 位地址模式 */
#define RT_I2C_NO_START (1u << 4) /* 无开始条件 */
#define RT_I2C_IGNORE_NACK (1u << 5) /* 忽视 NACK */
#define RT_I2C_NO_READ_ACK (1u << 6) /* 读的时候不发送 ACK */
#define RT_I2C_NO_STOP (1u << 7) /* 不发送结束位 */
使用示例:
#define AHT10_I2C_BUS_NAME "i2c1" /* 传感器连接的I2C总线设备名称 */
#define AHT10_ADDR 0x38 /* 从机地址 */
struct rt_i2c_bus_device *i2c_bus; /* I2C总线设备句柄 *//* 查找I2C总线设备,获取I2C总线设备句柄 */
i2c_bus = (struct rt_i2c_bus_device *)rt_device_find(name);/* 读传感器寄存器数据 */
static rt_err_t read_regs(struct rt_i2c_bus_device *bus, rt_uint8_t len, rt_uint8_t *buf)
{struct rt_i2c_msg msgs;msgs.addr = AHT10_ADDR; /* 从机地址 */msgs.flags = RT_I2C_RD; /* 读标志 */msgs.buf = buf; /* 读写数据缓冲区指针 */msgs.len = len; /* 读写数据字节数 *//* 调用I2C设备接口传输数据 */if (rt_i2c_transfer(bus, &msgs, 1) == 1){return RT_EOK;}else{return -RT_ERROR;}
}
I2C设备发送
原型:
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus, //设备句柄rt_uint16_t addr, //地址rt_uint16_t flags, //设备标志const rt_uint8_t *buf, //数据rt_uint32_t count); //数据大小
该函数是在rt_i2c_transfer
上封装的,方便向设备发送数据
I2C设备读取
原型:
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus, //设备句柄rt_uint16_t addr, //地址rt_uint16_t flags, //设备标志rt_uint8_t *buf, //缓存区rt_uint32_t count); //缓存区大小
该函数是在rt_i2c_transfer
上封装的,方便向设备发送数据
I2C设备使用
对于I2C设备的使用我们可以从board.h
中去看具体步骤
/** if you want to use i2c bus(soft simulate) you can use the following instructions.** STEP 1, open i2c driver framework(soft simulate) support in the RT-Thread Settings file** STEP 2, define macro related to the i2c bus* such as #define BSP_USING_I2C1** STEP 3, according to the corresponding pin of i2c port, modify the related i2c port and pin information* such as #define BSP_I2C1_SCL_PIN GET_PIN(port, pin) -> GET_PIN(C, 11)* #define BSP_I2C1_SDA_PIN GET_PIN(port, pin) -> GET_PIN(C, 12)*/
步骤一:在 RT-Thread Settings 中启用软件 I2C 驱动框架
步骤二:启用并定义一个具体的 I2C 总线实例 (BSP_USING_I2C1)
步骤三:修改指定 I2C 总线对应的 GPIO 引脚
PWM设备
PWM(Pulse Width Modulation , 脉冲宽度调制) 是一种对模拟信号电平进行数字编码的方法,通过不同频率的脉冲使用方波的占空比用来对一个具体模拟信号的电平进行编码,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替所需要波形的设备。
对于PWM设备也是我们常用的设备,对于驱动电机以及呼吸灯等有重要作用所以必须掌握。
=
PWM设备操作
PWM设备的操作接口如下:
函数 | 描述 |
---|---|
rt_device_find() | 根据 PWM 设备名称查找设备获取设备句柄 |
rt_pwm_set() | 设置 PWM 周期和脉冲宽度 |
rt_pwm_enable() | 使能 PWM 设备 |
rt_pwm_disable() | 关闭 PWM 设备 |
设置PWM周期和占空比
原型:
rt_err_t rt_pwm_set(struct rt_device_pwm *device, //pwm设备句柄int channel, //通道rt_uint32_t period, //周期nsrt_uint32_t pulse); //脉冲宽度ns
PWM 的通道 channel 可为正数或者负数。因为有的芯片的PWM是具有互补输出功能的,即PWM的某一个通道是可以靠两个引脚来发出一对互补的波形。当通道号为正数的时候,代表使用PWM的正常输出波形引脚;为其负数的时候,代表使用PWM的互补输出波形引脚。
#define PWM_DEV_CHANNEL 1 /* PWM通道的CH1引脚 */
#define PWM_DEV_CHANNEL -1 /* PWM通道的CH1N引脚 */
返回值:
返回 | 描述 |
---|---|
RT_EOK | 成功 |
-RT_EIO | device 为空 |
-RT_ENOSYS | 设备操作方法为空 |
其他错误码 | 执行失败 |
使能PWM
原型:
rt_err_t rt_pwm_enable(struct rt_device_pwm *device, int channel); //pwm设备句柄 通道
返回值:
返回 | 描述 |
---|---|
RT_EOK | 设备使能成功 |
-RT_ENOSYS | 设备操作方法为空 |
其他错误码 | 设备使能失败 |
关闭PWM
原型:
rt_err_t rt_pwm_disable(struct rt_device_pwm *device, int channel); //pwm设备句柄 通道
返回值:
返回 | 描述 |
---|---|
RT_EOK | 设备关闭成功 |
-RT_EIO | 设备句柄为空 |
其他错误码 | 设备关闭失败 |
PWM移植实例
对于PWM设备的使用我们参考board.h
中的相关步骤即可:
/** if you want to use pwm you can use the following instructions.** STEP 1, open pwm driver framework support in the RT-Thread Settings file** STEP 2, define macro related to the pwm* such as #define BSP_USING_PWM1** STEP 3, copy your pwm timer init function from stm32xxxx_hal_msp.c generated by stm32cubemx to the end if board.c file* such as void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) and* void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim)** STEP 4, modify your stm32xxxx_hal_config.h file to support pwm peripherals. define macro related to the peripherals* such as #define HAL_TIM_MODULE_ENABLED**/
- 步骤 1: 在 RT-Thread Settings 文件中,打开 PWM 驱动框架的支持。
- 步骤 2: 定义与 PWM 相关的宏,例如
#define BSP_USING_PWM1
。(#define BSP_USING_PWM1
和#define BSP_USING_PWM1_CH1
- )
- 步骤 3: 将您在 STM32CubeMX 中生成的
stm32xxxx_hal_msp.c
文件里的 PWM 定时器初始化函数,复制到board.c
文件的末尾。例如void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
和void HAL_TIM_MspPostInit(TIM_HandleTypeDef* htim)
。 - 步骤 4: 修改您的
stm32xxxx_hal_config.h
文件以支持 PWM 外设,即定义与该外设相关的宏,例如#define HAL_TIM_MODULE_ENABLED
。
==注意:==当我们使用定时器1时会发现,报错没有配置,此时我们就需要进入pwm_config.h
文件中,会发现定时器1没有定义,我们按照其他的定时器一样定义PWM1即可
#ifdef BSP_USING_PWM1
#ifndef PWM1_CONFIG
#define PWM1_CONFIG \{ \.tim_handle.Instance = TIM1, \.name = "pwm1", \.channel = 0 \}
#endif /* PWM1_CONFIG */
#endif /* BSP_USING_PWM1 */
现在我们对于PWM来进行移植实例来实现呼吸灯
#include <rtthread.h>
#include "pwm_app.h"
#include <rtdevice.h>
#define DBG_TAG "pwm_app"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>//测试使用定时器1通道1 PE9
static struct rt_device_pwm* pwm_led=NULL;
static rt_thread_t pwm_thread=NULL;void pwm_thread_entry(void* parameter)
{rt_uint32_t pulse = 0;int dir = 1; // 1: 变亮, 0: 变暗while(1){if (dir){pulse += 50000;}else{pulse -= 50000;}if (pulse >= 5000000 ){pulse = 5000000 ;dir = 0;}if (pulse <= 0){pulse = 0;dir = 1;}rt_pwm_set(pwm_led, 1, 5000000 , pulse);rt_thread_mdelay(10);}
}int pwm_init()
{//pwm初始化pwm_led=(struct rt_device_pwm*)rt_device_find("pwm1");rt_pwm_set(pwm_led, 1, 5000000, 0); //200hzrt_pwm_enable(pwm_led, 1);//线程创建pwm_thread=rt_thread_create("pwm_thread", pwm_thread_entry, NULL, 1024, 10, 1);rt_thread_startup(pwm_thread);return 0;
}
RTC设备
RTC (Real-Time Clock)实时时钟可以提供精确的实时时间,对于在一些像时钟、闹钟这样的实时时间获取是必不可少的一个外设,也比较为我们常见和使用,我们也需要掌握。
访问RTC设备
我们访问RTC设备就是去设置时间和读取时间两个接口:
函数 | 描述 |
---|---|
rt_device_find() | 查找RTC设备 |
rt_device_open() | 打开设备 |
set_date() | 设置日期,年、月、日(当地时区) |
set_time() | 设置时间,时、分、秒(当地时区) |
time() | 获取时间 |
设置年月日
原型:
rt_err_t set_date(rt_uint32_t year, rt_uint32_t month, rt_uint32_t day) //年 月 日
返回值:
返回 | 描述 |
---|---|
RT_EOK | 设置成功 |
-RT_ERROR | 失败,没有找到 rtc 设备 |
其他错误码 | 失败 |
设置时分秒
原型:
rt_err_t set_time(rt_uint32_t hour, rt_uint32_t minute, rt_uint32_t second) //时 分 秒
返回值:
返回 | 描述 |
---|---|
RT_EOK | 设置成功 |
-RT_ERROR | 失败,没有找到 rtc 设备 |
其他错误码 | 失败 |
获取时间
原型:
time_t time(time_t *t)
time_t是一种时间类型,返回的时间是格林威治时间
格林威治时间(UTC)1970年1月1日00:00:00 —— 以来所经过的秒数。
如何把格林威治时间转换成当前时间,方法如下:
方法一:
ctime()
直接一步到位,把 time_t
时间戳直接转换成一个预设好格式的、人类可读的字符串。
char *ctime(const time_t *timep); //格林威治时间地址
eg:
rt_kprintf("ctime output: %s", ctime(&now));
//->输出 ctime output: Wed Sep 24 10:30:00 2025
方法二:
localtime()
+printf()
/snprintf()
就是为了更加灵活同时更加使用RTOS系统,因为ctime()
会操作同一个静态内存区,所以可能会出现线程危险的情况。
struct tm原型:
struct tm
{int tm_sec; //秒int tm_min; //分int tm_hour; //小时int tm_mday; //一个月的第几天int tm_mon; //月 默认会减1(0-11)int tm_year; //年 默认会减1900int tm_wday; //星期几int tm_yday; //一年中的第几天int tm_isdst; //时令
};
struct tm nicely_cut_parts; //定义结构体
localtime_r(&a_very_big_number, &nicely_cut_parts); //转换成当前时间存在结构体中//格式化字符串
char time_str[48];
snprintf(time_str, sizeof(time_str),"%d-%02d-%02d %02d:%02d:%02d",nicely_cut_parts.tm_year + 1900,nicely_cut_parts.tm_mon + 1,nicely_cut_parts.tm_mday,nicely_cut_parts.tm_hour,nicely_cut_parts.tm_min,nicely_cut_parts.tm_sec);
RTC设备使用实例
首先我们对于RTC设备的使用,我们也可以通过board.h
文件中的步骤进行配置:
/** if you want to use rtc(hardware) you can use the following instructions.** STEP 1, open rtc driver framework(hardware) support in the RT-Thread Settings file** STEP 2, define macro related to the rtc* such as BSP_USING_ONCHIP_RTC** STEP 3, modify your stm32xxxx_hal_config.h file to support rtc peripherals. define macro related to the peripherals* such as #define HAL_RTC_MODULE_ENABLED**/
- 首先,我们需要在
RT-Thread Setings
中打开使用RTC驱动 - 然后在
board.h
中宏定义BSP_USING_ONCHIP_RTC
- 最后在
stm32xxxx_hal_config.h
中去取消宏定义#define HAL_RTC_MODULE_ENABLED
#include <rtthread.h>
#include "rtc_app.h"
#include <rtdevice.h>
#define DBG_TAG "rtc_app"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>static rt_device_t rtc_device = NULL;
static rt_thread_t rtc_thread = NULL;/* 线程入口函数:周期性读取并打印时间 */
void rtc_thread_entry(void *param)
{struct tm nicely_cut_parts; // 用于存放分解后的时间time_t now_seconds; // 用于存放自1970年以来的总秒数while (1){// 1. 获取当前时间的总秒数now_seconds = time(NULL);// 2. 将总秒数转换为本地时间(带时区)并分解到结构体中localtime_r(&now_seconds, &nicely_cut_parts);// 3. 格式化输出char time_str[48];snprintf(time_str, sizeof(time_str),"%d-%02d-%02d %02d:%02d:%02d",nicely_cut_parts.tm_year + 1900,nicely_cut_parts.tm_mon + 1,nicely_cut_parts.tm_mday,nicely_cut_parts.tm_hour,nicely_cut_parts.tm_min,nicely_cut_parts.tm_sec);LOG_D("Current Time: %s", time_str); // 使用LOG组件打印,更规范// 4. 线程挂起1秒rt_thread_mdelay(1000);}
}/* RTC 初始化函数 */
int rtc_init(void)
{// 1. 查找RTC设备rtc_device = rt_device_find("rtc");if (rtc_device == NULL){LOG_E("find rtc device failed!");return -RT_ERROR;}// 2. 打开RTC设备if (rt_device_open(rtc_device, RT_DEVICE_FLAG_RDWR) != RT_EOK){LOG_E("open rtc device failed!");return -RT_ERROR;}// 4. 设置RTC的初始时间set_date(2025, 9, 25);set_time(17, 46, 0);// 5. 创建并启动时间显示线程rtc_thread = rt_thread_create("rtc_thread", rtc_thread_entry, NULL, 1024, 25, 10);if (rtc_thread != NULL){rt_thread_startup(rtc_thread);}else{LOG_E("create rtc_thread failed!");return -RT_ERROR;}return RT_EOK;
}