IAR 选项介绍和ILINK链接器配置文件介绍

温故而知新
1 | lv_scr_act(); |
LittlevGL 使用两个名为 layer_top 和 layer_sys 的特殊层,两者在显示器的所有屏幕上都是 可见且通用的。但是,他们不会在多个物理显示器之间共享。layer_top 始终位于默认屏幕 (lv_scr_act()) 的顶部,layer_sys 则位于 layer_top 的顶部。
用户可以使用 layer_top 来创建一些随处可见的内容,例如,菜单栏,对话框。如果启用了 click 属性,layer_top 将吸收所用用户的点击并将其模态化。
1 | /* 创建一个父对象为 lv_layer_top 的按钮 */ |
LV_EVENT_FOCUSED 聚焦
LV_EVENT_FOCUSED 聚焦释放
LV_EVENT_REFRESH 查询以刷新对象,这个事件不会由库触发,用户可以使用
1 | // 以在回调函数中使用 lv_event_get_data() 函数获取自定义数 |
对象仅存储指向样式的指针,因此样式不能是在函数退出后销毁的局部变量。 创建样式的时候应该定义为静态变量或者全局变量。
成员 glass:不继承这种风格(为 1 的时候对象的子类不会继承这种风格,以为着自类需要重新设置风格

1 | body.border.opa//:边框不透明度(0-255) |
是一系列控件美化的集合,包含了所有控件的样式。
1 | /* 创建主题 */ |
1 | /* 创建主题 */ |
字体是文字的位图和其他信息的集合
1 | /* 创建一个新字体的 style */ |
Unicode 支持:LittlevGL 支持 UTF-8 编码的 Unicode 字符,在使用时需要确保文件的保存格式为 UTF-8
LittlevGL 官网有在线的字体转换工具,可以将 ttf 的字体文件转换为 LittlevGL 支持的字体文件,字体可以保存在内部数组,也可以保存在外部 flash 或者文件系统。显示中文的 c 文件必须使用 UTF8 编码。
LittlevGL 内置了几种 ASCII 字体,可以在 lv_conf.h 中启用:
内置字体是全局变量,使用时只需调用即可,内部字体是 4bpp除了 ASCII 范围的字体,内置字体还支持符号字体,例如:

1 | //符号字体可以直接使用 |
要使用汉字,需要将文件保存为 UTF-8 格式, 然后将字体文件加入到工程,在需要显示中文的文件里面声明新字体 LV_FONT_DECLARE(gb2312_puhui32)
可以将图像存储在两个位置:
存储在内部变量的图像由 lv_img_dsc_t 结构体保存,该结构体具有以下内容:
1 | lv_img_dsc_t |
要使用文件的方式处理图片, 需要注册 LittlevGL 的文件接口, 实现打开、读取、关闭等功能。也可以自行将转换后的 bin 格式的图片放到外部文件系统,上电后加载到 RAM,这种方式后面再进行讲解。
可以将图像转换成 c 格式的数组存储在内部 Flash,也可以转换为 bin文件存储在外部 Flash 或者文件系统。
在创建图像后将 c 文件添加到工程或者将 bin 文件复制到文件系统,然后使用以下方法显示图像:
1 | // 内部数组或flash中 |
LittlevGL 具有文件系统抽象模块,用户可以附加任何类型的文件系统,只需要注册对应的接口即可。文件系统由驱动器的 letter 标识。例如,如果SD 卡与字母关联,则可以用 ‘S’ 访问文件,例如 “S:/ui/yupian.bin”。
1 | lv_fs_drv_t my_fs_drv; /* 定义一个文件驱动 */ |
1.以上回调函数并不是每一个都需要实现的,根据用户的需求,把用户暂时没有使用的回调函数赋值为 NULL 即可。
2.如果我们需要打开一个文件,可以用以下方式:lv_fs_open(&file, “S:/ui/font.bin”, LV_FS_MODE_RD);其中路径以驱动器标识 ‘S’ 开头。
LittlevGL 内置任务系统,可以注册一个函数以使其被定期调用。任务是在 lv_task_handler() 中进行处理。任务是非抢占式的,意味这一个任务无法中断另一个任务,因此可以在任务中安全调用 LittlevGL 相关功能。
1 | lv_task_create(task_cb, period, LV_TASK_PRIO_OFF/LOWEST/LOW/MID/HIGH/HIGHEST, user_data); |
如果只想删除一个对象,而无需清理 my_screen_cleanup 中的任何内容,则可以使用 lv_obj_del_async,它将在下次调用 lv_task_handler 时删除该对象。
共有三种缓冲配置:
位图字体,顾名思义,就是文字是通过位图绘制出来的,字体的存储方式就是一个单色的位图,在 GUI 需要显示对应字体的时候,调用对应的数组或者读取文字的存储地址,来将位图绘制到屏幕,用户看到的就是文字。
1.将汉字转换为LittlevGL可用的C文件格式的字体和XBF格式的bin文件字体。
2.支持选择本地字体, 可以加入 GB2312 常用字体和 GBK 全中文字体, 支持抗锯齿。
1.需要确保 c 文件是 UTF-8 编码,
2.因c 文件保存为 UTF-8编码后,MDK 编译可能出错,打开配置工程,切换 C/C++选项卡,在 Misc Controns里面追加–locale=english 即可。
.c文件的字体使用:
1 | //第一步 引入字体定义 |
现在用的是UCS-2,即2个字节编码,而UCS-4是为了防止将来2个字节不够用才开发的.
Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符编码。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。
Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。
例1:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001,用这个比特流依次代替模板中的x,得到:1110 0110 1011 0001 1000 1001,即E6 B1 89。
UCS-2用两个字节编码,UCS-4用4个字节编码。
1 | lv_obj_set_x(obj, new_x); //设置对象的 X 轴坐标 |
1 | lv_obj_align(obj, base, LV_ALIGN_..., x_mod, y_mod); //base 是参考对象,如果为NULL,就是用父对象 |

1.如果对象的尺寸有变化,只需要调用 lv_obj_realign(obj) 就可以重新对齐对象,等效于 lv_obj_align 使用相同参数再次调用。
2.如果使用 lv_obj_set_auto_realign(obj, true) 进行自动对齐,那么在对象的大小发生变化时,将会自动重新对齐对象。
库会自动生成两个层
1 | lv_layer_top() ; lv_layer_sys(); // 可以获取顶层和系统层。 |
基础对象都会有一个MAIN样式, 使用 lv_obj_set_style(obj,&new_style);果设置为 NULL ,将会继承父对象的样式。这个函数只能用于基础对象的样式修改。
控件都有自己独立的样式设置函数,例如:lv_btn_set_style();
1 | lv_obj_refresh_style(obj); //以使用的样式修改后刷新,已对使用了该样式的obj刷新 |
函数 lv_obj_set_event_cb(obj,event_cb) 可以为对象设置事件回调函数。
函数 lv_event_send(obj,LV_EVENT_…,data) 可以手动发送事件给对象,例如可以将实体按键等输入设备以事件的方式发送到对象。
使用对象,事件和数据调用事件函数 lv_res_t lv_event_send_func(lv_event_cb_t event_xcb, lv_obj_t * obj, lv_event_t event, const void * data);
1.hidden 隐藏对象,他将不会被绘制,输入焦点也不会聚集在上面。他的子对象也会被隐藏。
2.click 允许单击对象,如果禁用,则单击事件将会传递到此事件后面的对象。例如,在按钮上添加label,label 默认不可单击,单击事件传递给按钮。
3.top 如果启动,则单击此对象或者其任何子对象是,此对象将进入前台。
4.drag 拖动。
5.drag_dir 仅在特定方向上启用拖动,例如 LV_DRAG_DIR_HOR/VER/ALL。
6.drag_throw 启用后,拖动后释放,还会有一个惯性移动的效果。
7.drag_parent 如果启用,拖动对象的时候,父级也会被拖动。递归检查,拖动也会传递给父对象的父对象。
8.parent_event 将事件传递给父对象,递归检查,事件也会传递给父对象的父对象。
9.opa_scale_enable 启用不透明度。可以使用函数 lv_obj_set_…(obj,true/false) 来启用或者禁用某个属性。
利用函数 lv_group_add_obj(group,obj) 将对象添加到组中以后,则可以通过函数 lv_obj_get_group(obj) 获取对象的当前组。
函数 lv_obj_is_focused(obj) 获取对象当前是否有添加到某个组中,如果没有将对象添加到组中,则返回 false。
基础对象使用 style.body 属性,屏幕默认的样式为 lv_style_scr 和 lv_style_plain_color;
创建和删除
lv_obj_create() 创建对象
lv_obj_del() 删除对象
lv_obj_del_async() 异步删除对象
lv_obj_clean() 清除所有子对象
lv_obj_invalidate() 将对象标记为无效
设置函数
lv_obj_set_parent() 设置新的父对象
lv_obj_move_foreground() 将对象移至前台
lv_obj_move_background() 将对象移至后台
lv_obj_set_pos() 设置对象坐标
lv_obj_set_x() 设置对象 X 坐标
lv_obj_set_y() 设置对象 Y 坐标
lv_obj_set_size() 设置对象尺寸
lv_obj_set_width() 设置对象宽度
lv_obj_set_height() 设置对象高度
lv_obj_align() 将一个对象与另一个对象对齐,左上角为原点
lv_obj_align_origo() 将一个对象与另一个对象对齐,中心为
lv_obj_realign() 重新对齐对象
lv_obj_set_auto_realign() 启用/禁用对象自动对齐
lv_obj_set_ext_click_area() 设置扩展点击区域大小
lv_obj_set_style() 设置样式
lv_obj_refresh_style() 通知对象样式已更改
lv_obj_report_style_mod() 修改样式后通知所有使用该样式对象
lv_obj_set_hidden() 启用/禁用隐藏对象
lv_obj_set_click() 启用/禁用可点击
lv_obj_set_top() 启用/禁用对象置于顶层
lv_obj_set_drag() 启用/禁用对象可拖动
lv_obj_set_drag_dir() 设置拖动方向
lv_obj_set_drag_throw() 启用/禁用拖动后是否有惯性移动
lv_obj_set_drag_parent() 启用/禁用父对象可拖动
lv_obj_set_parent_event() 启用/禁用将事件传递给父级
lv_obj_set_opa_scale_enable() 启用/禁用 opa 比例
lv_obj_set_opa_scale() 设置对象 opa 比例
lv_obj_set_protect() 设置对象保护
lv_obj_clear_protect() 清除对象保护
lv_obj_set_event_cb() 设置对象的事件回调函数
lv_event_send() 向对象发送事件
lv_event_send_func() 使用对象,事件和数据调用事件函数
lv_event_get_data() 获取当前事件的 data 参数
lv_obj_set_signal_cb() 设置对象的信号功能。由库内部使用
lv_signal_send() 向对象发送信号
lv_obj_set_design_cb() 为对象设置新的设计回调函数
lv_obj_allocate_ext_attr() 分配一个新的 ext
lv_obj_refresh_ext_draw_pad() 向对象发送 LV_SIGNAL_REFR_EXT_SIZE 信号
lv_obj_set_user_data() 设置对象的用户数据
参数获取函数
lv_obj_get_screen() 获取对象的屏幕
lv_obj_get_disp() 获取对象的显示
lv_obj_get_parent() 获取父对象
lv_obj_get_child() 获取子对象
lv_obj_get_child_back() 遍历对象的子对象
lv_obj_count_children() 计算对象的子级数量
lv_obj_count_children_recursive() 递归计算对象的子级
lv_obj_get_coords() 复制对象的区域
lv_obj_get_inner_coords() 获取对象的图形可用区域
lv_obj_get_x() 获取对象的 X 轴坐标
lv_obj_get_y() 获取对象的 Y 轴坐标
lv_obj_get_width() 获取对象的宽度
lv_obj_get_height() 获取对象高度
lv_obj_get_width_fit() 获取填充宽度
lv_obj_get_height_fit() 获取填充高度
lv_obj_get_auto_realign() 获取对象的自动重新对齐属性
lv_obj_get_ext_click_pad_left() 获取扩展的可点击区域的左侧填充
lv_obj_get_ext_click_pad_right() 获取扩展的可点击区域的右侧填充
lv_obj_get_ext_click_pad_top() 获取扩展的可点击区域的上侧填充
lv_obj_get_ext_click_pad_bottom() 获取扩展的可点击区域的下侧填充
lv_obj_get_ext_draw_pad() 获取对象的扩展尺寸属性
lv_obj_get_style() 获取对象的样式
lv_obj_get_hidden() 获取对象是否隐藏的属性
lv_obj_get_click() 获取对象是否可点击的属性
lv_obj_get_top() 获取对象是否置于顶层的属性
lv_obj_get_drag() 获取对象是否可拖动的属性
lv_obj_get_drag_dir() 获取对象的拖动方向
lv_obj_get_drag_throw() 获取对象是否有拖动惯性移动的属性
lv_obj_get_drag_parent() 获取对象是否有父对象拖动的属性
lv_obj_get_parent_event() 获取对象是否发送事件到父对象
lv_obj_get_opa_scale_enable() 获取对象是否设置 opa
lv_obj_get_opa_scale() 获取对象 opa
lv_obj_get_protect() 获取对象的保护
lv_obj_is_protected() 获取对象是否设置保护
lv_obj_get_signal_cb() 获取对象的信号回调函数
lv_obj_get_design_cb() 获取对象的设计回调函数
lv_obj_get_event_cb() 获取对象的事件回调函数
lv_obj_get_ext_attr() 获取 ext 指针
lv_obj_get_type() 获取对象类型
lv_obj_get_user_data() 获取对象的用户数据
lv_obj_get_user_data_ptr() 获取指向对象的用户数据的指针
lv_obj_get_group() 获取对象的组
lv_obj_is_focused() 获取对象是否聚焦
1 | //获取对象实际可用区域 |
1 | lv_label_set_text(label,"New text") //lv库分配内存 |
label 控件的大小可以自动扩展为文本大小, 也可以根据几种长文本模式来进行处理。
创建 label 并且设置文本和长模式后,控件的大小已经扩展到文本大小,因此需要先设置长模式,然后设置控件尺寸。
1 | void lv_label_set_align(lv_obj_t * label, lv_label_align_t align); |
lv_label_create() 创建 label 控件
设置函数
lv_label_set_text() 设置 label 的文本
lv_label_set_array_text() 设置 label 的数组文本
lv_label_set_static_text() 设置 label 的静态文本
lv_label_set_long_mode() 设置 label 的长模式
lv_label_set_align() 设置 label 的对齐方式
lv_label_set_recolor() 启用/禁用 label 的文字重新着色
lv_label_set_body_draw() 启用/禁用 label 的背景绘制
lv_label_set_anim_speed() 设置 label 动画速度
lv_label_set_style() 设置 label 的样式
lv_label_set_text_sel_start() 设置 label 选择开始索引
lv_label_set_text_sel_end() 设置 label 选择结束索引
参数获取函数
lv_label_get_text() 获取 label 的文本
lv_label_get_long_mode() 获取 label 的长模式
lv_label_get_align() 获取 label 的对齐方式
lv_label_get_recolor() 获取 label 的重新着色使能
lv_label_get_body_draw() 获取 label 的背景绘制使能
lv_label_get_anim_speed() 获取 label 的动画速度
lv_label_get_letter_pos() 获取 label 某个字符的相对坐标
lv_label_get_letter_on() 获取 label 指定坐标上的字符的索引
lv_label_is_char_under_pos() 检查 label 是否在某个坐标下绘制
lv_label_get_style() 获取 label 的样式
lv_label_get_text_sel_start() 获取 label 选择开始索引
lv_label_get_text_sel_end() 获取 label 选择结束索引
其他函数
lv_label_ins_text() 在 label 上追加文本
lv_label_cut_text() 从 label 中删除字符
void * ext_attr; 即用来指向各个不同空间自己的结构体的指针。
每个空间的ext_attr属性都不一样,例如:label:lv_label_ext_t
如果需要使用 lv_obj_set_pos()来设置子对象的位置,需要使用禁止布局,函数 lv_btn_set_layout(btn, LV_LAYOUT_OFF)
1 | LV_LAYOUT_OFF //没有布局 |
除了通用事件以外,按钮还发送以下事件
LV_EVENT_VALUE_CHANGED 切换按钮时发送
lv_btn_create() 创建 btn 控件
设置函数
lv_btn_set_toggle() 设置 btn 的切换使能
lv_btn_set_state() 设置 btn 的状态
lv_btn_toggle() 切换 btn 的状态
lv_btn_set_layout() 设置 btn 的布局
lv_btn_set_fit4() 设置 btn4 个方向的自动调整策略
lv_btn_set_fit2() 设置 btn2 个方向的自动调整策略
lv_btn_set_fit() 设置 btn 的自动调整策略
lv_btn_set_ink_in_time() 设置 btn 进入墨水动画效果的时间
lv_btn_set_ink_wait_time() 设置 btn 保持墨水动画效果的时间
lv_btn_set_ink_out_time() 设置 btn 退出墨水动画效果的时间
lv_btn_set_style() 设置 btn 的样式
lv_btn_get_state() 获取 btn 状态
lv_btn_get_toggle() 获取 btn 的切换使能状态
lv_btn_get_layout() 获取 btn 的布局
lv_btn_get_fit_left() 获取 btn 左边的调整策略
lv_btn_get_fit_right() 获取 btn 右边的调整策略
lv_btn_get_fit_top() 获取 btn 顶部的调整策略
lv_btn_get_fit_bottom() 获取 btn 底部的调整策略
lv_btn_get_ink_in_time() 获取 btn 进入墨水动画效果的时间
lv_btn_get_ink_wait_time() 获取 btn 保持墨水动画效果的时间
lv_btn_get_ink_out_time() 获取 btn 退出墨水动画效果的时间
lv_btn_get_style() 获取 btn 的样式


函数 描述
创建和删除
lv_arc_create() 创建 arc 控件
设置函数
lv_arc_set_angles() 设置 arc 的角度
lv_arc_set_style() 设置 arc 的样式
参数获取函数
lv_arc_get_angle_start() 获取 arc 的起始角度
lv_arc_get_angle_end() 获取 arc 的结束角度
lv_arc_get_style() 获取 arc 的样式

lv_btnm_set_btn_width(btnm,btn_id,width); /* 设置按钮的宽度 */



函数 描述
创建和删除
lv_btnm_create() 创建 btnm 控件
设置函数
lv_btnm_set_map() 设置 btnm 的 map
lv_btnm_set_ctrl_map() 设置 btnm 的控制 map
lv_btnm_set_pressed() 设置 btnm 的某个按钮按下
lv_btnm_set_style() 设置 btnm 的样式
lv_btnm_set_recolor() 启用/禁用 btnm 的文本重新着色
lv_btnm_set_btn_ctrl() 设置 btnm 某个按钮的控制属性
lv_btnm_clear_btn_ctrl() 清除 btnm 某个控件的控制属性
lv_btnm_set_btn_ctrl_all() 设置 btnm 所有按钮的控制属性
lv_btnm_clear_btn_ctrl_all() 清除 btnm 所有控件的控制属性
lv_btnm_set_btn_width() 设置 btnm 某个按钮的比例宽度
lv_btnm_set_one_toggle() 启用/禁用 btnm 的单按钮 toggle
参数获取函数
lv_btnm_get_map_array() 获取 btnm 的 map 数组
lv_btnm_get_recolor() 获取 btnm 的文本重新着色使能状态
lv_btnm_get_active_btn() 获取 btnm 用户最后一次操作的按钮的
索引(按下、释放等)
lv_btnm_get_active_btn_text() 获取 btnm 用户最后一次操作的按钮的
文本(按下、释放等)
lv_btnm_get_pressed_btn() 获取 btnm 按下按钮的索引
lv_btnm_get_btn_text() 获取 btnm 某一个按钮的文本
lv_btnm_get_btn_ctrl() 获取 btnm 某个按钮的控制属性使能状
态
lv_btnm_get_style() 获取 btnm 的样式
lv_btnm_get_one_toggle() 获取 btnm 的单按钮 toggle 使能状态
bar 控件的构造非常简单,主要有两部分:
bar控件多用来显示进度等信息。


函数 描述
创建和删除
lv_bar_create() 创建 bar 控件
设置函数
lv_bar_set_value() 设置 bar 控件的值
lv_bar_set_range() 设置 bar 控件的范围
lv_bar_set_sym() 启用/禁用 bar 控件的从 0 开始对称绘
制
lv_bar_set_anim_time() 设置 bar 控件的动画时间
lv_bar_set_style() 设置 bar 控件的样式
参数获取函数
lv_bar_get_value() 获取 bar 控件的值
lv_bar_get_min_value() 获取 bar 控件的最小值
lv_bar_get_max_value() 获取 bar 控件的最大值
lv_bar_get_sym() 获取 bar 控件的对称使能状态
lv_bar_get_anim_time() 获取 bar 控件的动画时间
lv_bar_get_style() 获取 bar 控件的样式


函数 描述
创建和删除
lv_sw_create() 创建 sw 控件
设置函数
lv_sw_on() 设置 sw 控件状态为打开
lv_sw_off() 设置 sw 控件状态为关闭
lv_sw_toggle() 切换 sw 控件的状态
lv_sw_set_style() 设置 sw 控件的样式
lv_sw_set_anim_time() 设置 sw 控件的动画时间
参数获取函数
lv_sw_get_state() 获取 sw 控件的状态
lv_sw_get_style() 获取 sw 控件的样式
lv_sw_get_anim_time() 获取 sw 控件的动画时间

函数 描述
创建和删除
lv_calendar_create() 创建 calendar 控件
设置函数
lv_calendar_set_today_date() 设置 calendar 控件的当前日期
lv_calendar_set_showed_date() 设置 calendar 控件的显示日期
lv_calendar_set_highlighted_dates
()
设置 calendar 控件的突出显示日期
lv_calendar_set_day_names() 设置 calendar 控件的日期名称
lv_calendar_set_month_names() 设置 calendar 控件的月份名称
lv_calendar_set_style() 设置 calendar 控件的样式
参数获取函数
lv_calendar_get_today_date() 获取 calendar 控件的当前日期
lv_calendar_get_showed_date() 获取 calendar 控件的显示日期
lv_calendar_get_pressed_date() 获取 calendar 控件按下的日期
lv_calendar_get_highlighted_dates
()
获取 calendar 控件的突出显示日期
lv_calendar_get_highlighted_dates
_num()
获取 calendar 控件的突出显示日期的
数量
lv_calendar_get_day_names() 获取 calendar 控件的日期名称
lv_calendar_get_month_names() 获取 calendar 控件的月份名称
lv_calendar_get_style() 获取 calendar 控件的样式



函数 描述
创建和删除
lv_canvas_create() 创建 canvas 控件
设置函数
lv_canvas_set_buffer() 设置 canvas 控件的缓冲区
lv_canvas_set_px() 填充 canvas 的像素点
lv_canvas_set_palette() 使用索引格式设置 canvas 的调色板颜
色
lv_canvas_set_style() 设置 canvas 控件的样式
参数获取函数
lv_canvas_get_px() 获取 canvas 控件的像素点颜色
lv_canvas_get_img() 获取 canvas 的整个画布的图像
lv_canvas_get_style() 获取 canvas 的样式
绘制函数
lv_canvas_copy_buf() 复制缓冲区内容到 canvas 控件上
lv_canvas_rotate() 旋转图像并将其绘制在 canvas 控件上
lv_canvas_fill_bg() 填充 canvas 的背景
lv_canvas_draw_rect() 绘制矩形区域到 canvas 控件
lv_canvas_draw_text() 绘制文字到 canvas 控件
lv_canvas_draw_img() 绘制图像到 canvas 控件
lv_canvas_draw_line() 绘制线条到 canvas 控件
lv_canvas_draw_polygon() 绘制填充区域到 canvas 控件
lv_canvas_draw_arc() 绘制圆到 canvas 控件
1 | //其他宏定义,这些宏定义用于根据颜色格式和尺寸计算缓冲区大小 |

我们的控件对象创建在活动屏幕,顶层是用来弹出对话框(称为模态对话框)的,只有按了对话框才能继续操作活动屏幕,系统层是不受限制的,通常是鼠标触摸等的坐标表示。




边界:盒子占用的实际空间大小。
边框:月饼盒的纸箱厚度。
填充:用来保护盒子里的内容的,月饼缓冲泡沫。
轮廓(外边距):盒子和盒子之间连续摆放时的距离。table是选中foucs一个空间就是选中这个围合边框。就是如果没有触摸,要切换选中对象是就是通过这个边框来选择当前foucs哪个对象。
outline:不占据空间不包括在盒子里面。
LVGL的盒子对象是不会被撑破的:盒子可以弹性扩充,需要装多大空间的东西盒子就会自动扩大。但是受限与物理屏幕的可见区是固定的,所以可以拖动屏幕的可视区来观看整个盒子中的内容。arc也是一个盒子。





样式数组



本地样式的优先级高于普通样式。
样式是可以继承的,默认继承父类。






当一个对象收到一个事件,会把这个事件发送给它的父对象,它的父对象会在发送这个事件给它的父对象,从下网上一直传递(冒泡)。



事件一级一级网上传,中间的对象不会处理回调,只有触发事件的对象和最终的父对象会处理事件,中间的对象只传递。

1.内核库是为了保证内核能够独立运行的一套小型的类似 C 库的函数实现子集。
1.在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。
2.最低优先级留给空闲线程使用。
3.调度器在寻找那些处于就绪状态的具有最高优先级的线程时,所经历的时间是恒定的。
1.根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式。
2.使用定时器定时回调函数(即超时函数),完成定时服务。
1.消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。
2.邮箱和消息队列的发送动作可安全用于中断服务例程中。因为消息队列空间不属于任何线程,归系统所有。
RT-Thread 将 PIN、I2C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。实现了按名称访问的设备管理子系统,可按照统一的 API 界面访问硬件设备。在设备驱动接口上,根据嵌入式系统的特点,对不同的设备可以挂接相应的事件。当设备事件触发时,由驱动程序通知给上层的应用程序。
1.启动流程图

2.MCU外设硬件初始化,在调度器运行之前$Sub$$main()中初始化
1 | void rt_hw_board_init(void) |
3.系统或自己实现的模块初始化,在调度器开始后,在mian线程入口main_thread_entry()中初始化
1 | /** |
int value = 555;
!!value二次取反:第一次!将value转换成bool值false,第一次!将bool value = false转换成true,确保value的值一定是bool值,只有两个值true/false。
静态内核对象:存放在RW/ZI段中,在系统启动时对象被自动初始化。
动态内核对象:程序运行时在堆中创建,然后手工初始化。
静态对象会占用RAM空间,不依赖与内存管理器,内存分配时间确定。
动态对象则依赖内存管理器,分配对象时根据系统当时的内存情况决定分配的时间长短,分配好后可再被删除。
内核对象管理系统管理所有的内核对象,不管是静态内核对象还是动态内核对象统一动过管理器由对象名字来访问内核对象。
内核对象容器(数组的元素,即内核对象链表头):

内核对象:

此类可作为基类被包含在其它对象中,从而扩展自己的属性,我们可以通过构建一个类(通常是一个device)继承该类,实例化后挂载到内核里面,内核就可以通过名字来找到我们自己创建的对象了。
1 | // 初始化一个静态的内核对象,要求对象已经存在空间 |
只要是位于静态空间的对象都会被rt_object_init()初始化为:RT_Object_Class_Static 对象类型,由此用于区分是否是静态对象或者动态对象。
1 | #define RT_WEAK __attribute__((weak)) |
线程包括:线程控制块、线程栈、线程入口函数


线程初始化_rt_thread_init()时默认:

0为最高优先级,一般设置0-31,共32级,idle优先级设置为最低31,用户线程一般不设置优先级为31,设置0-30。

僵尸链表,线程退出是会自动调用rt_thread_exit()应该是放在函数入口lr寄存器,把当前线程假如到僵尸链表,杂空闲任务里面检测该链表并删除线程。

钩子函数可以钩入功耗管理、喂狗等工作,因为必须保证空闲线程要一直处于就绪态,钩子函数里面不能出现任何会使线程挂起的操作。
硬件定时器:上下文是中断中,在定时器外设中断检查定时器是否超时,超时则执行超时函数。
软件定时器:上下文是线程中,创建一个线程专门检查软件定时器是否超时,超时则执行超时函数。
在硬件定时超时函数不应去挂起、等到等操作,不应去申请释放动态内存。

RT_TIMER_SKIP_LIST_LEVEL定时器跳表层数,默认跳表是一层,相当于不使用跳表算法。

1 | // 硬件定时器初始化:初始化硬件定时器链表 |
单次定时器会在定时器超时后被从定时器链表中移除,而周期定时器会被从定时器移除后,再次按照定时器的超时键先后顺序重新插入定时器链表。

两种进入临界区的方式:
1 | void rt_enter_critical(void); // 锁调度器方式,防止线程调度 |
信号量进行P操作释放一个信号量的时候,唤醒该信号量挂起链表上的第一个线程。
计数信号量适用于工作处理速度不匹配的场合,一般计数信号量也应配合互斥量混合使用对资源加锁访问。

线程与线程之间的互斥采用互斥锁,而中断与线程之间只能采用开关中断的方式进行互斥。IPC对象的内部实现时需要资源保护也使用开关中断的方式。
互斥锁的特点:
1.所有权:互斥量有所有权只能由同一个线程对互斥量加锁解锁,互斥量会被线程持有,所有权归于此线程,只有这个线程可以对互斥锁解锁,其它线程不可以。
2.递归性:互斥量可以多次获取(需多次释放),二值信号量不能多次获取否则线程会被死锁。
3.继承性:互斥量的持有线程会继承,尝试获取该互斥量比自己高优先级的线性的优先级。
持有互斥量的线程应尽快释放互斥量,且不能在持有互斥量期间手动改变线程的优先级,切记互斥量不能在中断服务函数中使用,因为互斥量只能属于某个线程。中断中若要进行资源保护就使用开关中断的方式。

1.特定事件唤醒线程。
2.任意事件唤醒线程。类似于linux的select()集合,可实现io多路复用。
3.多个事件发生时唤醒线程。
一对多:一个线程对应多个事件。
多对多:多个线程对应多个事件。
逻辑或:线程与任意事件发生同步。
逻辑与:线程与若干事件都发生同步。
事件无排队性:多次向同一线程发送相同事件效果和发送一次一样。
非阻塞方式的邮件发送可用于中断,邮件的接收通常是以阻塞的方式接收所以不能用于中断。

suspend_sender_thread:发送邮箱的挂起等待线程链表。
邮箱创建时需动态分配一段空间用来存放邮件。
邮箱的使用场景:
1.一次只能发送小于等于4字节内容的情况。
2.发送一个缓冲区信息结构体的指针。
1 | // 缓存区信息结构体 |
1 | struct rt_messagequeue |
消息队列可用于线程与线程、中断与线程之间通信,中断中发送消息必须为非阻塞方式。
消息队列维护着两条链表:已有消息链表(msg_queue_head 到 msg_queue_free)、空闲消息链表(msg_queue_free 到 msg_queue_tail),最开始msg_queue_head和msg_queue_tail都等于NULL,msg_queue_free在消息池的最后位置,有第一条新消息时msg_queue_head = new_msg,以后就是msg_queue_head和msg_queue_tail不变,msg_queue_free移动。
1 | rt_err_t rt_mq_recv(rt_mq_t mq, |
同步消息场景:一个线程发送消息给另一个线程,另一个线程收到消息后要给线程发送一个ACK这个ACK可以使用邮箱或信号量。【消息队列 + 信号量或邮箱】
1 | struct msg |
信号和信号量等的最大区别是信号是异步通信方式。
1 | struct rt_thread |
1.信号是在软件层次上对中断的一种模拟,线程收到信号 –> 执行创建信号时绑定好的回调函数
线程信号线的三种处理:1.类似中断执行指定的处理函数, 2.忽略信号,不做任何处理, 3.对信号处理保留系统的默认值,系统默认的处理方式。
2.当信号被传递给线程时,如果线程被挂起,线程改为就绪去处理对应信号。当线程正在运行收到信号,那么它会在当前线程栈的基础上建立新栈空间(类似与中断和线程的空间都是独立的)去处理对应的信号。使用的线程栈大小也会相应增加。
1 | rt_sighandler_t rt_signal_install(int signo, rt_sighandler_t handler); // 安装信号,在线程给信号绑定一个回调函数 |
等待信号和信号量等没有区别,都是同步阻塞等待。
1 | // set: 指定等待的信号 |
本章小结:
1.不可以在中断中接收邮件,不可以在中断中以等待方式发送邮件。
2.不可以在中断中接收消息队列的消息。
3.邮箱的邮件必须使用全局、静态、动态申请形式,邮箱内容直接发送邮内容的指针。
4.消息队列消息内容可以使用局部形式,消息会被复制进消息队列的空间。
5.消息队列是一种异步通信方式,如接收方需告诉发送方已经接收到消息,可以通过邮箱或信号量的方式。
实时操作系统内存管理的特定和要求:
1.分配内存块事件必须确定。
2.可自动处理内存碎片的产生,不能重启。
3.嵌入式系统需针对不同的硬件内存大小给出不同的内存管理算法。
小内存算法:针对内存小于2M
memheap内存算法:多内存池的算法,可将多个内存堆合并在一起用。
slab内存算法:多内存池的通用算法。
因为内存算法需考虑多线程间的互斥问题,所以不能在中断中分配或释放内存,否则可能引起当前线程被挂起等待。

1 | // __CC_ARM编译器(kile), ZI 段结尾处表示 |
1 | #define HEAP_MAGIC 0x1ea0 |


mem.c 对应小内存分配算法,memheap.c对应有多个内存堆的算法。
1.malloc一块大内存,把这块内存分成指定数量的若干小块内存并用链表管理,空闲链表和使用的链表。
2.宏观来看内存池和消息队列类似,消息队列是对内存池的进一步封装。
1 | rt_mp_t rt_mp_create(const char *name, |

本章小结:
1.内存池可以极大的加快内存分配的速度,不用去遍历mem链表寻找合适大小的内存块。
2.从内存池分配的内存块大小是固定的。


Cortex-M 有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级,可通过 CONTROL 特殊寄存器控制。工作模式状态切换情况如上图所示。
Cortex-M 的堆栈寄存器 SP 对应两个物理寄存器 MSP 和 PSP,MSP 为主堆栈,PSP 为进程堆栈,处理模式总是使用 MSP 作为堆栈,线程模式可以选择使用 MSP 或 PSP 作为堆栈,同样通过 CONTROL 特殊寄存器控制。复位后,Cortex-M 默认进入线程模式、特权级、使用 MSP 堆栈
中断前导程序主要工作如下:
1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
2)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
1 | void rt_interrupt_enter(void) // 通知内核进入中断状态,把全局变量 rt_interrupt_nest 加 1 |
中断后续程序主要完成的工作是:
1 通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。
1 | void rt_interrupt_leave(void) // 通知内核离开中断状态,将全局变量 rt_interrupt_nest 减 1 |
1.在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务程序进行中断响应、处理。
2.中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。
3.中断栈可以保存在打断线程的栈中,当从中断中退出时,返回相应的线程继续执行。
4.中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。
RT-Thread 采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。
1.中段上半步:在中断中执行,负责取得硬件状态和数据,做好一些软件上的标记或者发送信号量、消息等,用来启动中断的下半部操作。
2.中断下半步:在线程中执行,线程收到中断发来的信号量、消息,进行具体的解析业务操作,此部分比较耗时。
全局中断开关也称为中断锁,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个线程主动放弃了处理器控制权。当需要关闭整个系统的中断时,可调用下面的函数接口:
1 | rt_base_t rt_hw_interrupt_disable(void); // 关中断 |
使用中断锁来操作临界区的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。
例如,为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量:
1 | /* 关闭中断 */ |
在使用中断锁时,需要确保关闭中断的时间非常短,例如上面代码中的 a = a + value; 也可换成另外一种方式,例如使用信号量:
1 | /* 获得信号量锁 */ |
这段代码在 rt_sem_take 、rt_sem_release 的实现中,已经存在使用中断锁保护信号量内部变量的行为,所以对于简单如 a = a + value; 的操作,使用中断锁将更为简洁快速。
简单嵌套中断使用
1 | #include <rthw.h> |
使用 rt_interrupt_enter/leave() 的作用是,在中断服务程序中,如果调用了内核相关的函数(如释放信号量等操作),则可以通过判断当前中断状态,让内核及时调整相应的行为。例如:在中断中释放了一个信号量,唤醒了某线程,但通过判断发现当前系统处于中断上下文环境中,那么在进行线程切换时应该采取中断中线程切换的策略,而不是立即进行切换。
1 | void rt_interrupt_enter(void); // rt_interrupt_nest++ |
但如果中断服务程序不会调用内核相关的函数(释放信号量等操作),这个时候,也可以不调用 rt_interrupt_enter/leave() 函数。
当驱动外设工作时,其编程模式到底采用中断模式触发还是轮询模式触发往往是驱动开发人员首先要考虑的问题,并且这个问题在实时操作系统与分时操作系统中差异还非常大。因为轮询模式本身采用顺序执行的方式:查询到相应的事件然后进行对应的处理。所以轮询模式从实现上来说,相对简单清晰。例如往串口中写入数据,仅当串口控制器写完一个数据时,程序代码才写入下一个数据(否则这个数据丢弃掉)。相应的代码可以是这样的:
1 | /* 轮询模式向串口写入数据 */ |
在实时系统中轮询模式可能会出现非常大问题,因为在实时操作系统中,当一个程序持续地执行时(轮询时),它所在的线程会一直运行,比它优先级低的线程都不会得到运行。而分时系统中,这点恰恰相反,几乎没有优先级之分,可以在一个时间片运行这个程序,然后在另外一段时间片上运行另外一段程序。
所以通常情况下,实时系统中更多采用的是中断模式来驱动外设。当数据达到时,由中断唤醒相关的处理线程,再继续进行后续的动作。例如一些携带 FIFO(包含一定数据量的先进先出队列)的串口外设,其写入过程可以是这样的,如下图所示:

线程先向串口的 FIFO 中写入数据,当 FIFO 满时,线程主动挂起。串口控制器持续地从 FIFO 中取出数据并以配置的波特率(例如 115200bps)发送出去。当 FIFO 中所有数据都发送完成时,将向处理器触发一个中断;当中断服务程序得到执行时,可以唤醒这个线程。这里举例的是 FIFO 类型的设备,在现实中也有 DMA 类型的设备,原理类似。
对于低速设备来说,运用这种模式非常好,因为在串口外设把 FIFO 中的数据发送出去前,处理器可以运行其他的线程,这样就提高了系统的整体运行效率(甚至对于分时系统来说,这样的设计也是非常必要)。但是对于一些高速设备,例如传输速度达到 10Mbps 的时候,假设一次发送的数据量是 32 字节,我们可以计算出发送这样一段数据量需要的时间是:(32 X 8) X 1/10Mbps = 25us。当数据需要持续传输时,系统将在 25us 后触发一个中断以唤醒上层线程继续下次传递。假设系统的线程切换时间是 8us(通常实时操作系统的线程上下文切换时间只有几个 us),那么当整个系统运行时,对于数据带宽利用率将只有 25/(25+8) =75.8%。但是采用轮询模式,数据带宽的利用率则可能达到 100%。这个也是大家普遍认为实时系统中数据吞吐量不足的缘故,系统开销消耗在了线程切换上(有些实时系统甚至会如本章前面说的,采用底半处理,分级的中断处理方式,相当于又拉长中断到发送线程的时间开销,效率会更进一步下降)。
通过上述的计算过程,我们可以看出其中的一些关键因素:发送数据量越小,发送速度越快,对于数据吞吐量的影响也将越大。归根结底,取决于系统中产生中断的频度如何。当一个实时系统想要提升数据吞吐量时,可以考虑的几种方式:
1)增加每次数据量发送的长度,每次尽量让外设尽量多地发送数据;
2)必要情况下更改中断模式为轮询模式。同时为了解决轮询方式一直抢占处理机,其他低优先级线程得不到运行的情况,可以把轮询线程的优先级适当降低。
注:由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断做为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间。
本章小结:
1.中断服务程序应该尽可能精简。
2.对于是否使用中断下半部处理,需要考虑中断服务下半部的的处理时间是否大于发送信号量的时间。
3.建议实时系统尽量使用中断的模式驱动外设,配合DMA缓冲使用,一次发送尽可能多的数据,然后尝试一次中断和线程切换8us,如需使用轮询应设置轮询线程较低的优先级,保证其他线程的运行时间。
移植分成CPU架构移植和BSP(Board Support Package,板级支持包)两部分。
主要文件:cpuport.c context_rvds.S
主要接口:
1 | /************** context_rvds.S 实现 ***************/ |
在中断服务函数中进程线程切换需要等到中断结束后才能进行切换,在线程中进行线程切换是马上进行切换。
在中断程序处理完事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量(调用 rt_hw_context_switch_interrupt 函数时会标记该变量和设置from、to线程等待中断处理完成进行线性切换)如果为1,就根据 rt_interrupt_from_thread, rt_interrupt_to_thread 变量,进程一次上下文切换。
向上对内核提供一套统一的函数接口,包括全局中断开关函数、线程栈的初始化函数、上下文切换函数、时钟节拍配置和中断函数。

僵尸线程:已经被系统删除,线程不属于任何ipc量,对调度器不可见但是它的资源还未被释放的线程叫僵尸线程。
僵尸线程链表:rt_thread_defunct。
在线程创建时通过 rt_hw_stack_init 函数手动构建一个上下文内容,作为每个线程第一次执行的初始值 ,如下图:


Cortex-M处理器在线程上下文切换过程中,pendsv发生后,r0
psr 由硬件自动压栈/出栈,r4r11需程序手动压栈/出栈(由 rt_hw_context_switch 函数完成)。

特殊功能寄存器是按位来表示的。

开启一个定时器中断,可以是systick也可以是rtc,但需要保证这个定时器无论什么时候都在运行就算设备休眠进入低功耗。
1 | void SysTick_Handler(void) |
板卡移植:CPU架构移植已经实现CPU芯片相关的移植,但还需要使RAM、GPIO、UART等外设工作才能建立操作系统运行的基本环境。
传统意义上的嵌入式计算机CPU和RAM是毫无关系的,不同的CPU可以配不同的RAM,只是MCU把CPU和RAM集成在了一个芯片,所以CPU架构移植不包括RAM的部分。
主要工作:
1.初始化CPU内部寄存器,设定RAM工作时序。
2.实现时钟驱动及中断控制器驱动,完善中断管理。
3.实现串口和GPIO驱动。
4.初始化动态内存堆,实现动态内存堆管理。
1.拷贝rt-thread文件夹到工程文件夹,添加内核源码、cpulib层、board.c相关文件到工程,添加相关头文件目录。
1.在boar.c初始化滴答定时器配置中断,在中断函数中添加rt_rt_tick_increase();
1.初始化串口。
2.实现rt_hw_console_output()函数。
1.在rt_hw_board_init()函数调用rt_system_heap_init()初始化系统堆内存。
最后运行结果:

支持使用不同编译器,如GCC、ARM_CC等
搭建项目工程
在BSP工程目录下使用:scons –dist命令在BSP目录下生成dist目录,这边是一个新羡慕的工程框架。其中包含rttread源码,不相关的BSP文件夹及libcpu都会被移除。
1 | socons --target=mdk5 # 生成mdk5工程 |
1 | socons # 需arm平台的芯片 |
menuconfig是一种基于Kconfig的图形化配置工具。通过图形配置最终生成rtconfig.h配置文件,对系统进行配置、裁剪。

每次通过menconfig图形配置界面配置删除或添加软件包后,需要使用 scons –update命令更新软件包。
1 | pkgs --upgrade # 同步git服务器上的软件包 |
如果要修改MCU型号,工程配置等,建议直接修改 template 工程,再使用 socons –target=mdk5 重新生成工程。
scons 是按照模板工程生成工程的。
又称msh(module shell),类似与bash/dos。
又称C-style模式,此模式下控制台能解析并执行大部分C语言的表达式,并使用类似C语言的函数调用方式访问系统中的函数及全局变量,也能够通过命令行创建变量。
1 | list_thread() # 打印系统中所有的线程,C-style模式命令必须携带括号 |
1 | MSH_CMD_EXPORT(name, desc); |
1 | FINSH_FUNCTION_EXPORT(name, desc); |
IO设备管理层框架从下往上一次共三层:设备驱动层 –> 设备驱动框架层 –> IO设备管理层
IO设备框架向下操作硬件,向上提供统一的一套接口给应用程序调用,使得应用程序不用关系具体的硬件操作,更换硬件执行修改设备驱动操作硬件相关的代码。
1.实现了对设备驱动程序的封装,使程序完全脱离了具体的硬件。
2.提供给应用程序的接口:
1 | /************** device.c *******************/ |

3.提供给驱动程序实现的:
1 | // 驱动程序实例化一个设备对象,并通过此接口注册到IO设备管理层 |
1.对同类硬件设备驱动做抽象:将不同厂家的同类硬件设备驱动中相同的部分抽取出来。
2.将不同的部分留出接口,由驱动程序实现。
3.负责创建和注册IO设备(就是设备框架层将不同部分留出的接口,设备驱动层需要去实现这些接口,注册给设备框架层使用)。
4.通过IO管理层提供的接口,注册一个串口设备到IO管理层:
5.设备驱动框架是对同类设备的抽象(这一层可以没有),例如:显示设备和ssd1306的关系,显示设备可以是ssd1306或者lcd显示屏,但它们都属于显示设备都能够显示一个字母、显示一个数字。
设备框架层就是对同类设备的抽象,同类设备的一些共性。
4.不同芯片厂家的串口操作方式是不一样的,但他们肯定都有putc、getc、config的这些功能。
1 | /*************** serial.c ******************/ |

对于逻辑操作简单的设备,可以跳过设备框架层,直接注册到IO设备管理层,将设备框架层必要的东西直接在设备驱动层实现。
通常是直接调用厂家提供的操作硬件寄存器函数
1.是一组直接驱使硬件工作的程序,实现访问硬件提供的功能。
2.设备框架层提供的接口,注册一个串口设备到设备框架层。
硬件相关的:配置具体的外设、读、写等,硬件的功能实现函数。
这些硬件相关的操作函数都将被,驱动层使用注意不是调用(在驱动层封装成硬件设备对象的方法,再由驱动层注册到IO设备层),所以IO设备驱动层对设备对象的操作相当于直接调用了这些函数。
以上的这些层都是为应用程序服务的,宏观上这些层都可以叫做驱动,驱动只提供方法不会操作硬件,只有应用程序里面才会调用这些设备对象的方法操作硬件,可能进行设备初始化、打开、关闭、读、写,根据设备提供的方法不一样不一定所有的设备都有这些方法。
1 | /******************** drv_uart.c **************************/ |

例如shell.c是一个应用程序,它和用户直接交互
应用程序:实现产品具体功能的代码,与用户交互的、显示用的、一个触摸的键盘,分别获取用户的输入做出反应、输出信息给用户看、得到用户的输入。
1 | /******************** shell.c ****************************/ |
从上面的代码可以看出,只有应用程序才会去操作硬件,驱动程序只提供操作硬件的方法。
小结:
分层思想的原则:
1.确定本层的职责,搞清承上启下的对象是谁。
2.实现本层职责时,提供好给上层调用的接口和对象。
3.如果超出本层职责(操作过于细节),那么提供注册函数给自己的下层调用,让下层提供一个实现具体细节的方法给自己使用。
4.一般方法和信息都是通过对象的形式在层与层之间传递。
1 | // 设备类型 |
常见设备:字符设备:允许非结构数据传输,串行传输一次一个字节,块设备:每次传输一个数据块,如512byte,数据块是硬件强制性的,通常情况下操作,先读一个块数据,改变之中的一部分数据然后再写回去。

设备的多种打开方式:
1 | #define RT_DEVICE_FLAG_INT_RX 0x100 /**< INT mode on Rx */ |
如果上层应用程序需要设置设备的接收回调函数,则必须以 RT_DEVICE_FLAG_INT_TX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打开设备,否则不会回调函数。
1 | // 该函数主要执行 dev->open(); |

rt_device_open() 和 rt_device_close() 需成对使用。
1 | // 该函数主要执行 dev->control(); |
1 | // pos:根据设备的不同有不同的意义,读串口设备底层会忽略这个参数,pos参数没有一样 |
接收回调:
1 | // 设置接收数据回调:设置一个回调函数,当硬件设备接收到数据时, |
发送回调:
1 | // 设置发送完数据回调:当硬件设备发送完数据,驱动程序回调这个函数并把发送完成的数据块地址buffer作为参数传递给上层应用, |
上面两个函数都是应用程序注册给底层硬件驱动回调用的,相当于是底层驱动异步通知应用程序执行的操作结果。是一种层与层之间的沟通方式,和底层注册到上层类似。
本章小结:
1.没有提供驱动的设备,或者自己另外需要添加的设备,需要根据驱动框架自己实现设备的方法。
2.动态创建的设备销毁时要记得释放设备控制块占用的内存。
1 | int xxx_writedata(uint8_t* buffer, size_t buff_size) |
rtthread对常用的片内外设做了抽象,为同一类外设提供了通用接口,对于不同的MCU片内外设都可以使用同一套外设接口进行访问。
serial.c serial.h
1 | struct rt_serial_device |
举例说明下ops里面串口的配置方法:
1 | // control方法是可以说是多个方法的集合,它根据 cmd 的不同,可以执行不同的操作:比如:开中断、关中断、配置串口dma、配置dma中断等 |
串口设备使用步骤:
1.串口设备实例化一个串口驱动设备对象,并通过rt_hw_serial_register()接口注册到设备框架中。
2.串口设备驱动框架通过rt_device_register()接口将设备注册到IO设备管理中。
3.应用程序通过IO设备管理提供的接口访问串口设备硬件。
串口设备时序图:

1 | @startuml |
时序图软件:plantuml
1.驱动注册到驱动框架:注册设备的操作方法
1 | // 驱动注册到驱动框架 |
2.驱动框架注册到IO设备管理器:驱动框架设备挂载到内核对象链表
1 | // 驱动框架设备注册到IO设备管理器 |
注册设备时的flags参数:
1 | #define RT_DEVICE_FLAG_RDONLY 0x001 /**< read only */ |
驱动层到io设备管理层对象空间占用是一层一层减小

串口回调:
1 | // 在串口中断中回调 |
1 | // spi总线对象 |

SPI总线是一个虚拟的设备(对应MCU硬件上的SPI控制器),总线可以挂载多个从设备,靠CS引脚来区分,总线现在被哪个从设备占用。 struct rt_spi_bus总线对象里的owner用来记录当前总线被哪个从设备占用。struct rt_spi_device从设备对象里的bus用来记录当前从设备挂载在哪个总线上。
1.SPI总线设备创建,drv_spi.c 驱动层里面实例化一个SPI总线对象。
2.SPI总线设备注册到 SPI设备驱动框架层:spi_core.c、spi_device.c ,通过 rt_spi_bus_register() 接口。
3.SPI设备驱动框架对象注册到IO管理层:rt_device.c ,通过 rt_device_register() 接口。
SPI设备驱动框架层由两个文件组成:spi_core.c、spi_device.c
4.在SPI从机设备驱动创建一个SPI从设备:对应具体的SPI外设芯片,在设备的驱动程序实现:比如 ssd1306.c ,使用 rt_spi_bus_attach_device() 里面会实例化一个spi从设备对象,并把从设备对象挂载到指定的一个总线上。
由于 rt_spi_bus_attach_device() 只是创建了一个从设备对象,并把它挂载到一个总线,但并没有配置该从设备通信时主设备该设置成什么参数配置(每个从设备占用总线通信的时候可能需要的总线配置是不一样的,默认的配置是 0),这里需要接着调用 rt_spi_configure() 接口配置总线以适合该从设备和总线(主设备)通信:数据宽度、时钟速度、模式等配置。配置完这么参数后总线上的主设备从能和这个从设备通信。
1 | int ssd1306_attach_to_spi_bus(void) |
总线:主设备+虚拟总线的统称。
5.挂载从设备对象到SPI总线:ssd1306.c 和步骤 4 创建对象时合成在一起了,通过 rt_spi_bus_attach_device() 接口。
6.注册spi从设备对象到IO设备管理层:rt_device.c ,通过 rt_device_register() 接口。
7.通过从设备的设备句柄调用 rt_spi_transfer_message() 就可以对从设备收发数据。
8.spi_drv.c 驱动中在检测到该从机是首次收发数据的时候,会调用congfig() 先配置从机,接着在 通过 xfer() 收发本次的数据。
spi_drv.c 驱动中动过检测发送数据的总线对象里面的 owner 是否是本从设备,是代表总线之前就已经被该设备占用,之前肯定是配置过从设备了,不是就代表之前总线不是被该从设备占用需要配置该从设备。

小结:
1.rtthread中把MUC内部的SPI控制器虚拟成了一个虚拟总线。
2.总线在系统启动是就已经创建并注册到IO设备管理器。
3.当我们在应用程序中需要创建挂载一个从设备的时候,首先创建从设备并挂载到总线,还要设置好该从设备与总线通信时,总线的配置参数。
重复条件:在一次通信过程中,当主机需要和不同的从机传输数据或者需要切换读写操作,主机可再发送一个开始条件。
例如:主机需要读取从机某个寄存器的数据
1.主机发送起始信号。
2.主机发送从机地址,写标志。
3.从机应答后,主机再发送一次起始信号,发送寄存器地址,读标志。
4.主机释放总线,接收从机发来的数据。
5.主机收到数据ACK,不想再收从机的数据了就发NACK。
6.主机发送停止位,结束本次通信。

主机向从机的某个寄存器写数据:
1.主机发送起始信号。
2.主机发送从机地址,写标志。
3.从机应答后,主机接着发送寄存地址。
4.从机应答后,主机接着发送要写入的数据。
5.从机应答后,主机发送停止位结束本次通信。

本章小结:
1.i2c设备接口使用的从机地址均不包含读写的地址。
全称:Device File System,即设备虚拟文件系统。
1.提供POSIX标准接口:read、write、poll/select
2.支持多种文件系统,例如:FatFS、RomFS、DevFS,提供普通文件、设备文件、网络文件描述符管理。
3.支持多种类型的储存设备,如:SDCard、SPI Flash、Nand Flash等。
4.主要分为:POSIX接口层dfs_posix.c 、DFS虚拟文件系统层dfs_file.c 、设备抽象层。

可移植性操作系统接口,规定的一组api函数接口。
用户将具体的文件系统注册到DFS框架中,如:FatFS、RomFS、DevFS 这些文件系统。
1.FatFS:兼容微软FAT格式文件的开源文件系统,专为小型嵌入式设备开发,采用 ANSI C 编写,具有良好的硬件无关性以及可移植性,是 RT-Thread 中最常用的文件系统类型。
2.传统型的 RomFS 文件系统是一种简单的、紧凑的、只读的文件系统。
3.Jffs2 文件系统是一种日志闪存文件系统。主要用于 NOR 型闪存,基于 MTD 驱动层,特点是:可读写的、支持数据压缩的、基于哈希表的日志型文件系统,并提供了崩溃 / 掉电安全保护,提供写平衡支持等。
4.DevFS 即设备文件系统,在 RT-Thread 操作系统中开启该功能后,可以将系统中的设备在 /dev 文件夹下虚拟成文件,使得设备可以按照文件的操作方式使用 read、write 等接口进行操作。
5.UFFS 是 Ultra-low-cost Flash File System(超低功耗的闪存文件系统)的简称。它是国人开发的、专为嵌入式设备等小内存环境中使用 Nand Flash 的开源文件系统。与嵌入式中常使用的 Yaffs 文件系统相比具有资源占用少、启动速度快、免费等优势。
设备抽象层将物理设备如 SD Card、SPI Flash、Nand Flash,抽象成符合文件系统能够访问的设备,例如 FAT 文件系统要求存储设备必须是块设备类型。


1 | int dfs_mkfs(const char * fs_name, const char * device_name); |

1 | int dfs_mount(const char *device_name, |
其他在C语言代码中的操作直接调用文件系统提供的POSIX接口就行。shell中也可使用文件操作的命令,创建删除文件和目录文件、切换目录等命令。
1 | #include "dfs_fs.h" |

rtconfig.h中的宏添加:
1 | /* DFS虚拟文件系统所需宏 */ |
AT命令、SAL层(套接字抽象层)、BSD Socket API(标志网络套接字API)、LwIP

xxAA:片内 Flash 512K字节,400KB 的Flash可以用于应用程序,片内RAM 64K
字节。
xxAB:片内 Flash 256K字节,超过100KB 的 Flash可以用于应用程序,片内RAM
32K字节。

备注:链路层下面还有个物理层。
GAP:配置怎么来访问BLE的,包括配置广播的名字,连接首选项参数。GAP 层负责处理设备的接入方式和过程,包括设备发现,链路建立,链路终止以及实现绑定。
GATT:配置文件,建立在ATT上,通过ATT定义如何发现、读写服务、特征、描述符的方法。GATT层定义了服务器和客户端。
ATT:服务属性、特征,规定怎么去服务器端读属性,供给客户端进行读写。
SM:是否设置配对密码等安全相关的
以下底层:蓝牙协议栈内部管理。
LL:负责广播、扫描、建立连接和维护连接,确保数据的发送接收准确。本层有一个状态机。
注意:
1.主机和从机是链路层的叫法,服务器和客户端是GATT层的叫法,主机可以是服务器也可以是客户端,从机也一样。
2.外围设备、中心设备是GAP层的叫法。
应用层定义了三种种类型:特征(characteristic)、服务(service)和配置文件(profile)。
1.特征:以通用唯一识别码(UUID)作为表示的一个小块数据,可以被重复使用,不涉及行为。当计算机遇到一个从未接触过的特征时,计算机可以去更新这个特征的读取规则。
2.服务:人类可读的一组特征及其相关的行为规范,只定义了位于服务器上的相关特性和行为,而不定义客户端的行为。服务有两种类型,首要服务和次要服务。次要服务是那些协助主要
业务或其他次要服务的服务。3.配置文件:是用例或应用的最终体现。配置文件描述了如何发现和连接设备,配置文件还描述的客户端的行为,用于发现服务和服务特性。

1.扫描到白名单后,进入就绪态,在进入发起态,再变为连接态(不是扫描态直接就进入发起态)。
2.只有连接太会用到数据信道,但是如果是广播扩展广播包应该也是会用到数据信道的。
3.BLE芯片上电,链路层首先一定是进入就绪态,然后在进入到广播或者是扫描态。
4.这是是LL层的状态机,不是整个协议栈的状态机。
连接间隔:必须是 1.25ms 的倍数,范围是从最小值 6(7.5ms)- 3200(4.0s)
从机延迟:可以选择跳过连接事件,并保持睡眠,这一决定取决于外围设备。
监督超时:这是两个成功的连接事件之间间隔的最大值。如果超过这个时间还未出现成
功的连接事件,那么设备将会考虑失去连接,返回一个未连接状态。10ms 的步进(10ms 的倍数),10(100ms)- 3200(32.0s)
profile是 Service 的集合,它是预定义的,并不是实际存在于设备中。
特征是具有特定意义的数值,如心率、温度值等等。BLE 主从机之间的数据传输实际
传输的就是特征值。
UUID(Universally Unique Identifier)是一个 128位的数字,用来标志属性的类型。Service
和Characteristic都是一种属性,都需要一个唯一的UUID 来标识。
模式(Mode):模式描述是设备的工作状态,当一个设备被配置为按照某种方式操作一
段较长的时间时,称为模式。如广播模式,表示设备正处于广播状态,一般会持续很长
时间。
规程(Procedure):规程描述的是在有限的时间内进行特定的操作,如连接参数更新规
程,它是在较短的时间内执行了连接参数更新的操作。
GAP角色:广播者、观察者、外围设备、中心设备。
广播者:广播发送者,不是可连接的设备。
观察者:扫描广播,不能够启动连接。
外围设备:广播发送者,是可连接的设备,连接后成为从设备。
中心设备:扫描广播启动连接,连接后成为主设备。
GAP服务:设备名字、外观特征、外围设备首选连接参数、中心设备地址解析。
Device Name特征
Appearance特征 :蓝牙小图标
PPCP特征 :外围设备首选连接参数,在主从连接后外围设备发送给中心设备
Central Address Resolution特征 ,0 该设备不支持地址解析。1 该设备支持地址解析。
Resolvable Private Address Only特征 ,检查对端设备是否仅在绑定后使用可解析私有地址(RPA)
主从机奖励连接后,从机发起更新连接参数
从机和主机刚建立连接时,会使用“快速”的连接参数以达到迅速交换信息的目的
主机发起的叫请求,从机只有接受断开连接。从机发起的叫协商,主机可以接收或者拒绝。
字节序:大多数多字节域是从低字节开始传输的。注意,并不是所有的多字节域都是从
低字节开始传输的。
比特序:各个字节传输时,每个字节都是从低位开始。
设备地址:
1.静态地址(可使用芯片固化的,或者自己在上电时设置),最高两位必须为1
2.不可解析私有地址:周期变化的地址,最高2bit必须为0。
3.可解析设备地址:最高2bit必须是0和1。
Flags :
1.有限可发现模式。
一个处于有限可发现模式的设备正在广播,那么他一定是刚被用户操作过并且极希
望被连接。
发射功率等级
发射功率等级可以用来计算路径损耗,计算公式如下:
pathloss = Tx Power Level – RSSI。
例:发射功率+4db,扫描到的RSSI是-40db,则路径损耗为:4 - (-40)= 44db
从机广播自己的发射功率,主机可以结合RSSI和从机的发射工具计算路径损耗,判断哪个设备离自己近。
UUID全部是128位的,UUID有个128位的基数,通过将16位的UUID和UUID基数合并都到最终的UUID。

使用16位的uuid目的是为了,BLE传输更少的数据,提高效率。
0x1800 ~ 0x26FF:用作服务类通用唯一识别码。
0x2700 ~ 0x27FF:用于标识计量单位。
0x2800 ~ 0x28FF:用于区分属性类型。
0x2900 ~ 0x29FF:用作特征描述。
0x2A00 ~ 0x7FFF:用于区分特征类型。
广播服务数据(例如电池电量)
广播连接范围,用于给中心设备做参考
**广播厂商自定义数据 **
1 | typedef struct |

1.服务是一组特征和通过它们所公开的行为的集合,一个服务可以包含多个特征。如心率服务包含Heart Rate measurement、 Body Sensor Location和Heart Rate Control Point这些特征。
2.服务也和 Profile 一样,分为标准服务和自定义服务。
3.服务存在于从机中,每个服务代表从机的一个能力。
4.porfile规定必须包含哪些服务,哪些服务是可选的。

心率porfile必须包含:心率服务、设备信息服务,其他的一些服务是可选的。
特征包含三个部分:声明、数值和描述符,其中声明和数值是必不可少的,而描述符可
以是一个或多个。

对于特征来说,要建立一个特征,首先要进行声明,如下图所示:
特征性质是一个 8位字段


特征数值是一个属性,特征性质字段给出了特征数值属性的访问权限,如读/写等等,
通过特征性质字段的描述,确定了特征数值属性可以执行的操作类型。
特征的描述符大多数描述符是可选的,一个特征可以包含 0 到多个描述符,需要注意的
是如果使用了通知(notify)或指示(indicate),必须要有客户端特征配置描述符(Client
Characteristic Configuration Descriptor,CCCD),CCCD的UUID是0x2902。
心率 Profile 定义了如下图所示的两类角色:心率传感器和集中器(Collector),心率传
感器是测量心率和其它信息的设备,集中器是接收心率传感器心率测量值和其它信息的设备。
1.心率传感器应是 GATT服务器。
2.集中器应是GATT客户端。

BLE的数据传输是双向的:从机主动向主机发起数据传输和主机向从机发起数据传输。
从机如果想主动发起数据传输,只能通过两种方式:通知(Notify)和指示(Indicate)
1.通知(Notify) :从机发送通知后,不会关心主机有没有接收到,通知属于不可靠消息。
通知需要使用客户端配置描述符(CCCD)配置(CCCD使能/关闭通知)。
2.指示(Indicate) :从机发送指示后,必须得到主机的应答才能发送下一条指示,指示属
于可靠消息。指示同样需要使用客户端配置描述符(CCCD)配置(CCCD 使能/关闭指示)。
主机如果想主动发起数据传输,也有两种方式:读和写,通过读和写,主机可以读/写
特征值和特征描述符。
1.读(Read) :主机通过“读”可以读取从机的特征值和特征描述符,从而获取从机的数
据。
2.写(Write) :主机通过“写”可以写入从机的特征值和特征描述符,从而将数据发送给
从机。

主机通过读写从机的特征描述符来使能/关闭是否接收主机发来的通知或指示。


profile小结:
1.profile规定了两个角色:收集器(客户端)、传感器(服务端),传感器包含一个或多个服务,服务里面又包含特征、描述符。
2.收集器可以读写传感器的特征值和描述符。
3.传感器可以通过通知或者指示,主动给收集器发送数据。
4.收集器可以通过读写特征的描述符,决定是否被动接收传感器主动发来的特征值。
5.多有描述符的uuid都为固定的0x2902。
主机:客户端、收集器。
从机:服务端、传感器。
通知是不可靠协议,不需要主机应答。指示是可靠协议,需要主机应答,才进入下一次指示。
相关API函数:
服务添加到属性表 :
1 | uint32_t sd_ble_gatts_service_add ( |

添加特征 :
1 | uint32_t characteristic_add ( |

句柄就是一个uit16_t的值。

特征值的长度:可变长度。特征值的长度可以设置为可变长度和固定长度,对于可变长度,需要设置最大长度 max_len、初始长度 init_len和可变长度标志 is_var_len(设置为true)。对于固定长度,将最大长度 max_len 和初始长度init_len设置为一样,可变长度标志is_var_len 设置为false即可。

服务中提供了一个回调函数,在相应事件来到时,就可以扩展很多自己要做的事件了。

服务或者特征都是通过在创建时协议栈分配一个句柄(uint_16),发送数据的时候带上这个句柄,主机接收到数据就知道是哪个特征来的数据了。
1.心率服务需要发送和接收主机数据,需要知道当前主从是否已连接,所以心率服务要注册事件监视者(提供一个回调给协议栈)。调用函数:
1 | BLE_HRS_DEF(m_hrs); // 创建心率服务结构体,会注册一个回调给协议栈(注册监视者):void ble_hrs_on_ble_evt(ble_evt_t const *p_ble_evt, void *p_context) |
心率服务有读通知比较复杂,会创建一个结构体来标识服务。
2.设备信息服务只用响应主机的读,所以它不用注册事件监视者。只用创建自己的特征就行。调用函数:
1 | static uint16_t service_handle; |
设备信息服务只包含了一个只读的特征,对于这种简单的服务,不需要专门定义一个结构体来标志服务,只需要定义一个服务句柄变量来标志服务即可。
服务通过协议栈 API 函数 sd_ble_gatts_service_add()加入到属性表,服务加入成功后,协议栈会给服务分配 16 位的句柄,该句柄会通过函数的输出参数保存到之前定义的“service_handle”变量中,之后,设备信息服务句柄即可用来标志设备信息服务。
1 | uint32_t characteristic_add(uint16_t service_handle, // 添加服务时协议栈给服务分配的句柄 |
BLE用户部分只有发送函数(通知或指示),没有接收函数,数据的接收是协议栈完成,协议栈接收到数据会向用户层提交数据内容相应的事件。


1.掌握如何实现长包传输(单次传输最大长度 247个字节,有效载荷 244个字节)。
2.自定义服务需要向Softdevice 写入128位的UUID 基数。
3.广播中如何增加扫描响应?为什么要增加扫描响应?


对于广播中包含自定义服务 UUID,除了注意数据长度之外,还需要注意广播初始化中会用到自定义的UUID 的基数,而自定义UUID 的基数在是服务初始化时写入到 Softdevice的,因此服务初始化必须在广播初始化之前执行。

1.自定义UUID profile流程和标志uuid流程基本一样:首先添加服务,再添加服务下的各个特征。只是在最开始需要将128位的uuid注册到协议栈,并设置宏告诉协议栈自定义uuid的个数。
2.注册观察者,回调函数中接收 BLE_GATTS_EVT_WRITE 写事件和 BLE_GATTS_EVT_HVN_TX_COMPLETE 发送完成事件(主机写事件会携带收到的数据上来)。
3.在以上两个事件中基础下,标记自己的业务事件,传递数据,拿到数据并做自己相关的业务处理。

因为初始化广播的时候,广播扫描响应包要广播128位的自定义uuid,所以这时自定义uuid必须准备好(即先初始化服务准备好自定义uuid,再初始化广播,有先后顺序)。

1.蓝牙4.2之前,BLE 的MTU(Maximum Transmission Unit,最大传输单元)为23 个字节,这 23 个字节中包含了 1 个字节的操作码(op code)和 2 个字节的属性句柄(attribute handle) ,因此一次最多传输 20个字节。
2.从蓝牙 4.2开始,BLE 支持长包传输,MTU 扩展到了247个字节,除去 op code和attribute handle,一次可传输的最大长度为 244个字节。
3.客户端和服务器是通过协商来交换各自支持的 MTU 的长度的,这个过程称为 MTU 交换(Exchange MTU),通过MTU交换,客户端和服务器具备了自适应MTU 的功能,但是注册每个建立的连接只能在连接建立时交换一次,而不能实时通过 MTU交换协商数据传输长度。
若服务器和客户端连接后没有进行MTU交换操作,默认MTU等于23字节。
1.客户端发送 MTU交换请求

2.服务器发送 MTU 交换响应

MTU 交换之后,客户端和服务器应将 MTU 设置为交换的最小值,并且客户端和服务器的MTU 应设置为相同的值,以确保客户端可以正确检测长属性读取的最终数据包。



从机常用API:
1 | // 请求使能 SoftDevice,该函数会根据 sdk_config.h 文件中低频时钟的设置来配置低频时钟 |


FS优点:
FS缺点:

缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia-plus根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true