
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
[一、SensorTask 任务整体概述](#一、SensorTask 任务整体概述)
[3.1 工作原理](#3.1 工作原理)
[四、IMU 数据读取](#四、IMU 数据读取)
[4.1、I²C 总线基础](#4.1、I²C 总线基础)
[4.2、MS6DSV 传感器工作原理](#4.2、MS6DSV 传感器工作原理)
[1. 加速度计原理](#1. 加速度计原理)
[5.1、SensorSnap_Set 写入接口](#5.1、SensorSnap_Set 写入接口)
[5.2、SensorSnap_Get 读取接口](#5.2、SensorSnap_Get 读取接口)
前言
在前面系列文章中,我们先后完成了通信任务 CommTask 、控制任务 ControlTask的设计与实现。依托 FreeRTOS 任务通知、双模式状态机、运动学逆解与 PID 闭环,已经搭建起机器人底盘运动控制的核心调度与驱动体系,能够正常解析上位机速度指令,并完成电机 PWM 闭环输出。
但运动控制只是机器人运行的一部分,整机稳定运行还依赖实时状态监测,例如电池电压、车轮转速、车体姿态等关键数据。为此系统独立拆分出SensorTask 传感器采集任务,作为整机状态感知核心,统一负责各类外设数据的周期性采集、处理与汇总,为控制逻辑、上位机上报、设备安全防护提供可靠数据源,实现控制 + 感知双任务协同架构。
一、SensorTask 任务整体概述
SensorTask 属于系统底层常驻任务,采用周期性轮询的运行方式,独立于控制任务,避免传感器采集占用运动控制时序。核心职责如下:
初始化各类传感器(电池 ADC、编码器、IMU)。
周期性采集原始传感器数据。
做必要的单位换算与整形。
统一封装所有传感器数据,生成系统状态快照( g_status_snap**),快照的写入采用临界区保护,保证一次性完整拷贝,通过** SensorSnap_Get函数进行读取(也是临界区保护),供其他任务共享调用( 读快照**);**
提供线程安全的快照读写接口,供 UplinkTask (上报任务)使用。

二、电池电压采集实现
电池电压是机器人安全运行的重要指标,电压过低会导致动力不足、芯片工作异常,因此需要实时监测。
因为电池电压本质上还是属于模拟信号,mcu只能处理数字信号,因此这里我们采用ADC模数采集方案,利用 STM32 片内 ADC 外设,将电池模拟电压信号转化为数字量。通过电阻分压电路,将高压电池电压降压至单片机允许采样范围,再通过 ADC 采样换算出真实电压值。
本项目使用开发板上的光敏电阻电路来模拟电池电压变化,在不接入真实电池的情况下即可完成整套采样链路验证。

原理:光越强 -> 光敏二极管导通越强 -> 阻抗越低 -> 分压越少 -> 采样电压越低。
为保证数据平稳,本模块采用TIM8 触发 ADC3 + DMA 循环采样的方式,连续多次采样后取平均值,消除瞬时干扰带来的数值波动。
具体:TIM8 以固定频率触发 ADC3 进行一次采样,采样结果通过 DMA 自动搬运到 adc_buf 缓冲区中,并以循环模式不断刷新。这里我们设置缓冲长度为 32,也就是说系统会连续保存最近 32 次采样值。随后,软件对 adc_buf 中的数据进行累加求和并取平均,得到更稳定的原始 ADC 数值 raw。
STM32 的 ADC 通常为 12 位(0..4095),参考电压 VREF=3300mV。电压计算公式为:
电压(mV) = raw * 3300 / 4095。本代码即用宏VREF_MV=3300和ADC_MAX=4095完成此转换。
三、编码器转速采集
编码器是底盘速度闭环的核心反馈来源,单纯PID开环控制无法抵抗摩擦、电压波动等干扰,必须依靠编码器反馈实际轮速。
3.1 工作原理
编码器的本质,就是把 "轮子转动" 这个机械动作,转换成 MCU 可识别的电信号 ------ 也就是脉冲信号。
轮子转得越快,编码器输出的脉冲就越密集;转速越慢,脉冲的间隔就越稀疏。对单片机而言,它不需要 "亲眼看见" 轮子如何转动,只需要统计单位时间内脉冲数量,结合编码器线数、车轮周长,即可换算出车轮实际行进速度。
举例:
假设一个增量式编码器参数是 500 PPR(每圈 500 个脉冲),意味着编码器每转一圈输出 500个脉冲。
如果 MCU 在运行过程中统计到总共收到了 1000 个脉冲,就可以推断轮子转了 1000 / 500 = 2 圈。
轮子转过的圈数 = 收到的总脉冲数 ÷ PPR
测速的逻辑也很简单:速度 = 单位时间内转过的距离。可以换成频率来算。
假设在 1 秒内收到 1000 个脉冲(频率就是1000HZ),轮子转一圈是 500 脉冲,所以 1000 脉冲 / 秒就等于 1000 / 500 = 2 圈 / 秒,如果轮子周长是 628mm,那么速度就是 2 × 628 = 1256mm/s。
3.2、无硬件编码器的数据模拟方案
在调试阶段,若没有真实的电机和编码器硬件,这时候就可以用一种巧妙的方法:用 MCU 自己 "生成" 一串脉冲信号,再把这串脉冲通过跳线送回 MCU 的输入捕获引脚,从而在没有硬件编码器的情况下,也能完整跑通轮速采集链路。
具体实现步骤:
生成模拟脉冲:让TIM10/TIM14定时器输出 PWM 方波,把这个方波当作 "模拟编码器脉冲";
跳线回环:用杜邦线把 PWM 输出的 IO 口连接到TIM2的输入捕获 IO 口;
捕获与计算:TIM2捕获到规律的脉冲波形,像读取真实编码器一样测量脉冲间隔,计算出转速。
总体流程框图如下:

3.3、与仿真电机模型联动
本项目中,我们可以通过仿真电机模型得到左右轮的轮速结果vL_meas/vR_meas,再反向推导出 "对应轮速的编码器脉冲频率"。将这个频率设置到定时器 PWM 输出上,让 TIM10 输出左轮对应频率的方波,让 TIM14 输出右轮对应频率的方波 ------ 这一步就相当于 "生成编码器脉冲"。随后 TIM2 的输入捕获模块会去采集这串方波,通过测量相邻脉冲之间的时间间隔 dt 得到频率 hz = 1/dt,再根据轮子周长和编码器脉冲数(PPR)换算出最终的速度(mm/s)。这样就完整复现了真实系统的闭环链路。

四、IMU 数据读取
IMU(惯性测量单元)是机器人姿态感知的核心传感器,本项目使用的 MS6DSV六轴传感器,正是通过标准 I²C 总线 与 MCU 进行通信。
4.1、I²C 总线基础
I²C是嵌入式开发中非常常见的板级短距离串行通信总线,用来让 MCU 和各种外设芯片交换数据。它的定位有点像 "主控带着一串从设备一起聊天",优点是省引脚、连线简单、生态成熟。
I²C 最经典的特点是只需要两根线就能完成通信:
SCL(时钟线):由主设备提供时钟,控制通信时序;
SDA(数据线):数据按位在这条线上传输。
I²C 是典型的主从模型。
主设备(MCU)发起通信、决定时序;
从设备有自己的地址(常见 7 位,也支持 10 位),主设备通过地址 "点名" 某个从设备,然后进行读或写;

时序流程图如下:

具体可以参考以下博客:
STM32 I2C 总线通信实战|从原理到 OLED 屏数据收发(超详细)---STM32 HAL库专栏_单片机接oled显示屏原理图-CSDN博客
I2C 写寄存器时序(流程图)如下:

I2C 读寄存器时序(流程图)如下:

4.2、MS6DSV 传感器工作原理
MS6DSV 是一款高性能 3 轴加速度计 + 3 轴陀螺仪 六轴 MEMS 惯性传感器,是机器人获取姿态、运动、振动信息的核心部件。
MS6DSV 使用 GPIO 模拟 I2C 与 MCU 通信,遵循标准 I2C 主从机制。
该模块采用的是ST公司的 LSM6DSV16X六轴传感器芯片为核心。,实物图如下:

1. 加速度计原理
加速度计内部采用 MEMS 微机械结构,内置可移动质量块。
当设备发生运动或受到重力作用时,质量块产生位移,引起电容变化,传感器将其转换为电信号,最终输出 三轴线性加速度。
静止时:可测量重力加速度,用于计算倾斜角度。
运动时:可测量运动加速度,用于判断运动状态。
单位:mg(1g = 9.8m/s²)
2、陀螺仪原理
陀螺仪基于 科里奥利力(Coriolis Effect) 工作。内部驱动结构持续振动,当设备发生旋转时,振动块受到垂直方向的力,产生可检测的电容变化,从而解算出 三轴角速度。
用于检测旋转快慢与方向。
单位:mdps(毫度 / 秒)。
加速度计 + 陀螺仪共同构成 六轴姿态感知系统。
在 SensorTask 主循环中

五、全局快照
SensorTask 每隔一段时间会采集一次数据:电池电压、轮速、IMU(加速度 / 角速度 / 温度)。这些数据属于系统状态,不仅要给 UplinkTask 发送给上位机,可能还要给 LCD 显示、日志记录、监控任务使用。
如果每个任务都自己去读 ADC / 编码器 / IIC,那么耦合度会非常高:每个任务都要懂硬件细节;同时时序会混乱,不同任务读到的数据可能不是同一时刻的。
为了解决这个问题,本项目引入了一个共享的状态缓存 ------g_status_snap。它是一个在协议层定义的结构体(protocol.h),包含了机器人所有关键状态信息。
cpp
typedef struct
{
uint16_t battery_mv; /* 电池电压, mV */
int16_t temp; /* 温度, ℃ * 100 */
int16_t vx; /* x 方向速度, mm/s */
int16_t vy; /* y 方向速度, mm/s (差速底盘固定为 0) */
int16_t vz; /* 角速度, mrad/s */
int16_t gx; /* 角速度 X, 0.01 deg/s */
int16_t gy; /* 角速度 Y, 0.01 deg/s */
int16_t gz; /* 角速度 Z, 0.01 deg/s */
int16_t ax; /* 加速度 X, mg */
int16_t ay; /* 加速度 Y, mg */
int16_t az; /* 加速度 Z, mg */
} proto_status_t;
为了保证多任务并发访问的线程安全,项目为这个全局快照提供了两个线程安全的访问函数:SensorSnap_Set 和 SensorSnap_Get,它们是 g_status_snap 的唯一写入入口和唯一读入口。
5.1、SensorSnap_Set 写入接口
该函数由 SensorTask 调用,用于更新全局状态快照。
cpp
void SensorSnap_Set(const proto_status_t *s)
{
if (!s) return; // 入参判空,防止空指针
taskENTER_CRITICAL(); // 进入临界区,禁止任务切换
g_status_snap = *s; // 结构体整体拷贝,原子更新
taskEXIT_CRITICAL(); // 退出临界区
}
关键点:使用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 保护整个拷贝过程,确保数据是一次性完整写入的,避免其他任务读到 "半新半旧" 的撕裂数据。
5.2、SensorSnap_Get 读取接口
该函数由其他任务(如 UplinkTask、日志任务)调用,用于安全地获取当前系统状态。
cpp
void SensorSnap_Get(proto_status_t *out)
{
if (!out) return; // 入参判空,防止空指针
taskENTER_CRITICAL(); // 进入临界区,禁止任务切换
*out = g_status_snap; // 结构体整体拷贝,原子读取
taskEXIT_CRITICAL(); // 退出临界区
}
关键点:读取操作同样被临界区保护,确保读取过程不会被写入操作打断,保证拿到的是一份完整、一致的状态数据副本。
总结
整个机器人下位机三大核心任务分工明确、层层协作:
CommTask 通信任务:负责上位机指令接收、解析与分发,作为控制指令输入源头;
ControlTask 控制任务:接收运动指令,完成运动学逆解、PID 运算、PWM 电机输出;可通过SensorSnap_Get读取真实轮速快照,实现全闭环调速;
SensorTask 传感器任务:统一采集所有硬件状态,生成系统快照;为控制闭环、状态上报、设备监控提供稳定数据源。
三者依托 FreeRTOS 实时调度,基于状态快照实现数据解耦,无强耦合依赖,系统稳定性与可维护性大幅提升。