好的,作为一名高级嵌入式软件开发工程师,我将为您详细介绍这个基于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> typedef enum { GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, 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 ; typedef enum { I2C_PORT_0, I2C_PORT_1, I2C_PORT_MAX } i2c_port_t ; typedef enum { PWM_CHANNEL_0, PWM_CHANNEL_1, PWM_CHANNEL_MAX } pwm_channel_t ; void hal_gpio_init (gpio_pin_t pin, gpio_mode_t mode) ;void hal_gpio_set_level (gpio_pin_t pin, gpio_level_t level) ;gpio_level_t hal_gpio_get_level (gpio_pin_t pin) ;bool hal_i2c_init (i2c_port_t port, uint32_t clock_speed) ;bool hal_i2c_write_bytes (i2c_port_t port, uint8_t address, const uint8_t *data, size_t data_len) ;bool hal_i2c_read_bytes (i2c_port_t port, uint8_t address, uint8_t *data, size_t data_len) ;bool hal_pwm_init (pwm_channel_t channel, uint32_t frequency) ;bool hal_pwm_set_duty_cycle (pwm_channel_t channel, uint32_t duty_cycle_percent) ;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层实现文件 (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" 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); 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); } void hal_gpio_set_level (gpio_pin_t pin, gpio_level_t level) { gpio_set_level(pin, level); } gpio_level_t hal_gpio_get_level (gpio_pin_t pin) { return gpio_get_level(pin); } 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; conf.scl_io_num = I2C_MASTER_SCL_IO; 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); } 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); return true ; } 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); return true ; } 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, .timer_num = channel, .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 , .gpio_num = PWM_OUTPUT_GPIO, .speed_mode = LEDC_HIGH_SPEED_MODE, .hpoint = 0 , .timer_sel = channel, }; ret = ledc_channel_config(&channel_conf); return (ret == ESP_OK); } 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 ; 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); } 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); } 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 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 ; bool pca9685_init (i2c_port_t port, uint8_t address) ;bool pca9685_set_pwm_frequency (uint8_t address, float frequency) ;bool pca9685_set_pwm_pulse_width (uint8_t address, pca9685_channel_t channel, uint16_t pulse_width_us) ;bool pca9685_set_pwm_duty_cycle (uint8_t address, pca9685_channel_t channel, uint16_t duty_cycle) ;bool pca9685_set_servo_angle (uint8_t address, pca9685_channel_t channel, float angle) ;#endif
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> #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 #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 #define PCA9685_PWM_FREQ_DEFAULT 50 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 ); } static bool pca9685_read_reg (uint8_t address, uint8_t reg, uint8_t *value) { return hal_i2c_read_bytes(I2C_PORT, address << 1 , ®, 1 ) && hal_i2c_read_bytes(I2C_PORT, (address << 1 ) | 0x01 , value, 1 ); } bool pca9685_init (i2c_port_t port, uint8_t address) { if (!hal_i2c_init(port, 100000 )) { return false ; } if (!pca9685_set_pwm_frequency(address, PCA9685_PWM_FREQ_DEFAULT)) { return false ; } return true ; } bool pca9685_set_pwm_frequency (uint8_t address, float frequency) { float prescaleval = 25000000 ; 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; 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 ); if (!pca9685_write_reg(address, PCA9685_MODE1, oldmode | PCA9685_RESTART | PCA9685_ALLCALL)) return false ; return true ; } 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)); 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 ; 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 ; } 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 ; } 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 ; uint16_t pulse_max_us = 2500 ; 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_us
和 pulse_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 ; bool tb6612fng_init (void ) ;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 驱动实现文件 (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 #define MOTOR2_BIN1_PIN GPIO_PIN_X3 #define MOTOR2_BIN2_PIN GPIO_PIN_X4 #define MOTOR2_PWM_PIN PWM_CHANNEL_X2 #define MOTOR3_AIN1_PIN GPIO_PIN_X5 #define MOTOR3_AIN2_PIN GPIO_PIN_X6 #define MOTOR3_PWM_PIN PWM_CHANNEL_X3 #define MOTOR4_BIN1_PIN GPIO_PIN_X7 #define MOTOR4_BIN2_PIN GPIO_PIN_X8 #define MOTOR4_PWM_PIN PWM_CHANNEL_X4 #define MOTOR_STBY_PIN GPIO_PIN_X9 #define MOTOR_PWM_FREQ 1000 bool tb6612fng_init (void ) { 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); 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 ; } 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 ); 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); } else { hal_gpio_set_level(MOTOR_STBY_PIN, GPIO_LEVEL_LOW); } 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" #include "motor_control.h" #include "esp_log.h" #define PCA9685_ADDR PCA9685_DEFAULT_ADDRESS static const char *TAG = "app_main" ;void app_main (void ) { ESP_LOGI(TAG, "系统启动..." ); ESP_LOGI(TAG, "初始化 HAL..." ); 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 初始化失败!" ); } ESP_LOGI(TAG, "初始化舵机控制..." ); ESP_LOGI(TAG, "初始化电机控制..." ); ESP_LOGI(TAG, "系统初始化完成!" ); while (1 ) { hal_delay_ms(10 ); } }
注意:
app_main()
函数是整个应用程序的入口点。
代码中使用了 ESP-IDF 的日志库 esp_log.h
来输出日志信息,方便调试和监控系统运行状态。
应用层需要根据具体的应用场景实现命令解析、命令处理、状态反馈、用户界面等功能。
示例代码中 servo_control.h
和 motor_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 通道的配置、舵机和电机的校准参数等都需要根据实际情况进行配置。同时,错误处理、通信协议、用户界面、高级控制算法等也需要根据项目需求进行详细设计和实现。
希望这个详细的方案能够帮助您构建一个可靠、高效、可扩展的嵌入式系统平台!