先把命令和实际完成分开
这份完整例程用实体保持按钮启动一个有限时间、有限脉宽的空载实验。它输出位置请求,不读取舵机真实角度;“命令1500µs”不等于“轴已在90°”。原计划的角度限制在本例落实为1400..1600µs保守命令窗口,实物位置须另量。
参考:[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
指定部件与供电
舵机固定Pololu #2818 FEETECH FS90(位置型,不是FS90R连续旋转型),其商品页所链接厂家FS90-C V1.0手册900..2100µs/1500µs中点;我们不扩展到机械极限。供货标签/修订先与该资料核对,匿名SG90不能直接套同一电流/角度结论。商品页明确支持3.3V控制信号,不从“能插三根线”推断所有舵机都支持。
用独立稳压5.0V实验电源,额定至少1A,先设电流限制1A,带可立即断开的低压开关。板只由桥USB供电,外部5V绝不连板的5V/3V3;电源负极、舵机GND与板GND在低阻公共地相连,舵机大电流回路直接回电源,别绕过面包板主控细线。电源应能应对启动瞬态,1A是本例设定,不是保证每台舵机都满足。靠近舵机供电口并联470µF/≥10V极性电容和100nF陶瓷,核正负极;电容不能替代合格电源/导线,改线前断电并等电容放电。
厂商链接手册的4.8V/6V堵转栏是700/800mA;Pololu商品spec页自己的6V测试值600mA并说明不是厂家定值。二者不一致,不能挑600mA当所有批次上限。本例按独立≥1A台架留余量并记录实际启动电流;不手抓堵转来复现规格,也不把6V参数直接当5V实测。
先不装摆臂/负载,把舵机外壳固定,轴周围留空。核供应方此台JR接头的端点图,再追三根线;资料不足先不要加电。首次中点命令可能使轴突然到中点附近,启动无PWM也不保证轴原位/无抖动。断PWM仅撤控制信号,舵机电源仍在;放松、保持或残余动作由实物决定。
| 端点 | 连接 | 约束 |
|---|---|---|
| GPIO5/J1-5 | 常开ARM按钮到GND;同点外接10k到本板3V3 | LOW保持,初始必须松开 |
| GPIO6/J1-6 | 舵机signal,另10k到GND | 3.3V信号;用明确GND/V+/signal端点,不凭线色猜 |
| 外部5.0V+ | 舵机V+,电容正端 | 不接主控板电源脚 |
| 外部电源负极 | 舵机GND、电容负端、板GND/J1-22 | 电机回流直接回电源;信号有共同参考 |
| 桥USB | 主控供电、下载、UART0 | 外部执行器电源开关独立可断 |
参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
程序怎样限制命令
50Hz/20ms周期,14bit分辨率;duty=round(pulse_us×16384/20000)。1400/1500/1600µs对应1147/1229/1311,单步名义量化约1.22µs。这是计算与SDK请求值,真实周期/高脉宽用示波器或逻辑分析仪检查;看占空比7.5%时也别忽略周期必须是本例20ms。
上电默认ledc_stop→LOW,GPIO6的外部10k下拉在ROM/重启阶段也给控制线明确低电平。ARM启动就按住不会开始;松开稳定50ms后,再按住稳定并保持1秒才能启动。先请求1500µs并留1秒,再每100ms最多改变5µs,在1400..1600间缓慢往返;不跳过迟到时刻连补多步。持续保持最多10秒,到时停PWM并要求松开重来。厂家名义死区10µs大于本例5µs命令步进,真实轴可能隔几步才动或存在滞回;慢改命令不保证机械连续平滑。
原始ARM一旦释放,下一次owner tick就请求停PWM,不必等释放去抖;重新ARM仍须稳定释放/按下条件。主循环请求20ms tick,不将它叫严格20ms响应保证。时钟倒退或相邻owner运行间隔>250ms会撤本地使能,实际停信号发生在owner恢复执行时;不能保证卡死时自动切断动力。API错误锁闭至手工重启,先请求LOW,保留错误日志。
参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
台架步骤和通过条件
observations.csv有command与measured分列。要诊断抖动,同步看signal脉宽/周期与servo V+/主控3V3最小值,再查共地、接头/线阻和电源限流,最后才改软件。分辨率加到20bit不能补足缺电;把微秒直接当角度也不能校准零点。
- 执行器外电源关闭、ARM松开,只USB刷写。先不接舵机signal,在GPIO6/GND测上电默认LOW;再保持ARM检查50Hz及中点/窗口脉宽、释放时软件停信号。自己实际通过后才断电接舵机。
- 核三线与共地,外电源5V/1A限制,上电仍不按ARM,观察无请求状态;此前无反馈、任何抖动都记录,不补写“默认安全角度”。
- 外壳固定、轴空载。保持ARM,记录中点请求、真实初动、电源最小值/电流与板3V3;再观察慢扫窗口并对照串口命令,不把日志数值当真实角度。
- 松开ARM,分别检查控制线LOW、轴实际何时停止/是否放松;保持超过10秒检查软件期限。上电按住、快速抖动、RESET后需重新释放也单独记录。
- 只有这些空载检查通过,才在完全断电后安装轻小摆臂,重新检查机械余量;不顶住端点、不连接承重或夹持机构。结束先松按钮确认信号停,再断执行器电源和USB。
参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
故障定位与恢复
实际本轮app205488B、8组同源主机检查通过;未连接舵机或示波器,没有实际角度、抖动、电压、电流或上电默认位置结果。保留所有待测行。
| 现象 | 先证据 | 恢复 |
|---|---|---|
| 构建成功不动 | ARM先释放、持有1秒、signal波形、执行器5V和接头端点 | 先断外电源查线,勿一口气放宽脉宽 |
| 有PWM却抖动/复位 | 同步供电跌落、地线、电流限制、接头和机械阻力 | 卸负载、修电源;保留reset reason,勿关闭brownout |
| 初次ARM大幅动作 | 实际初始轴位与1500µs响应 | 先空轴,停机断电调整机械;不宣称自动归零已验证 |
| 停PWM后轴仍动/松 | 实际signalLOW与动力仍在 | 这是撤信号,不是制动/电源隔离;按台架开关断外电源 |
| STOP_REASON3/API_FAULT | owner时序/串口阻塞/错误码 | 停机保存日志,排查后松按钮再重启 |
参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
固定工程、构建与恢复
只支持官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,ESP-IDF5.5.5(commit b774170ff46c393eeb5e495ea37936038d3f4f4f),8MiB DIO80MHz/PSRAM关闭,PM关闭,UART0桥口115200。GPIO19/20原生USB与OPI专用脚未用于外设。实际工具Python3.12.13、GCC14.2.0 esp-14.2.0_20260121;无额外managed组件,不需要安装另一个SDK。
激活自己安装的固定IDF环境,进入解压工程根,使用新的配置目录:
REPLACE_PORT换成本板桥接串口。已有sdkconfig优先于defaults;复用旧配置时要核target/8MB/DIO/PSRAM=n/PM=n及实际UART。串口退出Ctrl+]。BOOT+RESET经桥口恢复ROM下载,先断执行器外部电源、松开全部按钮,再刷回已保存的正常固件,不改eFuse、不盲擦Flash。没有实板上传、运动、波形、电流或故障复位记录。
idf.py -B build-local -D SDKCONFIG=build-local/sdkconfig build
idf.py -B build-local -D SDKCONFIG=build-local/sdkconfig -p REPLACE_PORT flash monitor参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
纯逻辑与真实物理分开
host_check.c直接包含生产策略头,在macOS arm64以clang ASan/UBSan执行合成按键和64位时刻;它不模拟LEDC、GPIO、电机/舵机、惯量或电源。读者可执行:
主循环有自己的单写者,不使用硬件fade,因此用ledc_set_duty再ledc_update_duty;固定5.5.5的ledc_set_duty_and_update会要求fade服务已安装,不能不读前置条件就替换。ledc_stop把该通道输出停在LOW;下一次set+update会重新使能信号,这是固定源码事实,不是示波器结果。APB时钟与PM关闭保证配置的时钟前提,不保证实物抖动、电源或机械响应。软件只在owner实际运行时检查时间;任务阻塞/CPU挂死期间PWM硬件可能继续输出,不能把250ms迟到检查或软件看门狗当独立硬件急停。
每份observations.csv只有表头。自己台架必须保留板号/模块修订、完整源与镜像SHA、接线照片、供电设定及真实波形/现象/失败行。名义频率、量化误差和主机模型不是本站运动、电流、温升或电源稳定性数据。
cc -std=c11 -Wall -Wextra -Werror -fsanitize=address,undefined -fno-omit-frame-pointer host_check.c -o /tmp/actuator-host
/tmp/actuator-host参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
许可和上游
应用、策略头、主机夹具、图文为本站原创MIT,见LICENSE;SDK按其原许可证,未打包SDK或编译产物。固定源码依据ESP-IDF5.5.5 components/esp_driver_ledc/include/driver/ledc.h、src/ledc.c、gpio.h及esp_timer.h;板卡依据官方v1.1固定指南。完整上游链接和器件资料范围见THIRD_PARTY.md/versions.json。
参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项[6] Pololu2818 FS90:位置型、3.3V信号与脉宽范围[7] 厂家链接FS90-C V1.0:电压、堵转栏、900..2100µs与死区[8] Pololu伺服控制接口:脉宽而非占空比决定请求[9] Pololu独立测试电流及非厂家定值注记
实际构建的完整主程序与共享策略
下面两个文件与完整下载工程逐字对应。LEDC/GPIO由单个owner使用,策略头也直接供主机夹具调用;文件名注释只是分隔,不在构建时拼成一个文件。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <inttypes.h>
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "servo_policy.h"
static esp_err_t pulse_set(uint16_t pulse){
/* One owner, no fade/other task uses this channel. The thread-safe combined
API needs an installed fade service in IDF5.5.5; it is deliberately not used. */
esp_err_t e=ledc_set_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,servo_duty(pulse));
return e==ESP_OK?ledc_update_duty(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0):e;
}
void app_main(void){
gpio_config_t in={.pin_bit_mask=1ULL<<GPIO_NUM_5,.mode=GPIO_MODE_INPUT};ESP_ERROR_CHECK(gpio_config(&in));
ledc_timer_config_t timer={.speed_mode=LEDC_LOW_SPEED_MODE,.duty_resolution=LEDC_TIMER_14_BIT,.timer_num=LEDC_TIMER_0,.freq_hz=50,.clk_cfg=LEDC_USE_APB_CLK};ESP_ERROR_CHECK(ledc_timer_config(&timer));
ledc_channel_config_t channel={.gpio_num=GPIO_NUM_6,.speed_mode=LEDC_LOW_SPEED_MODE,.channel=LEDC_CHANNEL_0,.timer_sel=LEDC_TIMER_0,.duty=0,.hpoint=0};ESP_ERROR_CHECK(ledc_channel_config(&channel));ESP_ERROR_CHECK(ledc_stop(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,0));
printf("V06 REQUEST50Hz/14bit; SDK frequency=%"PRIu32"Hz; no physical waveform/position feedback. Release first, then hold ARM1s. External servo5V only; no automatic startup position.\n",ledc_get_freq(LEDC_LOW_SPEED_MODE,LEDC_TIMER_0));
Servo state={0};bool was=false,fault=false;uint16_t last=0;unsigned reason=0;
for(;;){
servo_tick(&state,gpio_get_level(GPIO_NUM_5)==0,(uint64_t)esp_timer_get_time());
if(!fault&&(was!=state.active||(state.active&&last!=state.pulse))){
esp_err_t e=state.active?pulse_set(state.pulse):ledc_stop(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,0);
if(e!=ESP_OK){fault=true;(void)ledc_stop(LEDC_LOW_SPEED_MODE,LEDC_CHANNEL_0,0);printf("PWM_API_FAULT=%s; stop requested; reset only after diagnosis\n",esp_err_to_name(e));}
else printf("SERVO_REQUEST active=%u pulse_us=%u duty=%"PRIu32"; signal/position unmeasured\n",state.active,state.pulse,state.active?servo_duty(state.pulse):0);
}
if(reason!=state.reason){reason=state.reason;printf("STOP_REASON=%u; PWM stop is not power removal or observed shaft stop\n",reason);}
was=state.active;last=state.pulse;vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ---- main/servo_policy.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
#include <stdbool.h>
typedef struct {
bool initialized,candidate,stable,released,holding,active;
uint64_t changed,pressed,started,stepped,last;
uint16_t pulse;int direction;unsigned reason;
} Servo;
/* reason:0 waiting/normal,1 released,2 ten-second limit,3 owner-clock gap.
Time budgets are checked only when this owner runs, never a hardware stop guarantee. */
static inline void servo_tick(Servo *s,bool pressed,uint64_t now){
if(!s->initialized){*s=(Servo){.initialized=true,.candidate=pressed,.stable=true,.changed=now,.last=now,.pulse=1500,.direction=1};}
bool gap=now<s->last||now-s->last>250000;s->last=now;
if(gap){s->active=false;s->released=false;s->holding=false;s->candidate=pressed;s->stable=true;s->changed=now;s->reason=3;}
if(pressed!=s->candidate){s->candidate=pressed;s->changed=now;}
if(now>=s->changed&&now-s->changed>=50000&&s->stable!=s->candidate){
s->stable=s->candidate;
if(!s->stable){s->released=true;s->holding=false;}
else if(s->released){s->pressed=now;s->holding=true;}
}
if(!pressed){if(s->active)s->reason=1;s->active=false;}
if(s->active&&now-s->started>=10000000){s->active=false;s->released=false;s->holding=false;s->reason=2;}
if(!s->active&&pressed&&s->stable&&s->holding&&s->released&&now-s->pressed>=1000000){
s->active=true;s->released=false;s->holding=false;s->started=now;s->stepped=now;s->pulse=1500;s->direction=1;s->reason=0;
}
if(s->active&&now-s->started>=1000000&&now-s->stepped>=100000){
s->stepped=now;s->pulse=(uint16_t)(s->pulse+5*s->direction);
if(s->pulse>=1600){s->pulse=1600;s->direction=-1;}else if(s->pulse<=1400){s->pulse=1400;s->direction=1;}
}
}
static inline uint32_t servo_duty(uint16_t pulse){return pulse<1400||pulse>1600?0:(uint32_t)(((uint64_t)pulse*16384+10000)/20000);}参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[3] GPIO模式、电平与外部上拉/下拉接口[4] 64位微秒单调时钟
默认构建的目标、时钟和资源范围
8MiB Flash、PSRAM关闭、PM关闭与UART0为本例实际配置。改defaults后重用旧sdkconfig不一定生效,须核最终生成值;OPI模组的存在不等于程序已经使用PSRAM。
# ---- sdkconfig.defaults ----
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
# CONFIG_SPIRAM is not set
# CONFIG_PM_ENABLE is not set
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
CONFIG_FREERTOS_HZ=1000参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[5] 官方DevKitC1 v1.1固定GPIO与互斥供电选项
原样策略的合成检查全文
本文件运行在主机,输入的是合成按键与时间;assert通过不证明电机、舵机、LEDC波形或电源已经实际验证。编译命令在上面的“纯逻辑与真实物理分开”一节。
// ---- host_check.c ----
/* SPDX-License-Identifier: MIT */
#include <assert.h>
#include <stdio.h>
#include "main/servo_policy.h"
static uint64_t time_us;
static void steps(Servo *s,bool p,unsigned count){for(unsigned i=0;i<count;i++){servo_tick(s,p,time_us);time_us+=20000;}}
int main(void){
Servo s={0};steps(&s,true,100);assert(!s.active);puts("PASS startup held never starts");
steps(&s,false,5);steps(&s,true,50);assert(!s.active);steps(&s,true,5);assert(s.active&&s.pulse==1500);puts("PASS stable release then1s hold arms neutral");
uint64_t start=s.started;while(time_us-start<1000000)steps(&s,true,1);assert(s.pulse==1500);steps(&s,true,1);assert(s.pulse==1505);puts("PASS neutral1s then5us scheduled increments");
bool high=false,low=false;while(time_us-start<9990000){steps(&s,true,1);assert(s.pulse>=1400&&s.pulse<=1600);high|=s.pulse==1600;low|=s.pulse==1400;}assert(high&&low);steps(&s,true,2);assert(!s.active&&s.reason==2);steps(&s,true,100);assert(!s.active);puts("PASS bounded1400..1600 pulses and10s hold limit requires release");
steps(&s,false,5);steps(&s,true,60);assert(s.active);steps(&s,false,1);assert(!s.active&&s.reason==1);puts("PASS raw release immediately requests signal stop at owner tick");
steps(&s,false,5);steps(&s,true,60);assert(s.active);time_us+=250001;steps(&s,true,1);assert(!s.active&&s.reason==3);steps(&s,true,80);assert(!s.active);puts("PASS owner gap closes output and held input cannot rearm");
assert(servo_duty(1399)==0&&servo_duty(1601)==0&&servo_duty(1400)==1147&&servo_duty(1500)==1229&&servo_duty(1600)==1311);puts("PASS explicit14bit pulse calculation and range rejection; not waveform measurement");
time_us=UINT64_C(5)*UINT64_C(4294967296)*1000;s=(Servo){0};steps(&s,false,5);steps(&s,true,60);assert(s.active);puts("PASS full64bit microsecond time after five32bit millisecond cycles");
puts("8 same-source synthetic groups passed; no servo, GPIO, waveform, angle or power measurement");
}参考:[1] IDF5.5.5 LEDC API:计时、分辨率、duty与stop[2] 固定LEDC实现:stop/重新update与fade服务前置[4] 64位微秒单调时钟
资料来源与版本说明
实际读固定LEDC/GPIO/计时实现与板针脚,厂商部件页及手册另留原文与摘要;编译、同源合成与真实机械/电气分开。FS90厂家/销售方电流差异与DRV8833 UVLO正文/表格差异均明确,不将规格当本站测量。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。