Liên kết nguồn mở dự án Tất cả tài liệu của dự án đều được công khai. Cách thức tải gói tài liệu:
- GitHub: https://github.com/snqx-lqh/ProjectReleasePage
- Gitee (mirror trong nước): https://gitee.com/snqx-lqh/ProjectOpenSourceReleasePage
Dự án có mã số B005 trong danh sách phát hành. Tìm đúng dự án tại trang phát hành để truy cập.
Tổng quan dự án Dự án này tập trung vào việc triển khai chức năng cân bằng cơ bản cho xe cân bằng STM32, sử dụng thư viện HAL và tích hợp với công cụ CubeMX. Bài viết cung cấp hướng dẫn chi tiết, phù hợp với người mới bắt đầu. Nội dung chỉ mang tính tham khảo cá nhân, không đi sâu vào lý thuyết, mà tập trung vào thực thi để xe hoạt động ổn định.
Phần này trình bày cách xây dựng các file driver điều khiển phần cứng.
Quản lý cấu trúc thư mục
Trong thư mục gốc, tạo thư mục User với ba nhánh con:
Algorithm: chứa các thuật toán xử lýDriver: lưu trữ các file driver thiết bịTask: chứa các tác vụ chức năng
Trong môi trường Keil, tạo 3 thư mục tương ứng trong trình quản lý file. Đồng thời, cập nhật đường dẫn bao gồm các thư mục vừa tạo vào cấu hình biên dịch.
Đoạn tiếp theo sẽ hoàn thiện toàn bộ các file driver trong thư mục Driver.
Driver LED Việc điều khiển LED đơn giản chỉ cần đóng gói hàm HAL thành các API dễ dùng.
#include "bsp_led.h"
void led_turn_on(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
void led_turn_off(void)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
void led_toggle_state(void)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
Driver nút nhấn
Lấy ý tưởng từ code mẫu của chính điểm nguyên, nhưng thay vì chờ chết, dùng biến đếm thời gian. Sử dụng kiểu liệt kê để xác định chế độ quét. Tham số dt là khoảng thời gian (ms) giữa các lần gọi hàm, ví dụ nếu gọi mỗi 5ms thì dt = 5. Điều này đảm bảo độ trễ phím luôn vượt quá 20ms.
bsp_key.h
#ifndef _BSP_KEY_H
#define _BSP_KEY_H
#include "main.h"
#include "gpio.h"
typedef enum {
SCAN_ONCE,
SCAN_CONTINUOUS
} key_mode_t;
#define KEY1_READ HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)
#define KEY2_READ HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin)
#define KEY3_READ HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin)
#define KEY1_VAL 1
#define KEY2_VAL 2
#define KEY3_VAL 3
uint8_t scan_key(const key_mode_t mode, const uint8_t dt);
#endif
bsp_key.c
#include "bsp_key.h"
uint8_t scan_key(const key_mode_t mode, const uint8_t dt)
{
static int counter = 0;
static int state = 0;
if (mode == SCAN_CONTINUOUS) state = 0;
if (state == 0 && (KEY1_READ == 0 || KEY2_READ == 0 || KEY3_READ == 0))
{
counter++;
if (counter * dt > 20)
{
state = 1;
counter = 0;
if (KEY1_READ == 0) return KEY1_VAL;
else if (KEY2_READ == 0) return KEY2_VAL;
else if (KEY3_READ == 0) return KEY3_VAL;
}
}
else if (KEY1_READ == 1 && KEY2_READ == 1 && KEY3_READ == 1)
{
state = 0;
}
return 0;
}
Driver động cơ & encoder Điều khiển động cơ thông qua PWM, đọc giá trị encoder bằng cách lấy số xung từ thời điểm trước khi reset đến hiện tại. Sau khi đọc, phải reset lại bộ đếm.
bsp_motor.h
#ifndef _BSP_MOTOR_H
#define _BSP_MOTOR_H
#include "main.h"
void motor_setup(void);
void set_motor_pwm(int16_t left_pwm, int16_t right_pwm);
int16_t get_encoder_count(uint8_t timer_id);
#endif
bsp_motor.c
#include "bsp_motor.h"
#include "tim.h"
#define MAX_PWM_VALUE 6900
// Gán chân điều khiển
#define SET_AIN1_HIGH() HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_SET)
#define SET_AIN1_LOW() HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET)
#define SET_AIN2_HIGH() HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_SET)
#define SET_AIN2_LOW() HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET)
#define SET_BIN1_HIGH() HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET)
#define SET_BIN1_LOW() HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET)
#define SET_BIN2_HIGH() HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_SET)
#define SET_BIN2_LOW() HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET)
#define SET_PWM_A(x) __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, x)
#define SET_PWM_B(x) __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, x)
void motor_setup(void)
{
HAL_TIM_Base_Start(&htim1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);
}
void set_motor_pwm(int16_t left_pwm, int16_t right_pwm)
{
// Giới hạn giá trị PWM
if (left_pwm > MAX_PWM_VALUE) left_pwm = MAX_PWM_VALUE;
else if (left_pwm < -MAX_PWM_VALUE) left_pwm = -MAX_PWM_VALUE;
if (right_pwm > MAX_PWM_VALUE) right_pwm = MAX_PWM_VALUE;
else if (right_pwm < -MAX_PWM_VALUE) right_pwm = -MAX_PWM_VALUE;
// Điều khiển phía trái
if (left_pwm >= 0)
{
SET_AIN1_HIGH();
SET_AIN2_LOW();
SET_PWM_A(left_pwm);
}
else
{
SET_AIN1_LOW();
SET_AIN2_HIGH();
SET_PWM_A(-left_pwm);
}
// Điều khiển phía phải
if (right_pwm >= 0)
{
SET_BIN1_HIGH();
SET_BIN2_LOW();
SET_PWM_B(right_pwm);
}
else
{
SET_BIN1_LOW();
SET_BIN2_HIGH();
SET_PWM_B(-right_pwm);
}
}
int16_t get_encoder_count(uint8_t timer_id)
{
int16_t count = 0;
switch (timer_id)
{
case 2:
count = __HAL_TIM_GET_COUNTER(&htim2);
__HAL_TIM_SET_COUNTER(&htim2, 0);
break;
case 4:
count = __HAL_TIM_GET_COUNTER(&htim4);
__HAL_TIM_SET_COUNTER(&htim4, 0);
break;
default:
break;
}
return count;
}
Driver cảm biến siêu âm HC-SR04
Cảm biến sử dụng chế độ truyền – nhận xung. Khi gửi tín hiệu, chân TRIG bật cao trong 10μs, sau đó đợi tín hiệu phản hồi trên chân ECHO. Khi phát hiện cạnh lên, khởi động bộ đếm; khi gặp cạnh xuống, dừng đếm và tính thời gian. Do bộ đếm 16-bit nên vượt ngưỡng 65535 sẽ gây ngắt, do đó cần đếm số lần ngắt (period_cnt) để tính tổng thời gian.
hc_sr04.h
#ifndef _HC_SR04_H
#define _HC_SR04_H
#include "main.h"
void send_trigger_pulse(void);
void init_hcsr04(void);
uint16_t get_echo_duration(void);
uint16_t get_distance_mm(void);
#endif
hc_sr04.c
#include "hc_sr04.h"
#include "tim.h"
#define TIMER_HCSR04 htim3
#define CHANNEL_HCSR04 TIM_CHANNEL_3
static uint32_t capture_value = 0;
static uint32_t interrupt_count = 0;
static uint8_t trigger_flag = 0;
static uint8_t capture_done = 0;
static uint16_t total_count = 0;
void init_hcsr04(void)
{
__HAL_TIM_CLEAR_FLAG(&TIMER_HCSR04, TIM_IT_UPDATE);
HAL_TIM_Base_Start_IT(&TIMER_HCSR04);
HAL_TIM_IC_Start_IT(&TIMER_HCSR04, CHANNEL_HCSR04);
}
uint16_t get_echo_duration(void)
{
return total_count;
}
uint16_t get_distance_mm(void)
{
return total_count / 58;
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
interrupt_count++;
}
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (!trigger_flag)
{
interrupt_count = 0;
__HAL_TIM_SET_COUNTER(htim, 0);
capture_value = 0;
__HAL_TIM_SET_CAPTUREPOLARITY(htim, CHANNEL_HCSR04, TIM_INPUTCHANNELPOLARITY_FALLING);
trigger_flag = 1;
}
else
{
capture_value = __HAL_TIM_GET_COMPARE(htim, CHANNEL_HCSR04);
__HAL_TIM_SET_CAPTUREPOLARITY(htim, CHANNEL_HCSR04, TIM_INPUTCHANNELPOLARITY_RISING);
trigger_flag = 0;
capture_done = 1;
if (capture_done)
{
total_count = interrupt_count * 65535 + (capture_value + 1);
capture_done = 0;
}
}
}
void send_trigger_pulse(void)
{
uint32_t i;
HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
for (i = 0; i < 72 * 40; i++) __NOP();
HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);
}
Driver cảm biến gia tốc - gyro MPU6050 Sử dụng driver gốc từ chính điểm nguyên, nhưng điều chỉnh để dùng I2C HAL. Bật ngắt và đặt tần số lấy mẫu 100Hz → ngắt mỗi 10ms.
mpu6050.h
#ifndef _MPU6050_DRIVER_H
#define _MPU6050_DRIVER_H
#include <stdint.h>
int mpu6050_init(void);
uint8_t mpu6050_set_gyro_range(uint8_t range);
uint8_t mpu6050_set_acc_range(uint8_t range);
uint8_t mpu6050_set_filter_bandwidth(uint16_t bandwidth);
uint8_t mpu6050_set_sample_rate(uint16_t rate);
int16_t mpu6050_get_temp(void);
uint8_t mpu6050_read_gyro(int16_t *gx, int16_t *gy, int16_t *gz);
uint8_t mpu6050_read_acc(int16_t *ax, int16_t *ay, int16_t *az);
#define GYRO_RANGE_250 0
#define GYRO_RANGE_500 1
#define GYRO_RANGE_1000 2
#define GYRO_RANGE_2000 3
#define ACC_RANGE_2G 0
#define ACC_RANGE_4G 1
#define ACC_RANGE_8G 2
#define ACC_RANGE_16G 3
#define MPU6050_I2C_ADDR 0x68
// Các địa chỉ register
#define REG_PWR_MGMT1 0x6B
#define REG_GYRO_CFG 0x1B
#define REG_ACCEL_CFG 0x1C
#define REG_SAMPLE_RATE 0x19
#define REG_INT_ENABLE 0x38
#define REG_TEMP_OUT 0x41
#define REG_GYRO_XOUT 0x43
#define REG_ACCEL_XOUT 0x3B
#endif
mpu6050.c
#include "mpu6050.h"
#include "i2c.h"
void delay_ms(uint16_t ms)
{
HAL_Delay(ms);
}
int write_reg(uint8_t reg, uint8_t data)
{
HAL_I2C_Mem_Write(&hi2c1, (MPU6050_I2C_ADDR << 1), reg, I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);
return 0;
}
int read_reg(uint8_t reg, uint8_t *data)
{
HAL_I2C_Mem_Read(&hi2c1, (MPU6050_I2C_ADDR << 1), reg, I2C_MEMADD_SIZE_8BIT, data, 1, HAL_MAX_DELAY);
return 0;
}
int read_regs(uint8_t reg, uint8_t *data, uint8_t len)
{
HAL_I2C_Mem_Read(&hi2c1, (MPU6050_I2C_ADDR << 1), reg, I2C_MEMADD_SIZE_8BIT, data, len, HAL_MAX_DELAY);
return 0;
}
int mpu6050_init(void)
{
uint8_t id;
write_reg(REG_PWR_MGMT1, 0x80);
delay_ms(100);
write_reg(REG_PWR_MGMT1, 0x00);
mpu6050_set_gyro_range(GYRO_RANGE_2000);
mpu6050_set_acc_range(ACC_RANGE_2G);
mpu6050_set_sample_rate(100);
write_reg(REG_INT_ENABLE, 0x01);
write_reg(REG_USER_CTRL, 0x00);
write_reg(REG_FIFO_EN, 0x00);
write_reg(REG_INTBP_CFG, 0x9C);
read_reg(REG_DEVICE_ID, &id);
if (id == MPU6050_I2C_ADDR)
{
write_reg(REG_PWR_MGMT1, 0x01);
write_reg(REG_PWR_MGMT2, 0x00);
mpu6050_set_sample_rate(100);
}
else return 1;
return 0;
}
uint8_t mpu6050_set_gyro_range(uint8_t range)
{
return write_reg(REG_GYRO_CFG, (range << 3) | 3);
}
uint8_t mpu6050_set_acc_range(uint8_t range)
{
return write_reg(REG_ACCEL_CFG, range << 3);
}
uint8_t mpu6050_set_filter_bandwidth(uint16_t bw)
{
uint8_t val = 6;
if (bw >= 188) val = 1;
else if (bw >= 98) val = 2;
else if (bw >= 42) val = 3;
else if (bw >= 20) val = 4;
else if (bw >= 10) val = 5;
return write_reg(REG_CFG, val);
}
uint8_t mpu6050_set_sample_rate(uint16_t rate)
{
uint8_t div = 1000 / rate - 1;
write_reg(REG_SAMPLE_RATE, div);
return mpu6050_set_filter_bandwidth(rate / 2);
}
int16_t mpu6050_get_temp(void)
{
uint8_t buf[2];
read_regs(REG_TEMP_OUT, buf, 2);
return ((uint16_t)buf[0] << 8) | buf[1];
}
uint8_t mpu6050_read_gyro(int16_t *gx, int16_t *gy, int16_t *gz)
{
uint8_t buf[6];
if (!gx || !gy || !gz) return 2;
if (read_regs(REG_GYRO_XOUT, buf, 6) != 0) return 1;
*gx = ((uint16_t)buf[0] << 8) | buf[1];
*gy = ((uint16_t)buf[2] << 8) | buf[3];
*gz = ((uint16_t)buf[4] << 8) | buf[5];
return 0;
}
uint8_t mpu6050_read_acc(int16_t *ax, int16_t *ay, int16_t *az)
{
uint8_t buf[6];
if (!ax || !ay || !az) return 2;
if (read_regs(REG_ACCEL_XOUT, buf, 6) != 0) return 1;
*ax = ((uint16_t)buf[0] << 8) | buf[1];
*ay = ((uint16_t)buf[2] << 8) | buf[3];
*az = ((uint16_t)buf[4] << 8) | buf[5];
return 0;
}
Driver OLED màn hình Sử dụng mã nguồn đã được chuyển đổi từ phiên bản SPI 0.96 inch của Trung Cảnh Nguyên. Link tải: https://pan.baidu.com/s/1U9r32qeS2jOANB0SNwtwnw — mã giải nén: 8888
Chỉnh sửa file .h để thay đổi cách gán mức logic cho các chân.
#ifndef __OLED_H
#define __OLED_H
#include "main.h"
#include "stdlib.h"
#define OLED_RES_LOW() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_RESET)
#define OLED_RES_HIGH() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_SET)
#define OLED_DC_LOW() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET)
#define OLED_DC_HIGH() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET)
#define OLED_CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET)
#define OLED_CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET)
#define OLED_CMD 0
#define OLED_DATA 1
void OLED_ClearPoint(uint8_t x, uint8_t y);
void OLED_WR_Byte(uint8_t dat, uint8_t mode);
void OLED_Init(void);
void OLED_Clear(void);
void OLED_Display_On(void);
void OLED_Display_Off(void);
void OLED_Refresh(void);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t t);
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode);
void OLED_ShowString(uint8_t x, uint8_t y, uint8_t *str, uint8_t size, uint8_t mode);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size, uint8_t mode);
void OLED_ScrollDisplay(uint8_t num, uint8_t space, uint8_t mode);
void OLED_ShowPicture(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t bitmap[], uint8_t mode);
#endif
Sửa hàm OLED_WR_Byte trong file .c để dùng SPI HAL thay vì bit-banging.
#include "oled.h"
#include "spi.h"
void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
if (cmd)
OLED_DC_HIGH();
else
OLED_DC_LOW();
OLED_CS_LOW();
HAL_SPI_Transmit(&hspi1, &dat, 1, 100);
OLED_CS_HIGH();
OLED_DC_HIGH();
}
void OLED_Clear_Buffer(void)
{
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 128; j++)
{
OLED_GRAM[j][i] = 0;
}
}
}
ADC đo điện áp nguồn Đọc giá trị ADC đơn giản từ kênh ADC1.
float get_battery_voltage(void)
{
uint16_t raw_val;
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
{
raw_val = HAL_ADC_GetValue(&hadc1);
}
return (raw_val / 4096.0f) * 3.3f * 11.0f;
}