1.9 串口绘图 - 实验篇

  • 利用 UART, 单片机向电脑端发送数据, 实现指定函数值绘制并显示灯状态
  • 利用 UART 电脑端向单片机发送指令让单片机点亮 LED

LED1: PG1, 原理图中低电平点亮 UART2: PD5 PD6, 在 A 板预留蓝牙接口Pasted image 20260711000051

时钟树 HSE 配置为 12M 不要忘记


CubeMX

配置LED1: PG1Pasted image 20260710232619 配置 UART2, 记得开 DMAPasted image 20260710232808

Pasted image 20260710232925 生成代码


代码

把我们要用到的板级支持包 User目录从 GitHub 仓库拷过来.

记得做如下修改Pasted image 20260710234909

导入库

1
2
3
4
5
6
/* Private includes ----------------------------------------------------------*/  
/* USER CODE BEGIN Includes */
#include <string.h>
#include "drv_bsp.h"
#include "drv_uart.h"
/* USER CODE END Includes */

定义用户变量

1
2
3
4
5
6
/* Private variables ---------------------------------------------------------*/  

/* USER CODE BEGIN PV */
uint8_t tx_buffer[12];
uint8_t rx_buffer[10];
/* USER CODE END PV */

写回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* USER CODE BEGIN PFP */  
/**
* @brief HAL 库 UART 接收 DMA 空闲中断
* @param Buffer UART编号
* @param Length 长度
*/void Serialplot_Call_Back(uint8_t *Buffer, uint16_t Length)
{
if (rx_buffer[0] == '0')
{
BSP_LED_1(BSP_LED_Status_DISABLED);
}
else if (rx_buffer[0] == '1')
{
BSP_LED_1(BSP_LED_Status_ENABLED);
}
else if (rx_buffer[0] == '2')
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
}
/* USER CODE END PFP */

初始化 UART2

1
2
3
/* USER CODE BEGIN 2 */  
Uart_Init(&huart2, rx_buffer, 10, Serialplot_Call_Back);
/* USER CODE END 2 */

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Infinite loop */  
/* USER CODE BEGIN WHILE */
while (1)
{
static uint32_t flag;
if (flag == 2500)
{
flag = 0;
}
float tmp_data = ((float)flag / 1000.0f) * ((float)flag / 1000.0f);
float led_status = !HAL_GPIO_ReadPin(LED1_GPIO_Port, LED1_Pin);

memcpy(&tx_buffer[0], &tmp_data, 4);
memcpy(&tx_buffer[4], &led_status, 4);

// 帧尾:00 00 80 7F
tx_buffer[8] = 0x00;
tx_buffer[9] = 0x00;
tx_buffer[10] = 0x80;
tx_buffer[11] = 0x7F;

UART_Send_Data(&huart2, tx_buffer, 12);

flag++;
HAL_Delay(0);

/* USER CODE END WHILE */

串口绘图软件我推荐使用SerialPlot, 但是这个只有 Windows 版本, 如果在 mac 上使用你要下载源码自己build 一下, 不想这么麻烦的话 mac 上也可以使用功能更全的跨平台的Serial Studio

来到串口绘图软件, 如果实在没有的话可以使用网页版 https://captainkaz.github.io/web-serial-plotter/

使用 Serial Studio 的话因为它没有自带的解析二进制浮点数的模式所以我们要手动写一个 js 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function parse(frame) {
// ===== 配置区 =====
var CHANNELS = 4; // 通道数
var LITTLE_ENDIAN = true; // true=小端(STM32), false=大端
var BYTES_PER_VAL = 4; // float=4, double=8
var HEADER_BYTES = 0; // 帧头字节数,没有填 0
var FOOTER_BYTES = 4; // 帧尾字节数,没有填 0
var HEADER_VALUE = []; // 帧头字节值,不校验填 []
var FOOTER_VALUE = [0x00,0x00,0x80,0x7F]; // 帧尾字节值,不校验填 []
// ==================

var dataBytes = CHANNELS * BYTES_PER_VAL;
var needBytes = HEADER_BYTES + dataBytes + FOOTER_BYTES;
if (!frame || frame.length < needBytes) return [];

if (HEADER_BYTES > 0 && HEADER_VALUE.length === HEADER_BYTES) {
for (var h = 0; h < HEADER_BYTES; h++) {
if (frame[h] !== HEADER_VALUE[h]) return [];
}
}

if (FOOTER_BYTES > 0 && FOOTER_VALUE.length === FOOTER_BYTES) {
var footStart = HEADER_BYTES + dataBytes;
for (var f = 0; f < FOOTER_BYTES; f++) {
if (frame[footStart + f] !== FOOTER_VALUE[f]) return [];
}
}

function readValue(arr, off, n, le) {
var bytes = new Uint8Array(n);
for (var k = 0; k < n; k++) bytes[k] = arr[off + k];
var dv = new DataView(bytes.buffer);
return (n === 8) ? dv.getFloat64(0, le) : dv.getFloat32(0, le);
}

var result = [];
for (var i = 0; i < CHANNELS; i++) {
result.push(readValue(frame, HEADER_BYTES + i * BYTES_PER_VAL, BYTES_PER_VAL, LITTLE_ENDIAN));
}
return result;
}

Pasted image 20260712165739

如果使用这个网页版的话自定义解析器插入这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ===== 配置区(只改这里) =====
var CHANNELS = 4; // 通道数
var LITTLE_ENDIAN = true; // true=小端(STM32), false=大端
var BYTES_PER_VAL = 4; // float=4, double=8
var HEADER_BYTES = 0; // 帧头字节数,没有填 0
var FOOTER_BYTES = 4; // 帧尾字节数,没有填 0
var HEADER_VALUE = []; // 帧头字节值,不校验填 []
var FOOTER_VALUE = [0x00,0x00,0x80,0x7F]; // 帧尾字节值,不校验填 []
// ==================

var dataBytes = CHANNELS * BYTES_PER_VAL;
var FRAME_LEN = HEADER_BYTES + dataBytes + FOOTER_BYTES;

if (uint8ArrayData.length < FRAME_LEN) {
return { values: null, frameByteLength: 0 };
}

if (HEADER_BYTES > 0 && HEADER_VALUE.length === HEADER_BYTES) {
for (var h = 0; h < HEADER_BYTES; h++) {
if (uint8ArrayData[h] !== HEADER_VALUE[h]) {
return { values: null, frameByteLength: 1 };
}
}
}

if (FOOTER_BYTES > 0 && FOOTER_VALUE.length === FOOTER_BYTES) {
var footStart = HEADER_BYTES + dataBytes;
for (var f = 0; f < FOOTER_BYTES; f++) {
if (uint8ArrayData[footStart + f] !== FOOTER_VALUE[f]) {
return { values: null, frameByteLength: 1 };
}
}
}

var dv = new DataView(
uint8ArrayData.buffer,
uint8ArrayData.byteOffset,
uint8ArrayData.byteLength
);

function readValue(off, n, le) {
return (n === 8) ? dv.getFloat64(off, le) : dv.getFloat32(off, le);
}

var values = [];
for (var i = 0; i < CHANNELS; i++) {
values.push(readValue(HEADER_BYTES + i * BYTES_PER_VAL, BYTES_PER_VAL, LITTLE_ENDIAN));
}
return { values: values, frameByteLength: FRAME_LEN };

Pasted image 20260711010809 整活 Pasted image 20260711015905