ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32与树莓派Jetson上下位机串口通信与PWM控制实战解析

STM32与树莓派Jetson上下位机串口通信与PWM控制实战解析 简介面向STM32嵌入式开发的无人船/无人车下位机工程需与树莓派、英伟达Jetson Nano等上位机配合使用适合高校计算机、通信、自动化等专业开展毕设、课程设计或初期项目立项。工程采用类似操作系统的多任务调度框架任务可按不同频率独立调用结构清晰且耦合度低便于后续按需裁剪功能模块——例如只需修改宏定义即可切换编译目标体现了一定的飞控级代码组织思路。压缩包共154个文件以C源码、头文件、汇编文件、Keil工程配置uvprojx/uvoptx、HEX固件和说明文档为主总大小约856KB其中STM32标准外设库样例覆盖定时器、Flash、RCC、ADC、I2C、CAN、USART等常用外设。附件还包含README说明与文档便于快速对照阅读。目前已有504人浏览学习资源内代码均完成功能验证作者毕设答辩平均分达96分可作为嵌入式入门进阶或无人平台类项目的可靠参考范本。1. 上下位机架构STM32做实时控制树莓派与Jetson Nano做决策一条串口线、两端C源码、中间一份协议文档无人船/无人车这类上下位机项目的基本盘就这么薄。标题里的分工很明确STM32做下位机管电机PWM波输出、编码器和IMU读取、电池电压采集这些都要毫秒级响应树莓派和英伟达Jetson Nano做上位机跑视觉、路径规划、远程指令解析算完只把速度值和转向值发给STM32。这种架构适合刚把样机装起来的人也适合接手现成C源码改功能的工程师。实际项目里最耗时间的往往不是某一块板子的代码而是两端的协议没对齐、引脚接错、波特率差一点就乱码。接下来按下位机怎么组织、上位机怎么连、联调排什么错、文档怎么用一步步写清楚每段都能落到代码和命令上。2. STM32端C源码外设初始化、PWM波输出与协议帧打包2.1 工程目录先分好再谈CubeMX改型STM32端程序最好是拿到就能编译的结构而不是单文件堆到底。一个只有中断函数和主循环的工程只有原作者能维护。我一般按四个目录拆分bsp/放电机、IMU、电压采集的驱动protocol/放帧格式的定义与解析app/放控制逻辑比如遥控指令到左右电机转速的映射main/只放初始化调用和while(1)主循环。无论是四轮差速车还是双推进器船这套结构都不需要大改。工程生成的常见入口还是STM32CubeMX。需要留意stm32 cube 程序更改型号这类场景CubeMX可以把F103工程另存为F407或G4系列但它只重新生成外设初始化代码用户写在回调函数之外的代码不会被同步迁移。别的坑都好说最大的问题是定时器时钟源。F103的定时器挂在APB1上主频72MHz时通常得到72MHz的计数时钟F407的TIM1、TIM8~TIM11挂在APB2上APB2为84MHz时定时器时钟是168MHz。如果你沿用老的预分频值PWM波输出频率直接翻倍或减半电机转速一下就不对。表格式地总结几个常用芯片的定时器时钟芯片主频定时器时钟来源TIM1时钟常见APB分频STM32F103C8T672MHzAPB1×272MHzAPB1 36MHzSTM32F407VET6168MHzAPB2×2168MHzAPB2 84MHzSTM32G431RBT6170MHzAPB×2170MHzAPB 85MHz改完芯片型号后不要急着编译先在CubeMX的Clock Configuration页里看一遍定时器时钟再回代码里核对预分频。还有一个同样隐蔽的点DMA请求映射在不同系列间不通用。F103的USART1_TX对应DMA1_Channel4F407则对应DMA2_Stream7CubeMX如果没重新生成而你又沿用旧工程的DMA配置数据会发到错误的周边设备。改完型号要检查的不止是Pinout还有DMA Request Table。2.2 电机驱动的PWM波输出频率、脉宽与死区无人车高配用舵机转向加电调驱动低配是两轮差速无人船基本是双推进器差速。共同点是都靠脉冲宽度调制控制电调或舵机而电调和舵机的输入约定高度一致50Hz刷新率脉宽1000~2000微秒中位1500微秒。刷新率不能高于50Hz太多否则电调啸叫、发热低于40Hz会导致舵机抖动。/* bsp/motor.c —— 双通道PWM输出带限幅与死区 */ #include bsp_motor.h #include main.h TIM_HandleTypeDef htim1; TIM_OC_InitTypeDef sConfigOC; void bsp_motor_init(void) { __HAL_RCC_TIM1_CLK_ENABLE(); htim1.Instance TIM1; htim1.Init.Prescaler 168 - 1; /* 168MHz/168 1MHz即1us计数一次 */ htim1.Init.Period 20000 - 1; /* 20ms周期对应50Hz刷新率 */ htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim1); sConfigOC.Pulse 1500; /* 上电先输出中位脉宽防止电机猛转 */ sConfigOC.OCMode TIM_OCMODE_PWM1; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); } void bsp_motor_set_us(uint16_t us_left, uint16_t us_right) { if (us_left 1000) us_left 1000; /* 防止越界烧电调 */ if (us_left 2000) us_left 2000; if (us_right 1000) us_right 1000; if (us_right 2000) us_right 2000; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, us_left); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, us_right); }这段代码里Prescaler 168 - 1是假设定时器时钟168MHz分频后得到1MHz计数频率即每1微秒计数一次Period 20000 - 1让20毫秒内计满20000次输出频率就是50Hz。两个通道分别对应左、右推进器这种双通道直出方案比外接舵机控制板少一层延迟。上电先输出1500微秒中位脉宽是为了让电调完成上电初始化不少电调要求先检测到中位信号否则拒绝响应油门。输出前的限幅是为了避免程序跑飞给出超过1000~2000微秒的脉宽直接全油门。无人船和无人车在映射逻辑上有两处不同。一是无人船倒车响应慢转向靠转速差控制量稍微大一点就偏航所以左右差值小于某一阈值时应当做死区处理直接按零输出二是船用螺旋桨正转推进、反转倒车的非对称特性反转时的推力明显小于正转靠同一个PID参数同时管正反转会出现低速抖动。常见做法是在下位机里加一张转速-脉宽映射表把非线性修正放在表里上位机只管下发目标航向和速度。另外如果电机不是电调而是RS485伺服电机就不走PWM波改走串口或485STM32要控制DE/RE方向引脚发送时拉高、接收时拉低波特率、协议号和电机地址都要带在指令里。这种方案常用于无人船的方向舵代码量不大但错误率极高多半是方向引脚时序不对建议一帧一帧调。2.3 传感器数据打包与DMA发送下位机向上位机回报的数据一般有IMU姿态、GPS经纬度、电池电压和电机电流。GPS低频单独成帧IMU按固定周期10ms或20ms发。帧格式是上下位机的合同我建议做成这样字段长度(字节)内容帧头2固定 0xAA 0x55长度1负载字节数类型10x01 传感器0x02 电机状态0x03 配置负载N按类型解析的数据体校验1对长度类型负载做异或打包的C代码放在protocol/frame.c与具体的传感器采样解耦/* protocol/frame.c —— 通用帧打包使用异或校验 */ #include string.h #include frame.h uint8_t protocol_build(uint8_t msg_type, const uint8_t *payload, uint16_t len, uint8_t *out) { uint16_t i; uint8_t xor msg_type ^ (uint8_t)len; out[0] 0xAA; out[1] 0x55; out[2] (uint8_t)len; out[3] msg_type; for (i 0; i len; i) { out[4 i] payload[i]; xor ^ payload[i]; } out[4 len] xor; /* 校验覆盖类型字段防止类型错位 */ return (uint8_t)(4 len 1); }校验从类型字节开始算因为帧头固定上位机通过状态机靠同步字节找边界但类型字段决定后续数据怎么解析必须纳入校验。打包完成后的发送建议用DMAHAL_UART_Transmit_DMA(huart1, buf, len);CPU可以继续跑IMU采集不会被逐字节发送卡住几百微秒。如果DMA发送和主循环写同一个缓冲区要么用双缓冲要么在发送前用__HAL_UART_DMA_GET_FLAG(huart1, huart1.hdmatx-Instance, DMA_FLAG_TC)确认上一次发送完成。3. 树莓派与Jetson Nano上位机用C源码读写串口与解析协议3.1 打开串口的标准写法与设备名差异上位机的核心职责是两条把算法算出的航向、速度指令发下去把STM32回报的姿态和电量收上来。树莓派4B和Jetson Nano以及Jetson Orin Nano在Linux下打开串口的方式完全一致设备名有差异而已。树莓派板载UART默认是/dev/ttyAMA0如果开了蓝牙可能变成/dev/ttyS0USB转TTL模块一般是/dev/ttyUSB0。Jetson Nano的40pin上外接USB转串口也是/dev/ttyUSB0板载的则在/dev/ttyTHS1。判断方法插上USB转串口后执行dmesg | tail -20看到cp210x或ch341就是典型的USB转TTL芯片。Jetson Nano有一个和树莓派不一样的坑官方镜像里/dev/ttyTHS1默认被nvgetty控制台服务占用。直接打开会拿到权限错误或者乱码先停掉服务sudo systemctl stop nvgetty sudo systemctl disable nvgetty sudo reboot/* upper/main.c —— 打开串口阻塞方式读取 */ #include stdio.h #include fcntl.h #include termios.h #include unistd.h int uart_open(const char *dev, speed_t baud) { int fd open(dev, O_RDWR | O_NOCTTY | O_NDELAY); if (fd 0) { perror(open); return -1; } struct termios tty; if (tcgetattr(fd, tty) ! 0) { perror(tcgetattr); return -1; } cfsetispeed(tty, baud); cfsetospeed(tty, baud); tty.c_cflag ~CSIZE; tty.c_cflag | CS8; tty.c_cflag ~PARENB; tty.c_cflag ~CSTOPB; tty.c_cflag | CLOCAL | CREAD; tty.c_cc[VMIN] 1; tty.c_cc[VTIME] 0; tcsetattr(fd, TCSANOW, tty); return fd; }参数说明CLOCAL | CREAD是Linux下使用串口的标配不加可能读不到数据VMIN 1表示至少读到1个字节才返回这样下面的解析状态机可以一字节一字节地推进VTIME 0表示不设超时。O_NDELAY只是让open()不阻塞真正控制读行为的是VMIN和VTIME很多新手在这里混淆。需要修改波特率时把B115200换成B460800要同步检查STM32端的USART分频误差高速率下误差容忍度更低。3.2 读取线程与协议状态机上位机的串口读取必须放在独立线程里。原因很简单无人船的路径规划、Jetson Nano上的图像推理可能耗时几十毫秒到几百毫秒如果串口读写和这些逻辑串在一起一帧姿态数据会被推迟几秒处理控制周期直接失效。下位机那头的姿态解算再快也没用。/* upper/parser.c —— 逐字节状态机解析 0xAA 0x55 帧 */ enum { ST_HEAD1, ST_HEAD2, ST_LEN, ST_TYPE, ST_PAYLOAD, ST_XOR }; int uart_parse_byte(uint8_t b, uint8_t *len_buf, uint8_t *type_buf, uint8_t *payload, uint8_t *payload_len) { static uint8_t state ST_HEAD1; static uint8_t idx 0; static uint8_t xor 0; switch (state) { case ST_HEAD1: if (b 0xAA) state ST_HEAD2; break; case ST_HEAD2: if (b 0x55) state ST_LEN; else state ST_HEAD1; /* 错位就回退到等帧头 */ break; case ST_LEN: *len_buf b; xor b; idx 0; state ST_TYPE; break; case ST_TYPE: *type_buf b; xor ^ b; state ST_PAYLOAD; break; case ST_PAYLOAD: payload[idx] b; xor ^ b; if (idx *len_buf) state ST_XOR; break; case ST_XOR: state ST_HEAD1; return (b xor) ? 1 : 0; /* 校验通过才返回整帧完成 */ } return 0; }这个状态机的关键在于回退到找帧头实际串口线上噪声很多如果第二字节不是0x55不要回到ST_HEAD2继续等0x55而是回到ST_HEAD1重新找0xAA。否则一帧错位后容易把前面残留的旧字节误认为新帧头。read()返回后把每个字节都送入这个函数返回1表示收到一帧完整数据。注意payload缓冲区长度要和协议里定义的最大帧长一致下位机改了帧长度上位机这里要同步改否则会越界写。上位机向STM32发送的指令通常很短但一次要写完。比如设定航向uint8_t cmd[8]; cmd[0] 0xAA; cmd[1] 0x55; cmd[2] 2; cmd[3] 0x03; /* 0x03 表示航向指令 */ cmd[4] yaw_deg 8; /* 大端序 */ cmd[5] yaw_deg 0xFF; cmd[6] cmd[2] ^ cmd[3] ^ cmd[4] ^ cmd[5]; write(fd, cmd, 7);byte order使用大端序是为了和STM32端(uint16_t)(buf[4] 8 | buf[5])一致。一次write写完7字节不要拆成多次写因为STM32的解析同样按字节流处理拆开可能把两次指令粘在一起。3.3 树莓派4B与Jetson Nano在GPIO和PWM上的差异上位机除了串口通常还要控制指示灯、蜂鸣器、继电器或给STM32一个复位信号。树莓派和Jetson Nano的40pin物理排布接近但引脚编号并不通用。代码层面两侧都已经用libgpiod取代老的sysfs接口。对比项树莓派4BJetson Nano常用串口设备/dev/ttyAMA0、/dev/ttyUSB0/dev/ttyTHS1、/dev/ttyUSB0硬件PWMGPIO18PWM0引脚33PWM0GPIO控制接口libgpiodlibgpiod串口用户组dialoutdialout板载AI推理有限CUDA加速更合适树莓派4B的GPIO18支持硬件PWM输出适合做呼吸灯或报警器Jetson Nano的硬件PWM在物理引脚33其余引脚要软件模拟PWM。软件PWM在无人船上最好只用于指示灯别用来驱动舵机它的抖动会让舵机发热甚至堵转。树莓派还有一个绕不过去的坑默认用户不在dialout组时打开/dev/ttyUSB0会报权限错误。sudo usermod -a -G dialout $USER # 把当前用户加入串口组 sudo reboot # 重新登录后生效如果接入的是/dev/ttyACM0STM32的USB虚拟串口插入后执行dmesg | tail确认是否识别为cdc_acm。Windows下如果看到STM32 Virtual COM Port叹号多半是USB描述符和系统不匹配换一根短USB线、换个口重插或者用STM32 ST-LINK Utility检查USB配置。说到树莓派如何解除外设功率输出限制这个搜索词背后其实是GPIO直接驱动负载导致电压被拉低。树莓派每个GPIO能提供的电流很小驱动继电器、全彩LED灯带要加三极管或MOS管正确的解除功率限制方向不是改config.txt里的电流上限而是给外设独立供电GPIO只做开关信号。Jetson Nano同样如此它的5V引脚负载能力受输入DC电源限制直接带高功率舵机会把板子供电拖垮。4. 联调排错STM32晶振电容、波特率误差与串口丢帧4.1 晶振电容计算与时钟误差的连锁反应标题带基于STM32的工程很多人把精力花在代码上忽略物理层。晶振是STM32系统稳定性的第一道坎。最常见的8MHz晶振两端各接一枚负载电容典型值18~22pF但正确的算法要根据晶振的负载电容来算主控引脚电容和PCB寄生电容大约4~6pF假定晶振负载电容为12pF则C1 C2 2 × CL - Cstray ≈ 2 × 12 - 5 19pF取最近的标称值通常就是18pF或20pF。如果换用30pF的直插电容频率偏移会显著增大直接影响串口波特率。波特率误差的连锁反应STM32的USART波特率由外设时钟和分频决定系统时钟偏了0.5%115200波特率的误差不到600bps勉强能通偏差超过2%就开始出现首字节错、间歇性乱码。这和一种现象高度吻合在串口调试助手里数据偶尔错乱重开又好了。排查时先用stty -F /dev/ttyUSB0 115200确认当前速率再在STM32端用STM32 ST-LINK Utility连接读取RCC-CFGR看实际时钟配置。4.2 串口丢帧的四个常见原因与对策联调阶段最常见的命令发了没反应姿态数据卡住几秒又恢复原因基本逃不出下面几类。处理次序也按优先级排现象常见原因对策偶发乱码波特率误差或晶振偏频用示波器测PWM/串口波形核对时钟树长时间静默后恢复USB转TTL接收缓冲溢出上位机read循环要快或调大termios缓冲一帧变成两帧下位机两次DMA发送间隔过长发送优先级提高到中断级或使用DMA双缓冲上电乱码之后正常树莓派与动力电池不共地两套电源GND短接或使用隔离串口补充一句无人车/无人船的电机是大干扰源大电流瞬间会让地电位跳动。如果树莓派用5V电源、动力电池是12V或7.4V两套系统的GND必须连在一起否则串口电平参考点不一致起始位被判错表现就是油门一推数据就乱。针对偶发丢帧先用Linux自带命令抓原始数据判断是发端还是收端问题stty -F /dev/ttyUSB0 115200 raw -echo timeout 5 cat /dev/ttyUSB0 | xxd -g1 | head -40看到帧头0xAA 0x55被频繁截断说明对端发送节奏不稳如果连帧头都收不到先检查接线和共地。这是文档和使用说明里最该附的检验步骤之一。4.3 调试连接与虚拟串口的常见故障树莓派4B红灯常亮、绿灯不亮这类现象和USB转串口其实是两套电源体系但如果上位机串口没起来很多人会误判为树莓派坏了。排查时要区分LED状态与串口枚举插入USB转TTL后先执行lsusb dmesg | grep -E tty|usb|ch341|cp210x能列出设备但open()报错检查权限列不出设备换线换口。STM32端连接调试器时常见error: no stm32 target found! if your product embeds debug authentication出现在ST-LINK无法连接目标芯片的场景。两个主要原因SWDIO/SWCLK没接全或板子没上电以及调试口被意外关闭。无人船控制器上如果把SWD引脚复用成了普通GPIO调试器就连不上解决方式是在STM32 ST-LINK Utility里选Connect under Reset模式重试。如果芯片开了读保护还要先做整片擦除或解除保护这一步会清掉Flash里的程序操作前确认固件有备份。STM32的虚拟串口在Linux下通常识别成/dev/ttyACM0不需要额外驱动但如果之前烧过其他固件USB描述符里的PID/VID已经变化Linux会把它识别成未知设备需要重烧USB库对应固件才能恢复枚举。5. 文档说明与使用说明里最容易被忽略的验证技巧5.1 文档说明该有的五类内容拿到一套STM32树莓派/Jetson的C源码包先不看代码先看文档里这五类内容引脚分配、通信参数、协议帧、编译环境、已知问题。很多无人船/无人车项目要改造型比如把树莓派换成Jetson Orin Nano首先要确认的就是串口设备名和GPIO编号有没有在文档里列出来。文档模块必须写清的内容用于验证的程序行为引脚定义每个外设的GPIO、定时器通道、串口号对照CubeMX Pinout查复用冲突通信参数波特率、数据位、校验位、流控stty参数与代码里termios一致协议帧帧头、类型字段、校验方式、字节序用od/xxd抓包对比编译环境编译器版本、HAL固件包版本、芯片型号减少我这编译不过的沟通成本已知问题时序坑、不能同时打开的外设联调时按图避坑5.2 用空跑流程验证接线与协议使用说明里最有价值的不是怎么编译而是空跑流程不插动力电池、不接螺旋桨只给逻辑电源让上位机发一个很小的油门值观察电机是否缓慢转动、传感器数据是否更新。这一步能排除至少一半接线问题。验证脚本可以直接看原始字节# 下位机上电后持续打印原始串口帧 timeout 10 od -A x -t x1z /dev/ttyUSB0能看到aa 55开头的规律帧说明下位机在正常发送全是ff或空白先查共地与波特率。5.3 12小时拷机日志与自动复位无人船下水后不可能现场开虚拟终端上位机程序里要留一个看门狗加自恢复的路径。常用技巧是把串口读不到有效帧的数量累计起来超过阈值就通过树莓派的一个GPIO拉低STM32复位脚让下位机重新初始化。这个功能要在使用说明里写清楚防止现场误判为整机故障。最后留一个拷机脚本验证长期稳定性#!/bin/bash # usage: ./uart_stress.sh /dev/ttyUSB0 3600 DEV${1:-/dev/ttyUSB0} DUR${2:-3600} LOGuart_$(date %s).log while [ $SECONDS -lt $DUR ]; do od -A n -t x1 -v $DEV 2/dev/null | tr -d \n | grep -o aa55 | wc -l $LOG sleep 5 done脚本每5秒抓一次串口原始数据统计aa55帧头出现次数并写入日志。拷机结束后用sort -k1 -n看哪一段时长帧头计数明显下降就能把问题定位到是上位机阻塞还是下位机死机这比通电看灯有效得多。本文还有配套的精品资源点击获取
返回列表