编程技术分享

分享编程知识,探讨技术创新

0%

简介:此项目为NodeMCU-32S开发板的拓展板,PCA9685驱动16路舵机,TB6612FNG驱动4路直流电机。此版本采用芯片与电机供电分离的设计,即需要双电源。电机电源为6.4V磷酸铁锂动力电池组。

好的,作为一名高级嵌入式软件开发工程师,我将为您详细介绍这个基于NodeMCU-32S拓展板的嵌入式系统的代码设计架构,并提供具体的C代码实现方案。这个项目旨在构建一个可靠、高效且可扩展的平台,用于控制舵机和直流电机,适用于各种机器人或自动化应用。
关注微信公众号,提前获取相关推文

1. 系统架构设计

为了构建一个清晰、模块化、易于维护和扩展的系统,我将采用分层架构的设计模式。分层架构将系统分解为不同的层次,每个层次负责特定的功能,并与相邻的层次进行交互。这种架构方式能够有效地解耦各个模块,提高代码的可重用性和可维护性。

本项目系统架构将分为以下几个层次:

  • 硬件抽象层 (HAL, Hardware Abstraction Layer): 这是最底层,直接与硬件交互。HAL层封装了对底层硬件的具体操作,例如GPIO控制、I2C通信、PWM输出等。上层模块通过HAL提供的统一接口来访问硬件,无需关心底层硬件的具体实现细节。这样做的好处是,当底层硬件发生改变时(例如更换了不同的GPIO引脚),只需要修改HAL层的代码,而上层模块的代码无需修改,提高了代码的可移植性。

  • 驱动层 (Driver Layer): 驱动层构建在HAL层之上,负责特定硬件设备(例如PCA9685舵机驱动芯片、TB6612FNG电机驱动芯片)的驱动逻辑。驱动层将HAL层提供的基本硬件操作接口组合起来,实现对特定硬件设备的高级控制功能。例如,PCA9685驱动模块会封装I2C通信协议,实现舵机的PWM信号生成和控制;TB6612FNG驱动模块会封装GPIO控制和PWM输出,实现直流电机的速度和方向控制。

  • 控制层 (Control Layer): 控制层构建在驱动层之上,负责实现系统的核心控制逻辑。例如,舵机控制模块负责根据指令计算出每个舵机的目标角度,并调用舵机驱动模块来控制舵机运动;电机控制模块负责根据指令控制电机的速度和方向,实现机器人的运动控制。控制层还会实现一些高级控制算法,例如运动学、逆运动学、步态规划等,以实现更复杂的机器人运动功能。

  • 应用层 (Application Layer): 应用层是最高层,直接面向用户或者其他系统。应用层负责接收用户的指令或外部系统的命令,并将指令传递给控制层进行处理。应用层还会负责系统的初始化、配置管理、状态监控、错误处理等功能。例如,应用层可以实现一个Wi-Fi控制接口,接收来自手机App或上位机软件的控制指令,然后将指令解析并传递给控制层。

  • 公共服务层 (Common Service Layer): 这是一个可选的辅助层,可以提供一些通用的服务功能,例如日志记录、时间管理、内存管理、配置管理等。这些服务可以被其他各个层次的模块调用,提高代码的复用性和系统的整体功能。

系统架构图示:

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
+---------------------+
| 应用层 (Application Layer) | (用户界面, 命令解析, 系统管理)
+---------------------+
|
| (控制指令, 状态反馈)
V
+---------------------+
| 控制层 (Control Layer) | (舵机控制, 电机控制, 运动算法)
+---------------------+
|
| (硬件控制指令)
V
+---------------------+
| 驱动层 (Driver Layer) | (PCA9685驱动, TB6612FNG驱动)
+---------------------+
|
| (硬件操作请求)
V
+---------------------+
| 硬件抽象层 (HAL Layer) | (GPIO, I2C, PWM, 定时器)
+---------------------+
|
| (直接硬件访问)
V
+---------------------+
| 硬件 (Hardware) | (NodeMCU-32S, PCA9685, TB6612FNG, 电机, 舵机)
+---------------------+

可选的公共服务层可以被各个层次调用。

2. 代码设计细节

接下来,我将详细介绍每个层次的代码设计,并提供具体的C代码示例。为了代码的组织性和可读性,我将采用模块化的设计方法,将每个层次和每个功能模块都封装成独立的C文件和头文件。

2.1. 硬件抽象层 (HAL Layer)

HAL层主要负责封装ESP32硬件平台的底层操作接口。由于本项目使用NodeMCU-32S开发板,底层硬件操作主要通过ESP-IDF (Espressif IoT Development Framework) 或 Arduino ESP32 库来实现。HAL层需要提供以下功能:

  • GPIO控制: 配置GPIO引脚为输入或输出模式,读取GPIO输入状态,设置GPIO输出状态。
  • I2C通信: 初始化I2C总线,发送和接收I2C数据。
  • PWM输出: 初始化PWM通道,设置PWM频率和占空比。
  • 定时器: 初始化定时器,设置定时器中断。
  • 延时函数: 提供毫秒级和微秒级延时函数。

HAL层头文件 (hal.h):

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#ifndef HAL_H
#define HAL_H

#include <stdint.h>
#include <stdbool.h>

// GPIO 定义
typedef enum {
GPIO_PIN_0,
GPIO_PIN_1,
GPIO_PIN_2,
// ... 定义所有需要用到的GPIO引脚
GPIO_PIN_MAX
} gpio_pin_t;

typedef enum {
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT
} gpio_mode_t;

typedef enum {
GPIO_LEVEL_LOW,
GPIO_LEVEL_HIGH
} gpio_level_t;

// I2C 定义
typedef enum {
I2C_PORT_0,
I2C_PORT_1,
I2C_PORT_MAX
} i2c_port_t;

// PWM 定义
typedef enum {
PWM_CHANNEL_0,
PWM_CHANNEL_1,
// ... 定义所有需要用到的PWM通道
PWM_CHANNEL_MAX
} pwm_channel_t;

// 初始化 GPIO
void hal_gpio_init(gpio_pin_t pin, gpio_mode_t mode);
// 设置 GPIO 输出电平
void hal_gpio_set_level(gpio_pin_t pin, gpio_level_t level);
// 读取 GPIO 输入电平
gpio_level_t hal_gpio_get_level(gpio_pin_t pin);

// 初始化 I2C
bool hal_i2c_init(i2c_port_t port, uint32_t clock_speed);
// 发送 I2C 数据
bool hal_i2c_write_bytes(i2c_port_t port, uint8_t address, const uint8_t *data, size_t data_len);
// 读取 I2C 数据
bool hal_i2c_read_bytes(i2c_port_t port, uint8_t address, uint8_t *data, size_t data_len);

// 初始化 PWM
bool hal_pwm_init(pwm_channel_t channel, uint32_t frequency);
// 设置 PWM 占空比 (0-100%)
bool hal_pwm_set_duty_cycle(pwm_channel_t channel, uint32_t duty_cycle_percent);
// 设置 PWM 频率
bool hal_pwm_set_frequency(pwm_channel_t channel, uint32_t frequency);

// 延时函数 (毫秒)
void hal_delay_ms(uint32_t ms);
// 延时函数 (微秒)
void hal_delay_us(uint32_t us);

#endif // HAL_H

HAL层实现文件 (hal.c) - 基于 ESP-IDF 示例:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include "hal.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/ledc.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// GPIO 初始化
void hal_gpio_init(gpio_pin_t pin, gpio_mode_t mode) {
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.pin_bit_mask = (1ULL << pin); // 使用宏将 pin_t 转换为 GPIO 编号
io_conf.pull_down_en = 0;
io_conf.pull_up_en = 0;
if (mode == GPIO_MODE_INPUT) {
io_conf.mode = GPIO_MODE_INPUT;
} else {
io_conf.mode = GPIO_MODE_OUTPUT;
}
gpio_config(&io_conf);
}

// 设置 GPIO 输出电平
void hal_gpio_set_level(gpio_pin_t pin, gpio_level_t level) {
gpio_set_level(pin, level); // 直接使用 GPIO 编号
}

// 读取 GPIO 输入电平
gpio_level_t hal_gpio_get_level(gpio_pin_t pin) {
return gpio_get_level(pin); // 直接使用 GPIO 编号
}

// I2C 初始化
bool hal_i2c_init(i2c_port_t port, uint32_t clock_speed) {
i2c_config_t conf;
conf.mode = I2C_MODE_MASTER;
conf.sda_io_num = I2C_MASTER_SDA_IO; // 定义 SDA 引脚
conf.scl_io_num = I2C_MASTER_SCL_IO; // 定义 SCL 引脚
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
conf.master.clk_speed = clock_speed;
esp_err_t ret = i2c_param_config(port, &conf);
if (ret != ESP_OK) {
return false;
}
ret = i2c_driver_install(port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
return (ret == ESP_OK);
}

// 发送 I2C 数据
bool hal_i2c_write_bytes(i2c_port_t port, uint8_t address, const uint8_t *data, size_t data_len) {
i2c_master_write_to_device(port, address, data, data_len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
// TODO: 错误处理和返回值判断
return true; // 简化示例,实际需要错误处理
}

// 读取 I2C 数据
bool hal_i2c_read_bytes(i2c_port_t port, uint8_t address, uint8_t *data, size_t data_len) {
i2c_master_read_from_device(port, address, data, data_len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
// TODO: 错误处理和返回值判断
return true; // 简化示例,实际需要错误处理
}

// PWM 初始化
bool hal_pwm_init(pwm_channel_t channel, uint32_t frequency) {
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.duty_resolution = LEDC_TIMER_13_BIT, // 13位分辨率 (8192 步)
.timer_num = channel, // PWM 通道即定时器编号
.freq_hz = frequency,
.clk_cfg = LEDC_AUTO_CLK,
};
esp_err_t ret = ledc_timer_config(&timer_conf);
if (ret != ESP_OK) {
return false;
}

ledc_channel_config_t channel_conf = {
.channel = channel,
.duty = 0, // 初始占空比为 0%
.gpio_num = PWM_OUTPUT_GPIO, // 定义 PWM 输出引脚
.speed_mode = LEDC_HIGH_SPEED_MODE,
.hpoint = 0,
.timer_sel = channel,
};
ret = ledc_channel_config(&channel_conf);
return (ret == ESP_OK);
}

// 设置 PWM 占空比 (0-100%)
bool hal_pwm_set_duty_cycle(pwm_channel_t channel, uint32_t duty_cycle_percent) {
if (duty_cycle_percent > 100) duty_cycle_percent = 100;
uint32_t duty_cycle = (duty_cycle_percent * 8192) / 100; // 计算占空比值 (13位分辨率)
esp_err_t ret = ledc_set_duty(LEDC_HIGH_SPEED_MODE, channel, duty_cycle);
if (ret != ESP_OK) return false;
ret = ledc_update_duty(LEDC_HIGH_SPEED_MODE, channel);
return (ret == ESP_OK);
}

// 设置 PWM 频率 (如果需要动态调整频率)
bool hal_pwm_set_frequency(pwm_channel_t channel, uint32_t frequency) {
esp_err_t ret = ledc_set_freq(LEDC_HIGH_SPEED_MODE, channel, frequency);
return (ret == ESP_OK);
}


// 延时函数 (毫秒)
void hal_delay_ms(uint32_t ms) {
vTaskDelay(ms / portTICK_PERIOD_MS);
}

// 延时函数 (微秒) - 使用 ESP-IDF 的 esp_timer
void hal_delay_us(uint32_t us) {
esp_timer_delay(us);
}

注意:

  • 上面的 hal.c 代码示例是基于 ESP-IDF 框架的,如果您使用 Arduino ESP32 库,需要将相应的 ESP-IDF 函数替换为 Arduino 函数,例如 gpio_config() 替换为 pinMode()gpio_set_level() 替换为 digitalWrite(),等等。
  • I2C_MASTER_SDA_IO, I2C_MASTER_SCL_IO, PWM_OUTPUT_GPIO, I2C_MASTER_TIMEOUT_MS 等宏定义需要在实际项目中根据硬件连接和需求进行定义。例如,可以在一个 config.h 文件中定义这些宏。
  • HAL层需要根据项目实际使用的硬件外设进行扩展,例如添加 SPI、UART、ADC、DAC 等接口的封装。
  • 错误处理部分在示例代码中被简化,实际项目中需要完善错误处理机制,例如检查函数返回值,返回错误码,记录错误日志等。

2.2. 驱动层 (Driver Layer)

驱动层构建在HAL层之上,负责驱动 PCA9685 舵机驱动芯片和 TB6612FNG 电机驱动芯片。

2.2.1. PCA9685 舵机驱动模块

PCA9685 是一款 16 通道 12 位 PWM 控制器,通过 I2C 接口进行通信。舵机驱动模块需要实现以下功能:

  • 初始化 PCA9685: 配置 PCA9685 的 I2C 地址,设置 PWM 频率。
  • 设置舵机角度: 根据舵机通道和目标角度,计算出对应的 PWM 占空比,并通过 I2C 写入 PCA9685 的寄存器。
  • 校准舵机: 提供舵机角度和 PWM 占空比之间的映射关系校准功能,因为不同舵机的物理特性可能略有差异。

PCA9685 驱动头文件 (pca9685.h):

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
#ifndef PCA9685_H
#define PCA9685_H

#include <stdint.h>
#include <stdbool.h>
#include "hal.h"

#define PCA9685_DEFAULT_ADDRESS 0x40 // 默认 I2C 地址

typedef enum {
PCA9685_CHANNEL_0,
PCA9685_CHANNEL_1,
PCA9685_CHANNEL_2,
PCA9685_CHANNEL_3,
PCA9685_CHANNEL_4,
PCA9685_CHANNEL_5,
PCA9685_CHANNEL_6,
PCA9685_CHANNEL_7,
PCA9685_CHANNEL_8,
PCA9685_CHANNEL_9,
PCA9685_CHANNEL_10,
PCA9685_CHANNEL_11,
PCA9685_CHANNEL_12,
PCA9685_CHANNEL_13,
PCA9685_CHANNEL_14,
PCA9685_CHANNEL_15,
PCA9685_CHANNEL_MAX
} pca9685_channel_t;

// 初始化 PCA9685
bool pca9685_init(i2c_port_t port, uint8_t address);
// 设置 PWM 频率 (40Hz - 1000Hz, 舵机通常使用 50Hz)
bool pca9685_set_pwm_frequency(uint8_t address, float frequency);
// 设置单个通道的 PWM 脉冲宽度 (单位: us)
bool pca9685_set_pwm_pulse_width(uint8_t address, pca9685_channel_t channel, uint16_t pulse_width_us);
// 设置单个通道的 PWM 占空比 (0-4095, 12位分辨率)
bool pca9685_set_pwm_duty_cycle(uint8_t address, pca9685_channel_t channel, uint16_t duty_cycle);
// 设置舵机角度 (0-180度, 需要校准)
bool pca9685_set_servo_angle(uint8_t address, pca9685_channel_t channel, float angle);

#endif // PCA9685_H

PCA9685 驱动实现文件 (pca9685.c):

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include "pca9685.h"
#include <math.h>

// PCA9685 寄存器地址
#define PCA9685_MODE1 0x00
#define PCA9685_MODE2 0x01
#define PCA9685_SUBADR1 0x02
#define PCA9685_SUBADR2 0x03
#define PCA9685_SUBADR3 0x04
#define PCA9685_ALLCALLADR 0x05
#define PCA9685_LED0_ON_L 0x06
#define PCA9685_LED0_ON_H 0x07
#define PCA9685_LED0_OFF_L 0x08
#define PCA9685_LED0_OFF_H 0x09
// ... LED1-LED15 寄存器地址类似 ...
#define PCA9685_ALL_LED_ON_L 0xFA
#define PCA9685_ALL_LED_ON_H 0xFB
#define PCA9685_ALL_LED_OFF_L 0xFC
#define PCA9685_ALL_LED_OFF_H 0xFD
#define PCA9685_PRESCALE 0xFE

#define PCA9685_RESTART 0x80
#define PCA9685_SLEEP 0x10
#define PCA9685_ALLCALL 0x01
#define PCA9685_OUTDRV 0x04

#define I2C_PORT I2C_PORT_0 // 使用 I2C 端口 0 (根据实际硬件连接修改)
#define PCA9685_PWM_FREQ_DEFAULT 50 // 默认 PWM 频率 50Hz

// 写入 PCA9685 寄存器
static bool pca9685_write_reg(uint8_t address, uint8_t reg, uint8_t value) {
uint8_t data[2] = {reg, value};
return hal_i2c_write_bytes(I2C_PORT, address << 1, data, 2); // 地址左移一位,因为 I2C 地址是 7 位
}

// 读取 PCA9685 寄存器
static bool pca9685_read_reg(uint8_t address, uint8_t reg, uint8_t *value) {
return hal_i2c_read_bytes(I2C_PORT, address << 1, &reg, 1) && // 先发送寄存器地址
hal_i2c_read_bytes(I2C_PORT, (address << 1) | 0x01, value, 1); // 再读取数据,地址最后一位设置为读
}


// 初始化 PCA9685
bool pca9685_init(i2c_port_t port, uint8_t address) {
if (!hal_i2c_init(port, 100000)) { // 初始化 I2C,100kHz 速率
return false;
}
if (!pca9685_set_pwm_frequency(address, PCA9685_PWM_FREQ_DEFAULT)) { // 设置默认 PWM 频率
return false;
}
return true;
}

// 设置 PWM 频率
bool pca9685_set_pwm_frequency(uint8_t address, float frequency) {
float prescaleval = 25000000; // 25MHz 晶振频率
prescaleval /= 4096;
prescaleval /= frequency;
prescaleval -= 1;

uint8_t prescale = floor(prescaleval + 0.5);

uint8_t oldmode;
if (!pca9685_read_reg(address, PCA9685_MODE1, &oldmode)) return false;
uint8_t newmode = (oldmode & 0x7F) | PCA9685_SLEEP; // 进入睡眠模式,设置 prescale 需要先进入睡眠
if (!pca9685_write_reg(address, PCA9685_MODE1, newmode)) return false;
if (!pca9685_write_reg(address, PCA9685_PRESCALE, prescale)) return false;
if (!pca9685_write_reg(address, PCA9685_MODE1, oldmode)) return false;
hal_delay_ms(5); // 等待 5ms
if (!pca9685_write_reg(address, PCA9685_MODE1, oldmode | PCA9685_RESTART | PCA9685_ALLCALL)) return false; // 退出睡眠,重启,响应 ALLCALL
return true;
}

// 设置单个通道的 PWM 脉冲宽度 (单位: us)
bool pca9685_set_pwm_pulse_width(uint8_t address, pca9685_channel_t channel, uint16_t pulse_width_us) {
float pulse_width_ticks = pulse_width_us * (4096.0f / (1000000.0f / PCA9685_PWM_FREQ_DEFAULT)); // 根据 PWM 频率计算 ticks
uint16_t on_time = 0;
uint16_t off_time = (uint16_t)pulse_width_ticks;
uint8_t channel_base_reg = PCA9685_LED0_ON_L + channel * 4; // 每个通道占用 4 个寄存器

if (!pca9685_write_reg(address, channel_base_reg + 0, on_time & 0xFF)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 1, on_time >> 8)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 2, off_time & 0xFF)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 3, off_time >> 8)) return false;
return true;
}

// 设置单个通道的 PWM 占空比 (0-4095, 12位分辨率)
bool pca9685_set_pwm_duty_cycle(uint8_t address, pca9685_channel_t channel, uint16_t duty_cycle) {
uint16_t on_time = 0;
uint16_t off_time = duty_cycle;
uint8_t channel_base_reg = PCA9685_LED0_ON_L + channel * 4;

if (!pca9685_write_reg(address, channel_base_reg + 0, on_time & 0xFF)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 1, on_time >> 8)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 2, off_time & 0xFF)) return false;
if (!pca9685_write_reg(address, channel_base_reg + 3, off_time >> 8)) return false;
return true;
}

// 设置舵机角度 (0-180度, 需要校准)
bool pca9685_set_servo_angle(uint8_t address, pca9685_channel_t channel, float angle) {
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;

// **舵机校准参数 (需要根据实际舵机调整)**
uint16_t pulse_min_us = 500; // 最小脉冲宽度 (0度) - 需要校准
uint16_t pulse_max_us = 2500; // 最大脉冲宽度 (180度) - 需要校准

uint16_t pulse_width_us = pulse_min_us + (uint16_t)((pulse_max_us - pulse_min_us) * (angle / 180.0f));

return pca9685_set_pwm_pulse_width(address, channel, pulse_width_us);
}

注意:

  • PCA9685_DEFAULT_ADDRESS 可以根据实际硬件连接进行修改,如果通过地址引脚 A0-A5 设置了不同的地址。
  • I2C_PORT 需要根据实际使用的 I2C 端口进行配置。
  • PCA9685_PWM_FREQ_DEFAULT 设置了默认的 PWM 频率为 50Hz,这通常适用于标准的舵机。您可以根据舵机的具体要求调整频率。
  • pca9685_set_servo_angle() 函数中的 舵机校准参数 pulse_min_uspulse_max_us 非常重要,需要根据实际使用的舵机进行校准。 可以通过实验调整这两个值,使得舵机在 0 度和 180 度时能够准确到达目标位置。不同品牌和型号的舵机,甚至同一型号的不同个体,都可能存在差异。
  • 代码中使用了静态函数 pca9685_write_reg()pca9685_read_reg() 来封装 I2C 寄存器读写操作,提高代码可读性和维护性。
  • 错误处理部分仍然被简化,实际项目中需要完善错误处理,例如在 I2C 通信失败时返回错误码。

2.2.2. TB6612FNG 电机驱动模块

TB6612FNG 是一款双通道电机驱动芯片,可以控制两路直流电机,或者通过桥接控制一路步进电机。本项目中使用 TB6612FNG 驱动 4 路直流电机,因此需要使用两片 TB6612FNG 芯片。电机驱动模块需要实现以下功能:

  • 初始化 TB6612FNG: 配置 TB6612FNG 的控制引脚 (AIN1, AIN2, BIN1, BIN2, PWMA, PWMB, STBY) 的 GPIO 模式和初始状态。
  • 控制电机速度和方向: 通过设置 GPIO 引脚和 PWM 占空比,控制电机的速度和方向 (前进、后退、停止)。
  • 使能/禁用电机驱动: 通过 STBY 引脚控制电机驱动芯片的使能状态,降低功耗。

TB6612FNG 驱动头文件 (tb6612fng.h):

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
#ifndef TB6612FNG_H
#define TB6612FNG_H

#include <stdint.h>
#include <stdbool.h>
#include "hal.h"

typedef enum {
MOTOR_1,
MOTOR_2,
MOTOR_3,
MOTOR_4,
MOTOR_MAX
} motor_channel_t;

typedef enum {
MOTOR_FORWARD,
MOTOR_BACKWARD,
MOTOR_STOP
} motor_direction_t;

// 初始化 TB6612FNG 电机驱动
bool tb6612fng_init(void);
// 控制电机速度 (0-100%) 和方向
bool tb6612fng_set_motor_speed_direction(motor_channel_t motor, uint8_t speed_percent, motor_direction_t direction);
// 停止所有电机
bool tb6612fng_stop_all_motors(void);
// 使能/禁用电机驱动芯片
bool tb6612fng_enable_driver(bool enable);

#endif // TB6612FNG_H

TB6612FNG 驱动实现文件 (tb6612fng.c):

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include "tb6612fng.h"

// 电机控制引脚定义 (根据实际硬件连接修改)
#define MOTOR1_AIN1_PIN GPIO_PIN_X1
#define MOTOR1_AIN2_PIN GPIO_PIN_X2
#define MOTOR1_PWM_PIN PWM_CHANNEL_X1 // 使用 PWM 通道 X1
#define MOTOR2_BIN1_PIN GPIO_PIN_X3
#define MOTOR2_BIN2_PIN GPIO_PIN_X4
#define MOTOR2_PWM_PIN PWM_CHANNEL_X2 // 使用 PWM 通道 X2
#define MOTOR3_AIN1_PIN GPIO_PIN_X5
#define MOTOR3_AIN2_PIN GPIO_PIN_X6
#define MOTOR3_PWM_PIN PWM_CHANNEL_X3 // 使用 PWM 通道 X3
#define MOTOR4_BIN1_PIN GPIO_PIN_X7
#define MOTOR4_BIN2_PIN GPIO_PIN_X8
#define MOTOR4_PWM_PIN PWM_CHANNEL_X4 // 使用 PWM 通道 X4
#define MOTOR_STBY_PIN GPIO_PIN_X9 // STBY 引脚

#define MOTOR_PWM_FREQ 1000 // 电机 PWM 频率 1kHz (可以根据电机特性调整)

// 初始化 TB6612FNG 电机驱动
bool tb6612fng_init(void) {
// 初始化 GPIO 引脚为输出模式
hal_gpio_init(MOTOR1_AIN1_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR1_AIN2_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR2_BIN1_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR2_BIN2_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR3_AIN1_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR3_AIN2_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR4_BIN1_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR4_BIN2_PIN, GPIO_MODE_OUTPUT);
hal_gpio_init(MOTOR_STBY_PIN, GPIO_MODE_OUTPUT);

// 初始化 PWM 通道
hal_pwm_init(MOTOR1_PWM_PIN, MOTOR_PWM_FREQ);
hal_pwm_init(MOTOR2_PWM_PIN, MOTOR_PWM_FREQ);
hal_pwm_init(MOTOR3_PWM_PIN, MOTOR_PWM_FREQ);
hal_pwm_init(MOTOR4_PWM_PIN, MOTOR_PWM_FREQ);

// 初始状态:停止所有电机,使能驱动芯片
tb6612fng_stop_all_motors();
tb6612fng_enable_driver(true);

return true;
}

// 控制电机速度 (0-100%) 和方向
bool tb6612fng_set_motor_speed_direction(motor_channel_t motor, uint8_t speed_percent, motor_direction_t direction) {
if (speed_percent > 100) speed_percent = 100;

gpio_pin_t ain1_pin, ain2_pin, bin1_pin, bin2_pin;
pwm_channel_t pwm_channel;

switch (motor) {
case MOTOR_1:
ain1_pin = MOTOR1_AIN1_PIN;
ain2_pin = MOTOR1_AIN2_PIN;
pwm_channel = MOTOR1_PWM_PIN;
break;
case MOTOR_2:
bin1_pin = MOTOR2_BIN1_PIN;
bin2_pin = MOTOR2_BIN2_PIN;
pwm_channel = MOTOR2_PWM_PIN;
break;
case MOTOR_3:
ain1_pin = MOTOR3_AIN1_PIN;
ain2_pin = MOTOR3_AIN2_PIN;
pwm_channel = MOTOR3_PWM_PIN;
break;
case MOTOR_4:
bin1_pin = MOTOR4_BIN1_PIN;
bin2_pin = MOTOR4_BIN2_PIN;
pwm_channel = MOTOR4_PWM_PIN;
break;
default:
return false; // 非法电机通道
}

switch (direction) {
case MOTOR_FORWARD:
hal_gpio_set_level(ain1_pin, GPIO_LEVEL_HIGH);
hal_gpio_set_level(ain2_pin, GPIO_LEVEL_LOW);
hal_pwm_set_duty_cycle(pwm_channel, speed_percent);
break;
case MOTOR_BACKWARD:
hal_gpio_set_level(ain1_pin, GPIO_LEVEL_LOW);
hal_gpio_set_level(ain2_pin, GPIO_LEVEL_HIGH);
hal_pwm_set_duty_cycle(pwm_channel, speed_percent);
break;
case MOTOR_STOP:
hal_gpio_set_level(ain1_pin, GPIO_LEVEL_LOW);
hal_gpio_set_level(ain2_pin, GPIO_LEVEL_LOW);
hal_pwm_set_duty_cycle(pwm_channel, 0); // 停止时 PWM 占空比设为 0
break;
default:
return false; // 非法电机方向
}

return true;
}

// 停止所有电机
bool tb6612fng_stop_all_motors(void) {
for (int i = MOTOR_1; i < MOTOR_MAX; i++) {
tb6612fng_set_motor_speed_direction((motor_channel_t)i, 0, MOTOR_STOP);
}
return true;
}

// 使能/禁用电机驱动芯片
bool tb6612fng_enable_driver(bool enable) {
if (enable) {
hal_gpio_set_level(MOTOR_STBY_PIN, GPIO_LEVEL_HIGH); // 使能 STBY (高电平使能)
} else {
hal_gpio_set_level(MOTOR_STBY_PIN, GPIO_LEVEL_LOW); // 禁用 STBY (低电平禁用)
}
return true;
}

注意:

  • MOTOR1_AIN1_PIN, MOTOR1_AIN2_PIN, MOTOR1_PWM_PIN, … MOTOR_STBY_PIN 等宏定义需要根据实际硬件连接进行修改,将这些宏定义与 NodeMCU-32S 开发板上的 GPIO 引脚和 PWM 通道对应起来。
  • MOTOR_PWM_FREQ 设置了电机 PWM 频率,可以根据电机的特性和驱动效果进行调整,例如某些电机在较高频率下可能表现更好,而另一些电机可能在较低频率下效率更高。
  • 代码中使用了 tb6612fng_stop_all_motors() 函数来方便地停止所有电机。
  • tb6612fng_enable_driver() 函数可以用于控制电机驱动芯片的整体使能状态,在不需要电机运动时可以禁用驱动芯片,降低功耗。

2.3. 控制层 (Control Layer)

控制层构建在驱动层之上,负责实现系统的核心控制逻辑,包括舵机控制和电机控制。

2.3.1. 舵机控制模块 (servo_control.c/h)

舵机控制模块负责根据指令控制 16 个舵机的角度。它可以提供以下功能:

  • 初始化舵机控制: 初始化 PCA9685 驱动模块。
  • 设置单个舵机角度: 接收舵机通道号和目标角度,调用 PCA9685 驱动模块的 pca9685_set_servo_angle() 函数来控制舵机运动。
  • 设置所有舵机角度: 接收所有舵机的目标角度数组,批量设置所有舵机角度。
  • 舵机角度校准: 提供接口来调整舵机的校准参数 (pulse_min_us, pulse_max_us),以提高舵机控制的精度。

2.3.2. 电机控制模块 (motor_control.c/h)

电机控制模块负责控制 4 个直流电机的速度和方向。它可以提供以下功能:

  • 初始化电机控制: 初始化 TB6612FNG 驱动模块。
  • 控制单个电机: 接收电机通道号、速度百分比和方向,调用 TB6612FNG 驱动模块的 tb6612fng_set_motor_speed_direction() 函数来控制电机运动。
  • 控制所有电机: 接收所有电机的速度和方向数组,批量控制所有电机。
  • 停止所有电机: 调用 TB6612FNG 驱动模块的 tb6612fng_stop_all_motors() 函数停止所有电机。

2.3.3. 高级控制算法 (可选)

如果项目需要实现更复杂的机器人运动功能,例如步态控制、运动学解算、路径规划等,可以在控制层添加相应的算法模块。这些算法模块可以调用舵机控制模块和电机控制模块,实现机器人的自主运动。

2.4. 应用层 (Application Layer)

应用层是最高层,负责系统的整体控制和用户交互。对于这个项目,应用层可以实现以下功能:

  • 系统初始化: 初始化 HAL 层、驱动层、控制层的所有模块。
  • 命令解析: 接收来自用户或上位机软件的控制命令,例如通过 Wi-Fi 或串口接收命令。命令可以是控制舵机角度、电机速度、执行预设动作等。
  • 命令处理: 解析命令,并将命令参数传递给控制层的相应模块进行处理。例如,如果接收到控制舵机角度的命令,则调用舵机控制模块的函数来设置舵机角度。
  • 状态反馈: 将系统的状态信息反馈给用户或上位机软件,例如舵机角度、电机速度、传感器数据等。
  • 错误处理: 处理系统运行过程中出现的错误,例如硬件故障、通信错误等,并进行相应的错误处理,例如记录错误日志、重启系统等。
  • 用户界面 (可选): 如果需要,可以在应用层实现简单的用户界面,例如通过串口或 LCD 屏幕显示系统状态,接收用户输入。对于 NodeMCU-32S 平台,更常见的用户界面是通过 Wi-Fi 连接到 Web 界面或手机 App 进行控制。

应用层示例代码 (main.c):

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
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include "hal.h"
#include "pca9685.h"
#include "tb6612fng.h"
#include "servo_control.h" // 假设有 servo_control.c/h
#include "motor_control.h" // 假设有 motor_control.c/h
#include "esp_log.h" // ESP-IDF 日志库

#define PCA9685_ADDR PCA9685_DEFAULT_ADDRESS

static const char *TAG = "app_main";

void app_main(void)
{
ESP_LOGI(TAG, "系统启动...");

// 初始化 HAL 层
ESP_LOGI(TAG, "初始化 HAL...");
// HAL 初始化代码 (例如 GPIO, I2C, PWM 初始化)

// 初始化驱动层
ESP_LOGI(TAG, "初始化 PCA9685 驱动...");
if (!pca9685_init(I2C_PORT_0, PCA9685_ADDR)) {
ESP_LOGE(TAG, "PCA9685 初始化失败!");
// 错误处理
}
ESP_LOGI(TAG, "初始化 TB6612FNG 驱动...");
if (!tb6612fng_init()) {
ESP_LOGE(TAG, "TB6612FNG 初始化失败!");
// 错误处理
}

// 初始化控制层 (假设有 servo_control_init() 和 motor_control_init() 函数)
ESP_LOGI(TAG, "初始化舵机控制...");
// servo_control_init();
ESP_LOGI(TAG, "初始化电机控制...");
// motor_control_init();

ESP_LOGI(TAG, "系统初始化完成!");

// 应用主循环
while (1) {
// 1. 接收用户命令 (例如通过 Wi-Fi 或串口)
// ... 命令解析代码 ...

// 2. 根据命令类型进行处理
// 例如:
// if (command_type == COMMAND_SET_SERVO_ANGLE) {
// servo_control_set_angle(command_servo_channel, command_servo_angle);
// } else if (command_type == COMMAND_SET_MOTOR_SPEED) {
// motor_control_set_speed_direction(command_motor_channel, command_motor_speed, command_motor_direction);
// }

// 3. 状态反馈 (例如通过 Wi-Fi 或串口)
// ... 状态反馈代码 ...

hal_delay_ms(10); // 适当延时
}
}

注意:

  • app_main() 函数是整个应用程序的入口点。
  • 代码中使用了 ESP-IDF 的日志库 esp_log.h 来输出日志信息,方便调试和监控系统运行状态。
  • 应用层需要根据具体的应用场景实现命令解析、命令处理、状态反馈、用户界面等功能。
  • 示例代码中 servo_control.hmotor_control.h 的头文件以及 servo_control_init()motor_control_init() 函数是假设存在的,实际项目中需要根据控制层的设计来实现这些模块。

3. 测试与验证

在完成代码编写后,需要进行充分的测试和验证,确保系统的可靠性和稳定性。测试可以分为以下几个阶段:

  • 单元测试: 对每个模块 (HAL 层、驱动层、控制层、应用层) 进行单独测试,验证模块的功能是否正确。例如,可以编写单元测试代码来测试 HAL 层的 GPIO 控制、I2C 通信、PWM 输出等功能,测试 PCA9685 驱动模块的舵机角度控制功能,测试 TB6612FNG 驱动模块的电机速度和方向控制功能。
  • 集成测试: 将各个模块集成起来进行测试,验证模块之间的接口是否正确,数据传递是否正常。例如,可以测试应用层通过控制层调用驱动层和 HAL 层来控制舵机和电机的整个流程。
  • 系统测试: 对整个系统进行全面的功能测试和性能测试,验证系统是否满足设计需求。例如,可以测试系统在不同负载下的运行稳定性,测试系统的实时性,测试系统的功耗等。
  • 耐久性测试: 进行长时间的运行测试,验证系统在长时间运行下的可靠性和稳定性。

4. 维护与升级

嵌入式系统的维护和升级是一个持续的过程。为了方便后续的维护和升级,需要注意以下几点:

  • 代码注释: 在代码中添加清晰、详细的注释,方便理解代码逻辑和功能。
  • 模块化设计: 采用模块化的设计方法,将系统分解为独立的模块,方便修改和替换模块。
  • 版本控制: 使用版本控制工具 (例如 Git) 管理代码,方便跟踪代码修改历史,回滚代码版本。
  • 日志记录: 在系统中添加日志记录功能,记录系统运行状态、错误信息等,方便排查问题。
  • OTA 升级 (可选): 如果系统需要远程升级功能,可以考虑实现 OTA (Over-The-Air) 升级功能,通过网络远程更新固件。NodeMCU-32S 平台支持 ESP-IDF 的 OTA 升级功能。

总结

以上是我为您设计的基于 NodeMCU-32S 拓展板的嵌入式系统的代码架构和 C 代码实现方案。这个方案采用了分层架构和模块化设计,将系统分解为 HAL 层、驱动层、控制层、应用层和公共服务层,每个层次和模块都负责特定的功能,提高了代码的可读性、可维护性和可扩展性。代码示例涵盖了 HAL 层、PCA9685 驱动、TB6612FNG 驱动以及应用层的主框架,您可以根据这个框架进行扩展和完善,实现更复杂的功能。

请注意,这只是一个基础框架,实际项目中还需要根据具体的需求和硬件连接进行调整和修改。例如,GPIO 引脚的定义、I2C 地址、PWM 通道的配置、舵机和电机的校准参数等都需要根据实际情况进行配置。同时,错误处理、通信协议、用户界面、高级控制算法等也需要根据项目需求进行详细设计和实现。

希望这个详细的方案能够帮助您构建一个可靠、高效、可扩展的嵌入式系统平台!

欢迎关注我的其它发布渠道