目录
- 项目概述
- 硬件系统设计
- 软件系统架构
- RT-Thread 操作系统移植
- U8G2 图形库移植
- 核心设备驱动开发
- 应用程序设计
- 系统测试与性能分析
- 故障排查与解决方案
- 项目优化与扩展
- 总结与技术展望
1. 项目概述
1.1 项目背景与意义
在电机研发、生产和质检过程中,精确测量电机运行参数是保证产品质量的关键环节。传统测试设备往往存在体积庞大、操作复杂、成本高昂等问题,难以满足生产线快速检测和实验室精密测试的双重需求。
随着嵌入式技术的发展,基于微控制器的小型化测试工装逐渐成为趋势。本项目旨在开发一款高精度、低成本、易操作的电机测试工装,能够实时测量有刷电机的母线电压、工作电流、转速、转动圈数等关键参数,并支持参数校准功能,为电机性能评估提供可靠数据支持。
1.2 项目目标与技术指标
本项目的核心目标是实现对有刷电机运行参数的高精度测量与直观显示,具体技术指标如下:
| 技术参数 | 测量范围 | 精度要求 | 刷新频率 |
|---|---|---|---|
| 母线电压 | 0-30V | ±0.05V | ≥10Hz |
| 工作电流 | 0-5A | ±0.01A | ≥10Hz |
| 电机转速 | 0-10000RPM | ±1RPM | ≥10Hz |
| 转动圈数 | 0-99999 圈 | ±1 圈 | 实时更新 |
| 校准总圈数 | 0-99999 圈 | ±1 圈 | 实时更新 |
| 系统响应时间 | ≤100ms | - | - |
| 显示分辨率 | 128×96 像素 | - | ≥5Hz |
1.3 核心技术选型
本项目采用的核心技术组件如下表所示:
| 技术领域 | 具体选型 | 关键特性 |
|---|---|---|
| 微控制器 | GD32F305RCT6 | 32 位 ARM Cortex-M4 内核,120MHz 主频,256KB Flash,48KB RAM |
| 操作系统 | RT-Thread 4.0.3 | 开源实时操作系统,微内核设计,丰富的设备驱动支持 |
| 图形库 | U8G2 | 支持多种 OLED/LCD 控制器,占用资源少,字体丰富 |
| 显示设备 | 128×96 OLED 屏 | SSD1327 驱动芯片,SPI 接口,16 级灰度,低功耗 |
| 高精度 ADC | ADS1115IDGSR | 16 位分辨率,I2C 接口,4 通道,可编程增益 |
| 电流传感器 | INA138NA/3K | 高精度电流分流监控器,共模电压范围宽 |
| 采样电阻 | 200mΩ | 高精度,低温度系数 |
| 匹配电阻 | 4.99KΩ | 高精度,确保 1:1 电压还原 |
| 开发工具 | Keil MDK 5.32 | 集成开发环境,支持调试与仿真 |
| 调试工具 | J-Link V9 | 支持在线调试与程序下载 |
1.4 项目创新点
- 高精度测量方案:采用 16 位 ADS1115 ADC 结合 INA138 电流传感器,实现电压电流的高精度测量
- 模块化设计:硬件与软件均采用模块化设计,便于维护与扩展
- 实时操作系统:基于 RT-Thread 的多任务调度,确保各参数测量的实时性
- 直观显示界面:数码管风格的显示界面,参数清晰易读
- 校准功能:支持参数校准,提高系统适应性和测量精度
2. 硬件系统设计
2.1 系统总体硬件架构
本系统硬件架构采用分层设计,从上到下分为:
- 核心控制层:以 GD32F305RCT6 为核心,负责系统控制与数据处理
- 数据采集层:包括 ADS1115 ADC、INA138 电流传感器等,负责电压电流采集
- 信号输入层:包括电机磁编码器接口,负责转速和圈数信号采集
- 显示层:SSD1327 OLED 显示屏,负责参数显示
- 人机交互层:包括按键,负责用户操作输入
- 电源层:负责为系统各模块提供稳定电源
硬件架构框图如下:
+------------------------+
| 电源模块(5V/3.3V) |
+------------------------+
|
+------------------------+
| GD32F305RCT6核心 |<----+
| (Cortex-M4, 120MHz) | |
+------------------------+ |
| | | |
+----------+ +--------+ | +------------+
| ADS1115 | | OLED屏 | | 磁编码器接口 |
| (I2C) | |(SPI) | | |
+----------+ +--------+ | +------------+
| |
+----------+ +--------+ |
| INA138 | |按键 | |
| 电流检测 | | | |
+----------+ +--------+ |
|
+------------------------+|
| 电机驱动与接口 |+
+------------------------+
2.2 核心控制器 GD32F305RCT6
GD32F305RCT6 是兆易创新推出的高性能 32 位微控制器,基于 ARM Cortex-M4 内核,具有卓越的运算性能和丰富的外设资源,非常适合本项目的需求。
其主要特性如下表所示:
| 特性 | 参数 |
|---|---|
| 内核 | ARM Cortex-M4,带 FPU 浮点运算单元 |
| 主频 | 最高 120MHz |
| 闪存 | 256KB,支持 ISP 和 IAP |
| SRAM | 48KB |
| 工作电压 | 2.6V-3.6V |
| 温度范围 | -40℃ to +85℃ |
| 封装 | LQFP64 (7mm x 7mm) |
| 定时器 | 11 个定时器,包括 2 个高级定时器 |
| 通信接口 | 5 个 UART,3 个 I2C,5 个 SPI |
| 通用 IO | 51 个 GPIO,支持多种复用功能 |
| 模数转换器 | 2 个 12 位 ADC,16 个通道,采样率可达 1Msps |
| 看门狗 | 独立看门狗和窗口看门狗 |
| DMA | 12 通道 DMA 控制器 |
选择该芯片的主要原因:
- 高性能 Cortex-M4 内核,足以应对多任务实时处理需求
- 丰富的外设接口,包括多个 I2C 和 SPI 接口,方便连接 ADS1115 和 OLED
- 足够的存储空间,可容纳 RT-Thread 操作系统和应用程序
- 良好的性价比和稳定的供货渠道
2.3 高精度数据采集模块设计
2.3.1 ADS1115 ADC 芯片
ADS1115 是一款 16 位高精度模数转换器,采用 I2C 接口,支持 4 路模拟输入,具有可编程增益放大器 (PGA),非常适合本项目的电压和电流采集需求。
其主要特性如下:
| 特性 | 参数 |
|---|---|
| 分辨率 | 16 位 |
| 最大采样率 | 860SPS |
| 输入通道 | 4 路单端或 2 路差分 |
| 可编程增益 | ±256mV 至 ±6.144V |
| 接口 | I2C (支持 100kHz 和 400kHz) |
| 工作电压 | 2.0V 至 5.5V |
| 功耗 | 正常模式:160μA,掉电模式:0.1μA |
| 转换时间 | 最快 1.17ms |
在本项目中,ADS1115 配置为:
- I2C 通信速率:400kHz
- 采样率:128SPS(兼顾精度和速度)
- 通道 0:测量电机母线电压
- 通道 1:测量电机工作电流(来自 INA138 输出)
- 增益设置:针对电压通道设置为 ±4.096V,电流通道设置为 ±2.048V
2.3.2 INA138 电流传感器电路
INA138 是一款高精度电流分流监控器,能够精确测量通过分流电阻的电流,并将其转换为电压信号输出。
其主要特性如下:
| 特性 | 参数 |
|---|---|
| 增益 | 50V/V (固定) |
| 共模电压范围 | -0.3V 至 + 60V |
| 带宽 | 500kHz |
| 失调电压 | 最大 50μV |
| 失调漂移 | 最大 0.5μV/℃ |
| 工作电压 | 2.7V 至 36V |
电流测量电路设计:
- 采样电阻:200mΩ,0.1% 精度,低温度系数
- INA138 增益:50V/V
- 后端匹配电路:4.99KΩ 高精度电阻,实现 1:1 电压还原
电流测量原理:
- 电机工作电流通过 200mΩ 采样电阻,产生电压降 V = I × R
- INA138 将该电压放大 50 倍:Vout1 = 50 × I × 0.2 = 10 × I
- 通过匹配电阻网络进行 1:10 衰减,得到 Vout2 = I × 1.0V/A
- 因此,最终输出电压与电流成 1:1 关系(1V 对应 1A)
这种设计使得电流测量电路输出电压直接反映电流大小,简化了软件计算,同时保证了测量精度。
2.3.3 电压测量电路
电机母线电压测量采用电阻分压网络,将最高 30V 的电机电压降至 ADS1115 的测量范围内。
分压电路设计:
- 上拉电阻:100KΩ,0.1% 精度
- 下拉电阻:10KΩ,0.1% 精度
- 分压比:1:11(30V 输入对应约 2.73V 输出)
这种设计可以将 0-30V 的电机电压转换为 0-2.73V 的电压信号,适合 ADS1115 在 ±4.096V 增益设置下测量。
2.3.4 数据采集模块原理图
数据采集模块的核心电路连接如下:
+----------------+ +----------------+
| 电机母线 | | 电机电流回路 |
| (0-30V) | | |
+-------+--------+ +-------+--------+
| |
| |
+-------v--------+ +-------v--------+
| 分压电阻网络 | | 200mΩ采样 |
| (100K + 10K) | | 电阻 |
+-------+--------+ +-------+--------+
| |
| |
+-------v--------+ +-------v--------+
| ADS1115 | | INA138 |
| CH0 (电压) |<-----| 输出 |
+----------------+ +-------+--------+
|
+-------v--------+
| 匹配电阻网络 |
| (4.99K) |
+-------+--------+
|
+-----> ADS1115 CH1 (电流)
2.4 OLED 显示模块
本项目选用 128×96 分辨率的 OLED 显示屏,采用 SSD1327 驱动芯片,通过 SPI 接口与 GD32F305 通信。
SSD1327 的主要特性:
| 特性 | 参数 |
|---|---|
| 显示分辨率 | 128×96 像素 |
| 灰度级别 | 16 级 |
| 接口方式 | SPI 和 I2C |
| 工作电压 | 1.65V-3.3V |
| 显示颜色 | 单色(通常为白色或蓝色) |
| 视角 | 接近 180 度 |
| 功耗 | 工作模式约 200μA,休眠模式 < 1μA |
OLED 与 GD32F305 的连接如下表所示:
| GD32F305 引脚 | OLED 引脚 | 功能描述 |
|---|---|---|
| PA5 (SPI1_SCK) | SCK | SPI 时钟线 |
| PA7 (SPI1_MOSI) | SDA | SPI 数据线 |
| PA4 | CS | 片选信号(低电平有效) |
| PA2 | DC | 数据 / 命令选择(高电平数据,低电平命令) |
| PA3 | RST | 复位信号(低电平复位) |
| 3.3V | VCC | 电源 |
| GND | GND | 地 |
2.5 磁编码器接口电路
有刷电机自带的磁编码器通常输出 A、B 两相正交脉冲信号,通过检测脉冲数和相位可以计算电机转速和转动方向。
磁编码器接口电路设计:
- 采用施密特触发器进行信号整形(74HC14)
- 加入 10KΩ 上拉电阻
- 信号通过 GPIO 连接到 GD32F305 的定时器输入捕获引脚
磁编码器与 GD32F305 的连接如下表所示:
| GD32F305 引脚 | 编码器信号 | 功能描述 |
|---|---|---|
| PB6 (TIM4_CH1) | A 相 | 编码器 A 相脉冲 |
| PB7 (TIM4_CH2) | B 相 | 编码器 B 相脉冲 |
| 3.3V | VCC | 编码器电源 |
| GND | GND | 地 |
2.6 电源模块设计
系统电源模块需要为各个组件提供稳定的工作电压:
| 模块 | 工作电压 | 最大电流 | 电源来源 |
|---|---|---|---|
| GD32F305 | 3.3V | 50mA | 线性稳压器 |
| ADS1115 | 3.3V | 1mA | 线性稳压器 |
| INA138 | 5V | 2mA | 开关稳压器 |
| OLED 显示屏 | 3.3V | 200mA | 线性稳压器 |
| 磁编码器 | 3.3V/5V | 10mA | 可切换电源 |
| 按键 | 3.3V | <1mA | 线性稳压器 |
电源模块采用 5V 外部供电,通过以下稳压器产生所需电压:
- 5V 转 3.3V:采用 AMS1117-3.3,提供最大 800mA 电流
- 外部 5V 直接供给 INA138 和可选的 5V 编码器
2.7 按键与人机交互
系统设计一个功能按键,用于进入 / 退出校准模式和确认操作:
| GD32F305 引脚 | 功能 | 描述 |
|---|---|---|
| PC13 | 校准按键 | 上拉输入,按下时为低电平 |
按键电路采用 10KΩ 上拉电阻,确保在未按下时为稳定的高电平。
3. 软件系统架构
3.1 系统总体架构
本系统软件采用分层架构设计,从上到下分为:
- 应用层:实现电机测试的业务逻辑,包括参数采集、数据处理、显示控制等
- 中间件层:包括 U8G2 图形库、数据滤波算法等
- 驱动层:包括 ADS1115 驱动、OLED 驱动、编码器驱动、按键驱动等
- RT-Thread 内核层:提供任务调度、内存管理、设备管理等核心功能
系统架构图如下:
+-----------------------------------------+
| 应用层 |
| +------------+ +------------+ |
| | 参数采集任务 | | 显示更新任务 | |
| +------------+ +------------+ |
| +------------+ +------------+ |
| | 校准管理任务 | | 按键处理任务 | |
| +------------+ +------------+ |
+-----------------------------------------+
| 中间件层 |
| +------------+ +------------+ |
| | U8G2库 | | 数据滤波算法 | |
| +------------+ +------------+ |
+-----------------------------------------+
| 驱动层 |
| +------------+ +------------+ |
| | ADS1115驱动 | | OLED驱动 | |
| +------------+ +------------+ |
| +------------+ +------------+ |
| | 编码器驱动 | | 按键驱动 | |
| +------------+ +------------+ |
+-----------------------------------------+
| RT-Thread内核层 |
| +------------+ +------------+ |
| | 任务调度 | | 内存管理 | |
| +------------+ +------------+ |
| +------------+ +------------+ |
| | 设备管理 | | 中断管理 | |
| +------------+ +------------+ |
+-----------------------------------------+
3.2 系统任务设计
基于 RT-Thread 的多任务特性,系统划分为以下主要任务:
| 任务名称 | 优先级 | 栈大小 | 周期 | 功能描述 |
|---|---|---|---|---|
| 参数采集任务 | 8 (高) | 1024 字节 | 100ms | 采集电压、电流、转速、圈数等参数 |
| 显示更新任务 | 6 (中) | 2048 字节 | 200ms | 更新 OLED 屏幕显示内容 |
| 校准管理任务 | 7 (中高) | 1024 字节 | 50ms | 处理校准逻辑,更新校准参数 |
| 按键处理任务 | 5 (中低) | 512 字节 | 50ms | 检测按键状态,处理用户输入 |
| 数据存储任务 | 4 (低) | 1024 字节 | 1000ms | 存储关键数据和校准参数 |
任务间通信采用 RT-Thread 提供的 IPC 机制:
- 信号量:用于同步任务和中断服务程序
- 消息队列:用于任务间传递数据
- 共享内存:用于存储全局参数
3.3 数据流程设计
系统数据流程如下:
- 参数采集任务通过 ADS1115 读取电压和电流数据,通过编码器接口读取转速和圈数
- 采集的数据经过滤波处理后存入共享内存
- 显示更新任务从共享内存读取数据,格式化后通过 U8G2 库显示在 OLED 上
- 按键处理任务检测到按键操作后,通过消息队列通知校准管理任务
- 校准管理任务根据校准逻辑更新校准参数,并存储到 Flash
数据流程图如下:
+----------------+ +----------------+
| 硬件设备 |----->| 参数采集任务 |
| (ADC/编码器) | | |
+----------------+ +--------+-------+
|
v
+----------------+ +--------+-------+
| 显示更新任务 |<-----| 共享内存 |
| | | |
+--------+-------+ +--------+-------+
| |
v |
+--------+-------+ +--------+-------+
| OLED显示屏 | | 校准管理任务 |<----+
| | | | |
+----------------+ +--------+-------+ |
| |
v |
+--------+-------+ |
| 数据存储任务 | |
| | |
+----------------+ |
|
+----------------+ |
| 按键处理任务 |--+
| |
+----------------+
3.4 UML 用例图
系统主要用例如下:
-
正常测试用例
- 启动系统
- 实时显示电机参数
- 监测电机运行状态
-
校准用例
- 进入校准模式
- 执行电压校准
- 执行电流校准
- 执行转速校准
- 保存校准参数
- 退出校准模式
-
系统管理用例
- 查看设备 ID
- 恢复出厂设置
- 查看系统版本
UML 用例图表示如下:
+----------------+
| 操作员 |
+-------+--------+
|
|
+----------------+ +------v------+ +----------------+
| 电机参数显示 |<----| 正常测试 |---->| 电机运行监测 |
+----------------+ +-------------+ +----------------+
^
|
+-------+------+ +-------------+ +----------------+
| 校准结果显示|<----| 校准操作 |---->| 校准参数保存 |
+--------------+ +-------------+ +----------------+
^
|
+----------------+ +-------------+ |
| 设备ID显示 |<----| 系统管理 |----------+
+----------------+ +-------------+ +----------------+
| 恢复出厂设置 |
+----------------+
3.5 UML 类图
系统核心类设计如下:
- MotorTestSystem 类:系统主类,负责协调各模块工作
- DataCollector 类:数据采集类,负责从硬件设备采集数据
- ADS1115 类:ADS1115 驱动类,负责与 ADC 芯片通信
- Encoder 类:编码器驱动类,负责读取电机转速和圈数
- DisplayManager 类:显示管理类,负责 OLED 显示
- U8G2Wrapper 类:U8G2 封装类,提供高层绘图接口
- CalibrationManager 类:校准管理类,负责校准逻辑
- KeyHandler 类:按键处理类,负责按键输入处理
- DataStorage 类:数据存储类,负责数据持久化
UML 类图表示如下:
+----------------+ +----------------+
| MotorTestSystem| | DataStorage |
+----------------+ +----------------+
| -collector:DataCollector| +saveParams() |
| -display:DisplayManager| +loadParams() |
| -calib:CalibrationManager| |
| -keyHandler:KeyHandler| |
+----------------+ +----------------+
| +start() | ^
| +stop() | |
+-------+--------+ |
| |
| |
+-------v--------+ +-------+--------+
| DataCollector | |CalibrationManager|
+----------------+ +----------------+
| -ads:ADS1115 | | -params:CalibParams|
| -encoder:Encoder| | -storage:DataStorage|
+----------------+ +----------------+
| +collectVoltage()| | +enterCalibMode()|
| +collectCurrent()| | +calibrateVoltage()|
| +collectSpeed() | | +calibrateCurrent()|
| +collectCount() | | +exitCalibMode()|
+----------------+ +----------------+
^ ^
| |
+-------+--------+ +-------+--------+
| ADS1115 | | KeyHandler |
+----------------+ +----------------+
| -i2cDevice | | -keyPin |
+----------------+ +----------------+
| +init() | | +init() |
| +readChannel() | | +getKeyState() |
+----------------+ +----------------+
+----------------+ +----------------+
| Encoder | | DisplayManager |
+----------------+ +----------------+
| -timerDevice | | -u8g2:U8G2Wrapper|
| -count | | -params:MotorParams|
| -speed | +----------------+
+----------------+ | +init() |
| +init() | | +updateDisplay()|
| +getCount() | | +showCalibScreen()|
| +getSpeed() | +----------------+
+----------------+ ^
|
+-------+--------+
| U8G2Wrapper |
+----------------+
| -u8g2:u8g2_t |
+----------------+
| +init() |
| +drawDigit() |
| +drawText() |
| +update() |
+----------------+
3.6 UML 状态图
系统主要有两种状态:正常测试状态和校准状态。状态转换如下:
- 系统上电后进入正常测试状态
- 在正常测试状态下,按下按键并保持 2 秒进入校准状态
- 在校准状态下,短按按键切换不同的校准项目
- 在校准状态下,长按按键 2 秒退出校准状态,返回正常测试状态
UML 状态图表示如下:
+---------------------+ +---------------------+
| 正常测试状态 |<-------| 校准状态 |
+---------------------+ +---------------------+
| - 显示电机参数 | | - 显示校准界面 |
| - 实时更新数据 | | - 支持多种校准项目 |
+---------------------+ +---------------------+
| |
| 长按按键2秒 | 长按按键2秒
v v
+---------------------+ +---------------------+
| 进入校准状态 |------->| 退出校准状态 |
| 准备流程 | | 保存参数流程 |
+---------------------+ +---------------------+
4. RT-Thread 操作系统移植
4.1 RT-Thread 简介
RT-Thread 是一款来自中国的开源实时操作系统,具有以下特点:
| 特点 | 描述 |
|---|---|
| 微内核设计 | 内核体积小,可裁剪,最小内核仅几 KB |
| 实时性强 | 支持优先级调度,调度延迟可预测 |
| 丰富的组件 | 提供文件系统、网络、GUI 等组件 |
| 设备驱动框架 | 统一的设备驱动模型,易于扩展 |
| 开发工具支持 | 支持 Keil、IAR、GCC 等主流开发工具 |
| 文档丰富 | 完善的开发文档和教程 |
| 活跃社区 | 有活跃的开发者社区提供支持 |
RT-Thread 采用模块化设计,由内核层、组件层和应用层构成,非常适合本项目的开发需求。
4.2 移植环境准备
移植 RT-Thread 到 GD32F305RCT6 需要以下工具和资源:
| 软件 / 工具 | 版本 | 用途 |
|---|---|---|
| Keil MDK | 5.32 | 集成开发环境 |
| RT-Thread | 4.0.3 | 操作系统源码 |
| GD32F30x 固件库 | 2.2.0 | 芯片外设驱动库 |
| RT-Thread GD32 BSP | 最新版 | 板级支持包 |
| J-Link V9 | - | 调试器 |
| 串口调试助手 | 任意版本 | 调试信息输出 |
4.3 移植步骤详解
4.3.1 获取 RT-Thread 源码
通过 Git 获取 RT-Thread 源码:
bash
git clone https://github.com/RT-Thread/rt-thread.git
cd rt-thread
git checkout v4.0.3
4.3.2 获取 GD32F30x BSP
RT-Thread 提供了 GD32 系列的板级支持包 (BSP),可以通过以下方式获取:
bash
git clone https://github.com/RT-Thread/rt-thread-bsp-gd32.git
在 BSP 中找到 GD32F305RCT6 对应的开发板配置,复制到 RT-Thread 的 bsp 目录下。
4.3.3 配置工程
-
打开 Keil MDK,加载 BSP 中的工程文件(project.uvprojx)
-
配置 RT-Thread 内核功能:
- 打开 RT-Thread 配置工具:
menuconfig - 配置内核选项:
- 系统时钟:120MHz
- 任务切换方式: PendSV
- 启用软件定时器
- 启用动态内存管理
- 配置外设驱动:
- 启用 SPI 设备驱动
- 启用 I2C 设备驱动
- 启用定时器设备驱动
- 启用 UART 设备驱动
- 打开 RT-Thread 配置工具:
-
保存配置并生成工程:
bash
scons --target=mdk5
4.3.4 修改启动文件
GD32F305 的启动文件需要适配 RT-Thread 的启动流程:
-
修改向量表,将 PendSV_Handler 和 SysTick_Handler 指向 RT-Thread 的实现
-
确保启动文件正确初始化系统时钟和外设
4.3.5 实现板级初始化
在 board.c 文件中实现板级初始化函数:
板级初始化代码
V1
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2021-05-20 Administrator first version
*/
#include <rthw.h>
#include <rtthread.h>
#include "gd32f30x.h"
#define SYSTEM_CLOCK_FREQ 120000000UL
/**
* @brief 系统时钟配置
*/
static void system_clock_config(void)
{
/* 使能外部高速时钟HXTAL */
rcu_osci_on(RCU_HXTAL);
/* 等待HXTAL稳定 */
while(rcu_osci_stab_wait(RCU_HXTAL) == ERROR);
/* 配置系统时钟源为HXTAL,PLL倍频系数为12 (8MHz * 12 = 96MHz) */
rcu_pll_config(RCU_PLLSRC_HXTAL, RCU_PLL_MUL15); // 8MHz * 15 = 120MHz
/* 使能PLL */
rcu_osci_on(RCU_PLL);
/* 等待PLL稳定 */
while(rcu_osci_stab_wait(RCU_PLL) == ERROR);
/* 配置AHB时钟 (HCLK = SYSCLK) */
rcu_ahb_clock_config(RCU_AHB_CKSYS_DIV1);
/* 配置APB1时钟 (PCLK1 = HCLK / 2) */
rcu_apb1_clock_config(RCU_APB1_CKAHB_DIV2);
/* 配置APB2时钟 (PCLK2 = HCLK) */
rcu_apb2_clock_config(RCU_APB2_CKAHB_DIV1);
/* 选择PLL作为系统时钟源 */
rcu_system_clock_source_config(RCU_CKSYSSRC_PLL);
/* 更新系统时钟频率变量 */
SystemCoreClockUpdate();
}
/**
* @brief 初始化GPIO引脚
*/
static void gpio_config(void)
{
/* 使能GPIO时钟 */
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOD);
/* 配置调试串口引脚 (PA9: USART0_TX, PA10: USART0_RX) */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
/* 配置OLED SPI引脚 (PA5: SCK, PA7: MOSI) */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7);
/* 配置OLED控制引脚 (PA4: CS, PA2: DC, PA3: RST) */
gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4);
/* 配置按键引脚 (PC13: KEY) */
gpio_init(GPIOC, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_13);
/* 配置编码器引脚 (PB6: TIM4_CH1, PB7: TIM4_CH2) */
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);
}
/**
* @brief 初始化外设时钟
*/
static void peripheral_clock_config(void)
{
/* 使能外设时钟 */
rcu_periph_clock_enable(RCU_USART0); // 调试串口
rcu_periph_clock_enable(RCU_SPI1); // OLED SPI
rcu_periph_clock_enable(RCU_I2C0); // ADS1115 I2C
rcu_periph_clock_enable(RCU_TIM4); // 编码器定时器
rcu_periph_clock_enable(RCU_TIMER2); // 系统定时器
}
/**
* @brief 板级初始化函数
*/
void rt_hw_board_init()
{
/* 关闭全局中断 */
rt_hw_interrupt_disable();
/* 配置系统时钟 */
system_clock_config();
/* 初始化GPIO */
gpio_config();
/* 初始化外设时钟 */
peripheral_clock_config();
/* 初始化调试串口 */
rt_hw_usart_init();
/* 初始化系统定时器 */
rt_hw_timer_init();
/* 初始化内存堆 */
rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
/* 打开全局中断 */
rt_hw_interrupt_enable();
}
#ifdef RT_USING_CONSOLE
/**
* @brief 控制台输出函数
*/
void rt_hw_console_output(const char *str)
{
/* 发送字符串到USART0 */
rt_size_t i = 0, size = 0;
char *ptr = (char *)str;
while (ptr[i] != '\0')
{
size++;
i++;
}
usart_data_transmit(USART0, (uint8_t *)str, size);
}
#endif
/**
* @brief 系统滴答定时器配置
*/
void SysTick_Handler(void)
{
/* 调用RT-Thread的系统滴答处理函数 */
rt_tick_increase();
}
4.3.6 配置设备驱动
配置 I2C 和 SPI 设备驱动,使其能被 RT-Thread 的设备框架管理:
设备驱动配置代码drv_i2c.c
V1
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2021-05-20 Administrator first version
*/
#include <rtthread.h>
#include <rtdevice.h>
#include "gd32f30x.h"
#include "drv_i2c.h"
#define I2C0_SCL_PIN GET_PIN(B, 8)
#define I2C0_SDA_PIN GET_PIN(B, 9)
static struct rt_i2c_bus_device i2c0_bus;
/**
* @brief 初始化I2C0
*/
static int i2c0_init(void)
{
/* 配置I2C0引脚 */
gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_8 | GPIO_PIN_9);
/* 复位I2C0 */
i2c_deinit(I2C0);
/* 配置I2C0时钟 */
i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
/* 使能I2C0应答 */
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
/* 使能I2C0 */
i2c_enable(I2C0);
return 0;
}
/**
* @brief I2C0传输函数
*/
static rt_size_t i2c0_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
rt_uint32_t i;
rt_err_t result = RT_EOK;
for (i = 0; i < num; i++)
{
if (msgs[i].flags & RT_I2C_WR)
{
/* 写操作 */
i2c_start_on_bus(I2C0);
while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
/* 发送从机地址 + 写 */
i2c_master_addressing(I2C0, msgs[i].addr, I2C_TRANSMITTER);
while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
/* 发送数据 */
rt_size_t j;
for (j = 0; j < msgs[i].len; j++)
{
i2c_data_transmit(I2C0, msgs[i].buf[j]);
while(!i2c_flag_get(I2C0, I2C_FLAG_TBE));
}
while(!i2c_flag_get(I2C0, I2C_FLAG_BTC));
}
else
{
/* 读操作 */
i2c_start_on_bus(I2C0);
while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
/* 发送从机地址 + 读 */
i2c_master_addressing(I2C0, msgs[i].addr, I2C_RECEIVER);
while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
/* 接收数据 */
rt_size_t j;
for (j = 0; j < msgs[i].len; j++)
{
/* 最后一个字节不发送ACK */
if (j == msgs[i].len - 1)
{
i2c_ack_config(I2C0, I2C_ACK_DISABLE);
}
while(!i2c_flag_get(I2C0, I2C_FLAG_RBNE));
msgs[i].buf[j] = i2c_data_receive(I2C0);
}
}
}
/* 发送停止信号 */
i2c_stop_on_bus(I2C0);
while(I2C_CTL0(I2C0) & I2C_CTL0_STOP);
/* 恢复ACK使能 */
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
return num;
}
/**
* @brief 注册I2C0设备
*/
int rt_hw_i2c_init(void)
{
i2c0_bus.i2c_ops->transfer = i2c0_transfer;
i2c0_init();
return rt_i2c_bus_device_register(&i2c0_bus, "i2c0");
}
INIT_BOARD_EXPORT(rt_hw_i2c_init);
drv_spi.c
SPI设备驱动配置代码
V1
/*
* Copyright (c) 2006-2021, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2021-05-20 Administrator first version
*/
#include <rtthread.h>
#include <rtdevice.h>
#include "gd32f30x.h"
#include "drv_spi.h"
#define SPI1_SCK_PIN GET_PIN(A, 5)
#define SPI1_MOSI_PIN GET_PIN(A, 7)
static struct rt_spi_bus_device spi1_bus;
/**
* @brief 初始化SPI1
*/
static int spi1_init(void)
{
/* 配置SPI1引脚 */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7);
/* 复位SPI1 */
spi_i2s_deinit(SPI1);
/* 配置SPI1为主机模式 */
spi_parameter_struct spi_init_struct;
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
spi_init_struct.device_mode = SPI_MASTER;
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.prescale = SPI_PRESCALE_DIV2; // 60MHz (120/2)
spi_init_struct.endian = SPI_ENDIAN_MSB;
spi_init(SPI1, &spi_init_struct);
/* 使能SPI1 */
spi_enable(SPI1);
return 0;
}
/**
* @brief SPI1传输函数
*/
static rt_size_t spi1_transfer(struct rt_spi_device *device,
const void *send_buf,
void *recv_buf,
rt_size_t length)
{
rt_size_t i;
const uint8_t *tx_buf = send_buf;
uint8_t *rx_buf = recv_buf;
for (i = 0; i < length; i++)
{
/* 发送数据 */
while (RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI1, tx_buf ? tx_buf[i] : 0x00);
/* 接收数据 */
while (RESET == spi_i2s_flag_get(SPI1, SPI_FLAG_RBNE));
if (rx_buf)
{
rx_buf[i] = spi_i2s_data_receive(SPI1);
}
else
{
/* 丢弃接收的数据 */
spi_i2s_data_receive(SPI1);
}
}
return length;
}
/**
* @brief 注册SPI1设备
*/
int rt_hw_spi_init(void)
{
spi1_bus.bus_ops.transfer = spi1_transfer;
spi1_init();
return rt_spi_bus_register(&spi1_bus, "spi1");
}
INIT_BOARD_EXPORT(rt_hw_spi_init);
4.3.7 测试移植结果
编写简单的测试程序验证 RT-Thread 是否移植成功:
RT-Thread移植测试代码
V1
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
static rt_thread_t tid1 = RT_NULL;
/**
* @brief 测试线程入口函数
*/
static void thread_entry(void *parameter)
{
rt_uint32_t count = 0;
while (1)
{
rt_kprintf("Hello RT-Thread! Count: %d\n", count++);
rt_thread_mdelay(1000);
}
}
/**
* @brief 主函数
*/
int main(void)
{
/* 创建测试线程 */
tid1 = rt_thread_create("test",
thread_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
/* 启动线程 */
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
return 0;
}
编译程序并下载到开发板,如果串口能周期性输出 "Hello RT-Thread! Count: x",则说明 RT-Thread 移植成功。
4.4 移植常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 系统无法启动 | 时钟配置错误 | 检查系统时钟配置,确保 PLL 和分频系数正确 |
| 串口无输出 | 串口引脚配置错误 | 检查串口引脚复用配置,确保波特率正确 |
| 线程无法调度 | 中断向量表错误 | 确保 PendSV 和 SysTick 中断指向 RT-Thread 实现 |
| 内存分配失败 | 堆内存配置错误 | 检查 HEAP_BEGIN 和 HEAP_END 定义,确保内存大小正确 |
| 外设无法工作 | 外设时钟未使能 | 在板级初始化中确保使能所需外设的时钟 |
5. U8G2 图形库移植
5.1 U8G2 库简介
U8G2 是一款开源的单色图形库,专为嵌入式系统设计,支持多种 OLED 和 LCD 控制器。其主要特点如下:
| 特点 | 描述 |
|---|---|
| 支持多种控制器 | 支持 SSD1306、SSD1327、SH1106 等多种 OLED 控制器 |
| 多种接口支持 | 支持 SPI、I2C、并行接口等 |
| 丰富的字体 | 内置多种大小和风格的字体 |
| 绘图功能 | 支持点、线、矩形、圆等基本图形绘制 |
| 低资源占用 | 内存占用小,适合资源有限的嵌入式系统 |
| 可裁剪性 | 可根据需求裁剪不需要的功能 |
在本项目中,我们将 U8G2 库移植到 GD32F305 上,用于控制 SSD1327 OLED 显示屏。
5.2 移植准备工作
移植 U8G2 库需要以下文件和工具:
| 文件 / 工具 | 用途 |
|---|---|
| u8g2.h | U8G2 库头文件 |
| u8g2.c | U8G2 库核心实现 |
| u8x8.h | 底层驱动接口定义 |
| u8x8.c | 底层驱动实现 |
| u8x8_d_ssd1327_128x96.h | SSD1327 驱动 |
| u8x8_gpio.h | GPIO 接口定义 |
| u8x8_byte.h | 数据传输接口定义 |
从 U8G2 官方仓库获取最新版本的库文件:
bash
git clone https://github.com/olikraus/u8g2.git
5.3 移植步骤详解
5.3.1 添加 U8G2 文件到工程
将以下文件添加到 RT-Thread 工程中:
- u8g2.h, u8g2.c
- u8x8.h, u8x8.c
- u8x8_d_ssd1327_128x96.h
- u8x8_gpio.h, u8x8_gpio.c
- u8x8_byte.h, u8x8_byte.c
配置头文件路径,确保编译器能找到 U8G2 的头文件。
5.3.2 实现硬件接口函数
U8G2 需要底层硬件接口函数来操作 GPIO 和 SPI,我们需要实现这些函数:
U8G2硬件接口实现
V1
#include "u8g2.h"
#include "rtthread.h"
#include "rtdevice.h"
#include "drv_gpio.h"
// OLED硬件引脚定义
#define OLED_CS_PIN GET_PIN(A, 4)
#define OLED_DC_PIN GET_PIN(A, 2)
#define OLED_RST_PIN GET_PIN(A, 3)
// SPI设备名称
#define OLED_SPI_DEVICE "spi1"
// SPI设备句柄
static rt_device_t spi_dev = RT_NULL;
/**
* @brief 初始化OLED控制引脚
*/
static void oled_gpio_init(void)
{
// 配置CS、DC、RST引脚为输出模式
rt_pin_mode(OLED_CS_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(OLED_DC_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(OLED_RST_PIN, PIN_MODE_OUTPUT);
// 初始化引脚状态
rt_pin_write(OLED_CS_PIN, PIN_HIGH); // 初始不选中
rt_pin_write(OLED_DC_PIN, PIN_LOW); // 默认为命令模式
rt_pin_write(OLED_RST_PIN, PIN_HIGH); // 不复位
}
/**
* @brief 初始化SPI设备
*/
static rt_err_t spi_device_init(void)
{
// 查找SPI设备
spi_dev = rt_device_find(OLED_SPI_DEVICE);
if (spi_dev == RT_NULL)
{
rt_kprintf("can't find spi device %s\n", OLED_SPI_DEVICE);
return RT_ERROR;
}
// 配置SPI设备
struct rt_spi_configuration cfg;
cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0; // 主机模式,模式0
cfg.max_hz = 10 * 1000 * 1000; // 10MHz
cfg.data_width = 8; // 8位数据
// 打开SPI设备并配置
if (rt_device_open(spi_dev, RT_DEVICE_OFLAG_RDWR) != RT_EOK)
{
rt_kprintf("open spi device %s failed\n", OLED_SPI_DEVICE);
return RT_ERROR;
}
if (rt_spi_configure(spi_dev, &cfg) != RT_EOK)
{
rt_kprintf("configure spi device %s failed\n", OLED_SPI_DEVICE);
return RT_ERROR;
}
return RT_EOK;
}
/**
* @brief 通过SPI发送数据
* @param data 要发送的数据
* @param len 数据长度
*/
static void spi_send_data(const uint8_t *data, uint32_t len)
{
if (spi_dev == RT_NULL || data == RT_NULL || len == 0)
return;
struct rt_spi_message msg;
msg.send_buf = data;
msg.recv_buf = RT_NULL;
msg.length = len;
msg.cs_take = 0; // 不自动控制CS
msg.cs_release = 0;
rt_spi_transfer_message(spi_dev, &msg);
}
/**
* @brief U8G2硬件初始化函数
*/
uint8_t u8g2_gpio_and_spi_init(u8g2_t *u8g2, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_GPIO_AND_SPI_INIT:
// 初始化GPIO
oled_gpio_init();
// 初始化SPI
spi_device_init();
// 复位OLED
rt_pin_write(OLED_RST_PIN, PIN_LOW);
rt_thread_mdelay(10);
rt_pin_write(OLED_RST_PIN, PIN_HIGH);
rt_thread_mdelay(10);
break;
case U8X8_MSG_SPI_TRANSMIT:
// 发送数据
rt_pin_write(OLED_CS_PIN, PIN_LOW); // 选中设备
spi_send_data((uint8_t *)arg_ptr, arg_int);
rt_pin_write(OLED_CS_PIN, PIN_HIGH); // 取消选中
break;
case U8X8_MSG_GPIO_DC:
// 设置DC引脚状态
rt_pin_write(OLED_DC_PIN, arg_int);
break;
case U8X8_MSG_DELAY_MILLI:
// 延时函数
rt_thread_mdelay(arg_int);
break;
default:
return 0; // 未处理的消息
}
return 1; // 消息处理成功
}
5.3.3 初始化 U8G2 库
编写 U8G2 初始化函数,配置 SSD1327 显示屏:
U8G2初始化代码
V1
#include "u8g2.h"
#include "u8g2_hw.h"
#include "rtthread.h"
// 全局U8G2对象
static u8g2_t u8g2;
/**
* @brief 初始化U8G2库和OLED显示屏
* @return 0: 成功, -1: 失败
*/
int u8g2_oled_init(void)
{
// 初始化SSD1327 128x96显示屏,使用SPI接口
u8g2_Setup_ssd1327_128x96_noname_f(&u8g2, U8G2_R0,
u8g2_gpio_and_spi_init,
u8g2_gpio_and_spi_init);
// 初始化显示屏
if (u8g2_InitDisplay(&u8g2) != U8X8_MSG_OK)
{
rt_kprintf("OLED display initialization failed\n");
return -1;
}
// 开启显示屏
u8g2_SetPowerSave(&u8g2, 0);
// 清屏
u8g2_ClearBuffer(&u8g2);
rt_kprintf("OLED display initialized successfully\n");
return 0;
}
/**
* @brief 获取U8G2对象指针
* @return U8G2对象指针
*/
u8g2_t *u8g2_get_instance(void)
{
return &u8g2;
}
// 注册初始化函数,在系统启动时自动初始化
INIT_APP_EXPORT(u8g2_oled_init);
5.3.4 实现数码管风格显示函数
为了实现类似数码管的显示效果,我们需要实现专门的数字显示函数:
数码管风格显示函数
V1
#include "u8g2.h"
#include "u8g2_init.h"
#include "rtthread.h"
#include <stdarg.h>
// 数码管风格字体(需要在U8G2中启用)
#define DIGIT_FONT_LARGE u8g2_font_7Segments_24x40_mn
#define DIGIT_FONT_MEDIUM u8g2_font_7Segments_16x24_mn
#define DIGIT_FONT_SMALL u8g2_font_7Segments_10x16_mn
/**
* @brief 绘制单个数码管风格数字
* @param x X坐标
* @param y Y坐标
* @param digit 要显示的数字(0-9)
* @param size 大小(0:小, 1:中, 2:大)
* @return 数字宽度
*/
static uint8_t draw_single_digit(uint8_t x, uint8_t y, uint8_t digit, uint8_t size)
{
u8g2_t *u8g2 = u8g2_get_instance();
uint8_t width = 0;
// 根据大小选择字体
switch(size)
{
case 0: // 小
u8g2_SetFont(u8g2, DIGIT_FONT_SMALL);
width = 10;
break;
case 1: // 中
u8g2_SetFont(u8g2, DIGIT_FONT_MEDIUM);
width = 16;
break;
case 2: // 大
u8g2_SetFont(u8g2, DIGIT_FONT_LARGE);
width = 24;
break;
default:
return 0;
}
// 绘制数字
if (digit < 10)
{
u8g2_DrawGlyph(u8g2, x, y, '0' + digit);
}
return width;
}
/**
* @brief 绘制带小数点的数字
* @param x X坐标
* @param y Y坐标
* @param number 要显示的数字
* @param integer_digits 整数位数
* @param decimal_digits 小数位数
* @param size 大小(0:小, 1:中, 2:大)
* @return 总宽度
*/
uint8_t draw_number_with_decimal(uint8_t x, uint8_t y, float number,
uint8_t integer_digits, uint8_t decimal_digits,
uint8_t size)
{
u8g2_t *u8g2 = u8g2_get_instance();
uint8_t current_x = x;
uint32_t integer_part, decimal_part;
uint8_t i, digit;
uint8_t digit_width;
// 确保位数合理
if (integer_digits == 0 && decimal_digits == 0)
return 0;
// 计算整数部分和小数部分
integer_part = (uint32_t)number;
decimal_part = (uint32_t)((number - integer_part) * rt_pow(10, decimal_digits) + 0.5);
// 获取数字宽度
switch(size)
{
case 0: // 小
digit_width = 10;
break;
case 1: // 中
digit_width = 16;
break;
case 2: // 大
digit_width = 24;
break;
default:
return 0;
}
// 绘制整数部分
for (i = integer_digits; i > 0; i--)
{
uint32_t divisor = rt_pow(10, i - 1);
digit = (integer_part / divisor) % 10;
current_x += draw_single_digit(current_x, y, digit, size);
}
// 绘制小数点
if (decimal_digits > 0)
{
switch(size)
{
case 0: // 小
u8g2_DrawGlyph(u8g2, current_x - 3, y + 5, '.');
break;
case 1: // 中
u8g2_DrawGlyph(u8g2, current_x - 5, y + 8, '.');
break;
case 2: // 大
u8g2_DrawGlyph(u8g2, current_x - 8, y + 12, '.');
break;
}
current_x += 2; // 小数点占用宽度
}
// 绘制小数部分
for (i = decimal_digits; i > 0; i--)
{
uint32_t divisor = rt_pow(10, i - 1);
digit = (decimal_part / divisor) % 10;
current_x += draw_single_digit(current_x, y, digit, size);
}
return current_x - x;
}
/**
* @brief 绘制整数
* @param x X坐标
* @param y Y坐标
* @param number 要显示的整数
* @param digits 显示位数
* @param size 大小(0:小, 1:中, 2:大)
* @return 总宽度
*/
uint8_t draw_integer(uint8_t x, uint8_t y, uint32_t number, uint8_t digits, uint8_t size)
{
uint8_t current_x = x;
uint8_t i, digit;
// 获取数字宽度
uint8_t digit_width;
switch(size)
{
case 0: // 小
digit_width = 10;
break;
case 1: // 中
digit_width = 16;
break;
case 2: // 大
digit_width = 24;
break;
default:
return 0;
}
// 绘制数字
for (i = digits; i > 0; i--)
{
uint32_t divisor = rt_pow(10, i - 1);
digit = (number / divisor) % 10;
current_x += draw_single_digit(current_x, y, digit, size);
}
return current_x - x;
}
5.3.5 移植验证
编写测试程序验证 U8G2 库移植是否成功:
U8G2移植测试代码
V1
#include "u8g2.h"
#include "u8g2_init.h"
#include "digit_display.h"
#include "rtthread.h"
#define TEST_THREAD_PRIORITY 6
#define TEST_THREAD_STACK_SIZE 1024
#define TEST_THREAD_TIMESLICE 5
static rt_thread_t test_thread = RT_NULL;
/**
* @brief U8G2测试线程
*/
static void u8g2_test_thread(void *parameter)
{
u8g2_t *u8g2 = u8g2_get_instance();
uint8_t count = 0;
while (1)
{
// 清屏
u8g2_ClearBuffer(u8g2);
// 显示标题
u8g2_SetFont(u8g2, u8g2_font_ncenB14_tr);
u8g2_DrawStr(u8g2, 0, 20, "OLED Test");
// 显示数码管风格数字
draw_number_with_decimal(0, 50, 12.34 + count * 0.1, 2, 2, 1);
// 显示整数
draw_integer(60, 50, 1234 + count, 4, 1);
// 刷新显示
u8g2_SendBuffer(u8g2);
count++;
if (count >= 100)
count = 0;
rt_thread_mdelay(100);
}
}
/**
* @brief 启动U8G2测试
*/
int u8g2_test_start(void)
{
// 检查OLED是否已经初始化
u8g2_t *u8g2 = u8g2_get_instance();
if (u8g2->u8x8.display_info == NULL)
{
rt_kprintf("OLED not initialized, test aborted\n");
return -1;
}
// 创建测试线程
test_thread = rt_thread_create("u8g2_test",
u8g2_test_thread,
RT_NULL,
TEST_THREAD_STACK_SIZE,
TEST_THREAD_PRIORITY,
TEST_THREAD_TIMESLICE);
if (test_thread != RT_NULL)
{
rt_thread_startup(test_thread);
rt_kprintf("U8G2 test started\n");
return 0;
}
else
{
rt_kprintf("Failed to create U8G2 test thread\n");
return -1;
}
}
MSH_CMD_EXPORT(u8g2_test_start, "Start U8G2 test");
编译并运行测试程序,如果 OLED 屏幕能显示标题和动态变化的数码管风格数字,则说明 U8G2 移植成功。
5.4 移植常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 屏幕无显示 | 复位引脚未正确操作 | 检查复位时序,确保复位信号正确 |
| 显示乱码 | SPI 通信错误 | 检查 SPI 时钟极性和相位设置,确保与 SSD1327 匹配 |
| 部分区域不显示 | 显示屏型号配置错误 | 确保使用正确的显示屏型号配置函数 (u8g2_Setup_ssd1327_128x96_noname_f) |
| 字体显示异常 | 字体未启用 | 在 U8G2 配置中启用所需字体 |
| 刷新速度慢 | SPI 速率太低 | 提高 SPI 通信速率,最高可达 10MHz |
6. 核心设备驱动开发
6.1 ADS1115 ADC 驱动
ADS1115 是一款 16 位高精度 ADC,通过 I2C 接口与微控制器通信。我们需要开发驱动程序来操作 ADS1115,实现电压和电流的采集。
6.1.1 ADS1115 寄存器结构
ADS1115 有 4 个主要寄存器:
| 寄存器 | 地址 | 功能 |
|---|---|---|
| 转换寄存器 | 0x00 | 存储最新的转换结果 |
| 配置寄存器 | 0x01 | 配置 ADS1115 的工作模式、增益、数据速率等 |
| 低阈值寄存器 | 0x02 | 比较器低阈值 |
| 高阈值寄存器 | 0x03 | 比较器高阈值 |
在本项目中,我们主要使用转换寄存器和配置寄存器。
配置寄存器是一个 16 位寄存器,各 bit 功能如下:
| Bit | 功能 | 描述 |
|---|---|---|
| 15 | OS | 操作状态位,1 = 开始单次转换 |
| 14-12 | MUX[2:0] | 输入多路选择器配置 |
| 11-9 | PGA[2:0] | 可编程增益放大器配置 |
| 8 | MODE | 工作模式,0 = 连续转换,1 = 单次转换 |
| 7-5 | DR[2:0] | 数据速率配置 |
| 4-2 | COMP_MODE | 比较器模式 |
| 1 | COMP_POL | 比较器输出极性 |
| 0 | COMP_LAT | 比较器锁存 |
6.1.2 ADS1115 驱动实现
ADS1115驱动代码
V1
#include "ads1115.h"
#include "rtthread.h"
#include "rtdevice.h"
// ADS1115默认I2C地址
#define ADS1115_ADDR_GND 0x48 // A0接地
#define ADS1115_ADDR_VCC 0x49 // A0接VCC
#define ADS1115_ADDR_SDA 0x4A // A0接SDA
#define ADS1115_ADDR_SCL 0x4B // A0接SCL
// 本项目使用A0接地的地址
#define ADS1115_ADDR ADS1115_ADDR_GND
// ADS1115寄存器地址
#define ADS1115_REG_CONV 0x00 // 转换寄存器
#define ADS1115_REG_CONFIG 0x01 // 配置寄存器
#define ADS1115_REG_LOWTH 0x02 // 低阈值寄存器
#define ADS1115_REG_HIGHTH 0x03 // 高阈值寄存器
// 全局ADS1115设备结构体
static struct ads1115_device ads1115_dev;
/**
* @brief 向ADS1115写入数据
* @param reg 寄存器地址
* @param data 要写入的数据
* @return 0: 成功, -1: 失败
*/
static int ads1115_write_reg(uint8_t reg, uint16_t data)
{
uint8_t buf[3];
if (ads1115_dev.i2c_bus == RT_NULL)
return -1;
// 构建写入数据:寄存器地址 + 数据(高8位 + 低8位)
buf[0] = reg;
buf[1] = (data >> 8) & 0xFF;
buf[2] = data & 0xFF;
// 通过I2C写入数据
if (rt_i2c_master_send(ads1115_dev.i2c_bus, ADS1115_ADDR, buf, 3) != 3)
{
rt_kprintf("ADS1115 write register failed\n");
return -1;
}
return 0;
}
/**
* @brief 从ADS1115读取数据
* @param reg 寄存器地址
* @param data 存储读取数据的指针
* @return 0: 成功, -1: 失败
*/
static int ads1115_read_reg(uint8_t reg, uint16_t *data)
{
uint8_t buf[2];
if (ads1115_dev.i2c_bus == RT_NULL || data == RT_NULL)
return -1;
// 发送要读取的寄存器地址
if (rt_i2c_master_send(ads1115_dev.i2c_bus, ADS1115_ADDR, ®, 1) != 1)
{
rt_kprintf("ADS1115 send register address failed\n");
return -1;
}
// 读取数据
if (rt_i2c_master_recv(ads1115_dev.i2c_bus, ADS1115_ADDR, buf, 2) != 2)
{
rt_kprintf("ADS1115 read register failed\n");
return -1;
}
// 转换为16位数据
*data = (buf[0] << 8) | buf[1];
return 0;
}
/**
* @brief 配置ADS1115
* @param channel 通道(0-3)
* @param gain 增益
* @param rate 数据速率
* @return 0: 成功, -1: 失败
*/
static int ads1115_configure(uint8_t channel, ads1115_gain_t gain, ads1115_rate_t rate)
{
uint16_t config = 0;
// 检查参数有效性
if (channel > 3 || gain > ADS1115_GAIN_6_144V || rate > ADS1115_RATE_860)
return -1;
// 配置寄存器:
// - 单次转换模式
// - 禁用比较器
config |= (1 << 15); // OS = 1 (开始单次转换)
config |= (channel << 12); // MUX[2:0] = 通道
config |= (gain << 9); // PGA[2:0] = 增益
config |= (1 << 8); // MODE = 1 (单次转换模式)
config |= (rate << 5); // DR[2:0] = 数据速率
config |= (0 << 4); // COMP_MODE = 0 (传统模式)
config |= (0 << 3); // COMP_POL = 0 (低电平有效)
config |= (0 << 2); // COMP_LAT = 0 (非锁存)
config |= (3 << 0); // COMP_QUE = 3 (禁用比较器)
// 保存当前配置
ads1115_dev.current_channel = channel;
ads1115_dev.current_gain = gain;
// 写入配置寄存器
return ads1115_write_reg(ADS1115_REG_CONFIG, config);
}
/**
* @brief 等待转换完成
* @param timeout 超时时间(ms)
* @return 0: 转换完成, -1: 超时
*/
static int ads1115_wait_conversion(uint32_t timeout)
{
uint16_t config;
uint32_t start_time = rt_tick_get();
while (1)
{
// 读取配置寄存器
if (ads1115_read_reg(ADS1115_REG_CONFIG, &config) != 0)
return -1;
// 检查OS位(bit 15),1表示转换完成
if (config & (1 << 15))
return 0;
// 检查超时
if (rt_tick_get() - start_time > rt_tick_from_millisecond(timeout))
{
rt_kprintf("ADS1115 conversion timeout\n");
return -1;
}
rt_thread_mdelay(1);
}
}
/**
* @brief 读取指定通道的原始ADC值
* @param channel 通道(0-3)
* @param value 存储ADC值的指针
* @return 0: 成功, -1: 失败
*/
int ads1115_read_raw(uint8_t channel, int16_t *value)
{
uint16_t raw_data;
if (value == RT_NULL)
return -1;
// 配置ADS1115
if (ads1115_configure(channel, ads1115_dev.default_gain, ads1115_dev.default_rate) != 0)
return -1;
// 等待转换完成
if (ads1115_wait_conversion(100) != 0)
return -1;
// 读取转换结果
if (ads1115_read_reg(ADS1115_REG_CONV, &raw_data) != 0)
return -1;
// 转换为有符号整数
*value = (int16_t)raw_data;
return 0;
}
/**
* @brief 读取指定通道的电压值(V)
* @param channel 通道(0-3)
* @param voltage 存储电压值的指针
* @return 0: 成功, -1: 失败
*/
int ads1115_read_voltage(uint8_t channel, float *voltage)
{
int16_t raw_data;
float gain_voltage;
if (voltage == RT_NULL)
return -1;
// 读取原始ADC值
if (ads1115_read_raw(channel, &raw_data) != 0)
return -1;
// 根据增益计算电压
switch (ads1115_dev.current_gain)
{
case ADS1115_GAIN_6_144V:
gain_voltage = 6.144f;
break;
case ADS1115_GAIN_4_096V:
gain_voltage = 4.096f;
break;
case ADS1115_GAIN_2_048V:
gain_voltage = 2.048f;
break;
case ADS1115_GAIN_1_024V:
gain_voltage = 1.024f;
break;
case ADS1115_GAIN_0_512V:
gain_voltage = 0.512f;
break;
case ADS1115_GAIN_0_256V:
gain_voltage = 0.256f;
break;
default:
gain_voltage = 6.144f;
break;
}
// 计算电压值
*voltage = (raw_data * gain_voltage) / 32768.0f;
return 0;
}
/**
* @brief 设置默认增益
* @param gain 增益
*/
void ads1115_set_default_gain(ads1115_gain_t gain)
{
if (gain <= ADS1115_GAIN_6_144V)
{
ads1115_dev.default_gain = gain;
}
}
/**
* @brief 设置默认数据速率
* @param rate 数据速率
*/
void ads1115_set_default_rate(ads1115_rate_t rate)
{
if (rate <= ADS1115_RATE_860)
{
ads1115_dev.default_rate = rate;
}
}
/**
* @brief 初始化ADS1115
* @param i2c_bus_name I2C总线名称
* @return 0: 成功, -1: 失败
*/
int ads1115_init(const char *i2c_bus_name)
{
// 查找I2C总线设备
ads1115_dev.i2c_bus = rt_i2c_bus_find(i2c_bus_name);
if (ads1115_dev.i2c_bus == RT_NULL)
{
rt_kprintf("can't find I2C bus %s\n", i2c_bus_name);
return -1;
}
// 初始化默认参数
ads1115_dev.current_channel = 0;
ads1115_dev.default_gain = ADS1115_GAIN_4_096V; // 默认增益±4.096V
ads1115_dev.default_rate = ADS1115_RATE_128; // 默认速率128SPS
rt_kprintf("ADS1115 initialized successfully\n");
return 0;
}
// 导出到自动初始化
INIT_DEVICE_EXPORT(ads1115_init);
ADS1115驱动头文件
V1
#ifndef __ADS1115_H
#define __ADS1115_H
#include <rtthread.h>
// 增益配置枚举
typedef enum
{
ADS1115_GAIN_6_144V = 0, // ±6.144V
ADS1115_GAIN_4_096V, // ±4.096V
ADS1115_GAIN_2_048V, // ±2.048V (默认)
ADS1115_GAIN_1_024V, // ±1.024V
ADS1115_GAIN_0_512V, // ±0.512V
ADS1115_GAIN_0_256V // ±0.256V
} ads1115_gain_t;
// 数据速率配置枚举
typedef enum
{
ADS1115_RATE_8 = 0, // 8SPS
ADS1115_RATE_16, // 16SPS
ADS1115_RATE_32, // 32SPS
ADS1115_RATE_64, // 64SPS
ADS1115_RATE_128, // 128SPS (默认)
ADS1115_RATE_250, // 250SPS
ADS1115_RATE_475, // 475SPS
ADS1115_RATE_860 // 860SPS
} ads1115_rate_t;
// ADS1115设备结构体
struct ads1115_device
{
struct rt_i2c_bus_device *i2c_bus; // I2C总线设备
uint8_t current_channel; // 当前通道
ads1115_gain_t current_gain; // 当前增益
ads1115_gain_t default_gain; // 默认增益
ads1115_rate_t default_rate; // 默认数据速率
};
/**
* @brief 初始化ADS1115
* @param i2c_bus_name I2C总线名称
* @return 0: 成功, -1: 失败
*/
int ads1115_init(const char *i2c_bus_name);
/**
* @brief 读取指定通道的原始ADC值
* @param channel 通道(0-3)
* @param value 存储ADC值的指针
* @return 0: 成功, -1: 失败
*/
int ads1115_read_raw(uint8_t channel, int16_t *value);
/**
* @brief 读取指定通道的电压值(V)
* @param channel 通道(0-3)
* @param voltage 存储电压值的指针
* @return 0: 成功, -1: 失败
*/
int ads1115_read_voltage(uint8_t channel, float *voltage);
/**
* @brief 设置默认增益
* @param gain 增益
*/
void ads1115_set_default_gain(ads1115_gain_t gain);
/**
* @brief 设置默认数据速率
* @param rate 数据速率
*/
void ads1115_set_default_rate(ads1115_rate_t rate);
#endif /* __ADS1115_H */
6.1.3 电压电流采集应用
基于 ADS1115 驱动,实现电压和电流采集功能:
电压电流采集代码
V1
#include "voltage_current.h"
#include "ads1115.h"
#include "rtthread.h"
// 通道定义
#define VOLTAGE_CHANNEL 0 // 电压采集通道
#define CURRENT_CHANNEL 1 // 电流采集通道
// 校准参数
static float voltage_offset = 0.0f;
static float voltage_scale = 1.0f;
static float current_offset = 0.0f;
static float current_scale = 1.0f;
// 数据滤波缓冲区
#define FILTER_DEPTH 5
static float voltage_buffer[FILTER_DEPTH];
static float current_buffer[FILTER_DEPTH];
static uint8_t buffer_index = 0;
/**
* @brief 初始化电压电流采集
* @return 0: 成功, -1: 失败
*/
int voltage_current_init(void)
{
// 初始化ADS1115
if (ads1115_init("i2c0") != 0)
{
rt_kprintf("Voltage current sensor initialization failed\n");
return -1;
}
// 配置ADS1115通道增益
// 电压通道:使用±4.096V增益
// 电流通道:使用±2.048V增益(因为电流范围较小)
ads1115_set_default_gain(ADS1115_GAIN_4_096V);
// 初始化滤波缓冲区
rt_memset(voltage_buffer, 0, sizeof(voltage_buffer));
rt_memset(current_buffer, 0, sizeof(current_buffer));
rt_kprintf("Voltage current sensor initialized successfully\n");
return 0;
}
/**
* @brief 简单移动平均滤波
* @param buffer 数据缓冲区
* @param new_value 新数据
* @param depth 缓冲区深度
* @return 滤波后的值
*/
static float moving_average_filter(float *buffer, float new_value, uint8_t depth)
{
float sum = 0.0f;
uint8_t i;
// 存储新值
buffer[buffer_index] = new_value;
// 计算平均值
for (i = 0; i < depth; i++)
{
sum += buffer[i];
}
// 更新索引
buffer_index = (buffer_index + 1) % depth;
return sum / depth;
}
/**
* @brief 读取电机母线电压
* @param voltage 存储电压值的指针(V)
* @return 0: 成功, -1: 失败
*/
int read_motor_voltage(float *voltage)
{
float raw_voltage;
float filtered_voltage;
if (voltage == RT_NULL)
return -1;
// 读取原始电压(来自分压电路)
if (ads1115_read_voltage(VOLTAGE_CHANNEL, &raw_voltage) != 0)
return -1;
// 电压分压电路:分压比为1:11(30V输入对应约2.73V输出)
// 因此实际电压 = 测量电压 * 11
raw_voltage *= 11.0f;
// 应用校准参数
raw_voltage = (raw_voltage - voltage_offset) * voltage_scale;
// 应用滤波
filtered_voltage = moving_average_filter(voltage_buffer, raw_voltage, FILTER_DEPTH);
// 限制电压范围
if (filtered_voltage < 0.0f)
filtered_voltage = 0.0f;
if (filtered_voltage > 30.0f)
filtered_voltage = 30.0f;
*voltage = filtered_voltage;
return 0;
}
/**
* @brief 读取电机工作电流
* @param current 存储电流值的指针(A)
* @return 0: 成功, -1: 失败
*/
int read_motor_current(float *current)
{
float raw_voltage;
float raw_current;
float filtered_current;
if (current == RT_NULL)
return -1;
// 临时切换到适合电流测量的增益
ads1115_set_default_gain(ADS1115_GAIN_2_048V);
// 读取INA138输出电压
if (ads1115_read_voltage(CURRENT_CHANNEL, &raw_voltage) != 0)
{
// 恢复电压通道的增益设置
ads1115_set_default_gain(ADS1115_GAIN_4_096V);
return -1;
}
// 恢复电压通道的增益设置
ads1115_set_default_gain(ADS1115_GAIN_4_096V);
// 电流测量电路为1:1电压还原(1V对应1A)
raw_current = raw_voltage;
// 应用校准参数
raw_current = (raw_current - current_offset) * current_scale;
// 应用滤波
filtered_current = moving_average_filter(current_buffer, raw_current, FILTER_DEPTH);
// 限制电流范围
if (filtered_current < 0.0f)
filtered_current = 0.0f;
if (filtered_current > 5.0f)
filtered_current = 5.0f;
*current = filtered_current;
return 0;
}
/**
* @brief 设置电压校准参数
* @param offset 偏移量(V)
* @param scale 比例系数
*/
void set_voltage_calibration(float offset, float scale)
{
voltage_offset = offset;
voltage_scale = scale;
}
/**
* @brief 设置电流校准参数
* @param offset 偏移量(A)
* @param scale 比例系数
*/
void set_current_calibration(float offset, float scale)
{
current_offset = offset;
current_scale = scale;
}
/**
* @brief 获取电压校准参数
* @param offset 存储偏移量的指针(V)
* @param scale 存储比例系数的指针
*/
void get_voltage_calibration(float *offset, float *scale)
{
if (offset != RT_NULL)
*offset = voltage_offset;
if (scale != RT_NULL)
*scale = voltage_scale;
}
/**
* @brief 获取电流校准参数
* @param offset 存储偏移量的指针(A)
* @param scale 存储比例系数的指针
*/
void get_current_calibration(float *offset, float *scale)
{
if (offset != RT_NULL)
*offset = current_offset;
if (scale != RT_NULL)
*scale = current_scale;
}
// 导出初始化函数
INIT_APP_EXPORT(voltage_current_init);
6.2 磁编码器驱动
有刷电机自带的磁编码器输出 A、B 两相正交脉冲,通过检测这些脉冲可以计算电机的转速和转动圈数。我们将使用 GD32F305 的定时器来捕获这些脉冲。
6.2.1 编码器工作原理
磁编码器通常有两个输出通道(A 和 B),其输出波形是相位相差 90 度的方波:
- 当电机正转时,A 相脉冲超前 B 相脉冲 90 度
- 当电机反转时,B 相脉冲超前 A 相脉冲 90 度
通过定时器的编码器模式,可以同时检测脉冲数和方向,从而计算电机的位置、转速和转动圈数。
6.2.2 编码器驱动实现
磁编码器驱动代码encoder.c
V1
#include "encoder.h"
#include "rtthread.h"
#include "gd32f30x.h"
#include "drv_timer.h"
// 编码器定时器定义
#define ENCODER_TIMER TIM4
#define ENCODER_TIMER_CLK RCU_TIM4
// 编码器脉冲数与电机圈数的比例(根据实际编码器参数设置)
#define ENCODER_PPR 1000 // 每圈脉冲数
// 全局编码器数据
static struct encoder_data encoder = {
.count = 0,
.speed = 0,
.direction = 0,
.last_timer_count = 0,
.ppr = ENCODER_PPR
};
// 定时器更新信号量
static struct rt_semaphore timer_sem;
/**
* @brief 初始化编码器定时器
*/
static void encoder_timer_init(void)
{
timer_parameter_struct timer_initpara;
timer_ic_parameter_struct timer_icinitpara;
// 使能定时器时钟
rcu_periph_clock_enable(ENCODER_TIMER_CLK);
// 复位定时器
timer_deinit(ENCODER_TIMER);
// 配置定时器基本参数
timer_initpara.prescaler = 0;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 65535; // 最大计数
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(ENCODER_TIMER, &timer_initpara);
// 配置编码器模式
timer_icinitpara.icpolarity = TIMER_IC_POLARITY_RISING;
timer_icinitpara.icselection = TIMER_IC_SELECTION_DIRECTTI;
timer_icinitpara.icprescaler = TIMER_IC_PSC_DIV1;
timer_icinitpara.icfilter = 0x0F; // 滤波
// 配置通道1
timer_ic_init(ENCODER_TIMER, TIMER_CH_1, &timer_icinitpara);
// 配置通道2
timer_ic_init(ENCODER_TIMER, TIMER_CH_2, &timer_icinitpara);
// 配置编码器模式:同时捕获CH1和CH2
timer_encoder_mode_config(ENCODER_TIMER, TIMER_ENCODER_MODE_TI12,
TIMER_IC_POLARITY_RISING, TIMER_IC_POLARITY_RISING);
// 清除中断标志
timer_flag_clear(ENCODER_TIMER, TIMER_FLAG_UPDATE);
// 使能定时器更新中断
timer_interrupt_enable(ENCODER_TIMER, TIMER_INT_UPDATE);
// 配置NVIC
nvic_irq_enable(TIMER4_IRQn, 5, 0);
// 启动定时器
timer_enable(ENCODER_TIMER);
}
/**
* @brief 编码器定时器中断服务函数
*/
void TIMER4_IRQHandler(void)
{
if (timer_interrupt_flag_get(ENCODER_TIMER, TIMER_INT_UPDATE) != RESET)
{
// 清除中断标志
timer_interrupt_flag_clear(ENCODER_TIMER, TIMER_INT_UPDATE);
// 释放信号量,通知速度计算线程
rt_sem_release(&timer_sem);
}
}
/**
* @brief 编码器速度计算线程
* @param parameter 线程参数
*/
static void encoder_speed_thread(void *parameter)
{
uint32_t current_count;
int32_t diff;
rt_tick_t last_tick = rt_tick_get();
while (1)
{
// 等待定时器更新中断
rt_sem_take(&timer_sem, RT_WAITING_FOREVER);
// 读取当前定时器计数值
current_count = timer_counter_read(ENCODER_TIMER);
// 计算脉冲差值(考虑溢出)
diff = (int32_t)(current_count - encoder.last_timer_count);
// 更新总计数
encoder.count += diff;
// 计算速度 (RPM)
// 速度 = (脉冲数 / 每圈脉冲数) * (60秒 / 采样时间(秒))
// 采样时间为1秒
encoder.speed = (abs(diff) * 60) / encoder.ppr;
// 确定方向
encoder.direction = (diff > 0) ? 1 : 0;
// 更新上一次计数值
encoder.last_timer_count = current_count;
// 记录当前时间戳
last_tick = rt_tick_get();
}
}
/**
* @brief 初始化编码器
* @return 0: 成功, -1: 失败
*/
int encoder_init(void)
{
rt_err_t ret;
// 初始化信号量
ret = rt_sem_init(&timer_sem, "encoder_sem", 0, RT_IPC_FLAG_FIFO);
if (ret != RT_EOK)
{
rt_kprintf("Failed to initialize encoder semaphore\n");
return -1;
}
// 初始化编码器定时器
encoder_timer_init();
// 创建速度计算线程
rt_thread_t thread = rt_thread_create("encoder",
encoder_speed_thread,
RT_NULL,
512,
7,
10);
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
rt_kprintf("Failed to create encoder thread\n");
return -1;
}
rt_kprintf("Encoder initialized successfully\n");
return 0;
}
/**
* @brief 获取编码器数据
* @param data 存储编码器数据的指针
* @return 0: 成功, -1: 失败
*/
int encoder_get_data(struct encoder_data *data)
{
if (data == RT_NULL)
return -1;
// 关中断保护
rt_base_t level = rt_hw_interrupt_disable();
// 复制数据
data->count = encoder.count;
data->speed = encoder.speed;
data->direction = encoder.direction;
data->ppr = encoder.ppr;
// 开中断
rt_hw_interrupt_enable(level);
return 0;
}
/**
* @brief 获取电机转动圈数
* @param revolutions 存储圈数的指针
* @return 0: 成功, -1: 失败
*/
int encoder_get_revolutions(float *revolutions)
{
if (revolutions == RT_NULL)
return -1;
// 关中断保护
rt_base_t level = rt_hw_interrupt_disable();
// 计算圈数:总脉冲数 / 每圈脉冲数
*revolutions = (float)encoder.count / encoder.ppr;
// 开中断
rt_hw_interrupt_enable(level);
return 0;
}
/**
* @brief 重置编码器计数
*/
void encoder_reset_count(void)
{
// 关中断保护
rt_base_t level = rt_hw_interrupt_disable();
// 重置计数器
encoder.count = 0;
encoder.last_timer_count = timer_counter_read(ENCODER_TIMER);
// 开中断
rt_hw_interrupt_enable(level);
}
/**
* @brief 设置编码器每圈脉冲数
* @param ppr 每圈脉冲数
*/
void encoder_set_ppr(uint32_t ppr)
{
if (ppr > 0)
{
// 关中断保护
rt_base_t level = rt_hw_interrupt_disable();
encoder.ppr = ppr;
// 开中断
rt_hw_interrupt_enable(level);
}
}
// 导出初始化函数
INIT_DEVICE_EXPORT(encoder_init);
磁编码器驱动头文件encode.h
V1
#ifndef __ENCODER_H
#define __ENCODER_H
#include <rtthread.h>
// 编码器数据结构体
struct encoder_data
{
int32_t count; // 总脉冲计数
uint32_t speed; // 转速 (RPM)
uint8_t direction; // 方向: 0=反转, 1=正转
uint16_t last_timer_count; // 上一次定时器计数值
uint32_t ppr; // 每圈脉冲数
};
/**
* @brief 初始化编码器
* @return 0: 成功, -1: 失败
*/
int encoder_init(void);
/**
* @brief 获取编码器数据
* @param data 存储编码器数据的指针
* @return 0: 成功, -1: 失败
*/
int encoder_get_data(struct encoder_data *data);
/**
* @brief 获取电机转动圈数
* @param revolutions 存储圈数的指针
* @return 0: 成功, -1: 失败
*/
int encoder_get_revolutions(float *revolutions);
/**
* @brief 重置编码器计数
*/
void encoder_reset_count(void);
/**
* @brief 设置编码器每圈脉冲数
* @param ppr 每圈脉冲数
*/
void encoder_set_ppr(uint32_t ppr);
/**
* @brief 获取电机转速
* @param speed 存储转速的指针(RPM)
* @return 0: 成功, -1: 失败
*/
int encoder_get_speed(uint32_t *speed);
/**
* @brief 获取电机转动方向
* @param direction 存储方向的指针(0=反转, 1=正转)
* @return 0: 成功, -1: 失败
*/
int encoder_get_direction(uint8_t *direction);
#endif /* __ENCODER_H */

9811

被折叠的 条评论
为什么被折叠?



