Tanuki_Bayashin’s diary

電子工作を趣味としています。最近はラズベリーパイPicoというマイコンを使って楽しんでいます

サーボモータで倒立振子を作ってみた~ソフト編

※なにか気になる点がありましたらコメント欄にご記入ください。また、工作や回路を製作する場合には、細かい作業などに対して、細心の注意を払われるようお願いいたします。

1.はじめに

サーボモータ(指令値に対し回転数を制御するタイプ)を用いて倒立振子を組み立ててみました。Youtubeのチャンネルを参考にしました。

参考にしたYoutubeのチャンネル
www.youtube.com

写真1 製作した倒立振子

結論から言って、まだ十分にバランスが取れてはいないです。今後、さらに取り組んでいきたいです。

この記事では、ソフトの部分を中心に書いていきます。ハードに関する記事は以下を参照してください。

ハード編のリンク先:
サーボモータで倒立振子を作ってみた~ハード編 - Tanuki_Bayashin’s diary

マイコンには”Raspberry Pi Pico”を使用しました。(以下Picoと記述します)

※アマチュア電子工作erの記事です。至らない点は大目に見てくださいませ。(*- -)(*_ _)ペコリ

訂正

 2023/10/27 リストg-2 timer_irq_end()内にs[100]を宣言する行を追加しました。

2.制作したコード群

 制作したコードを列記していきます。MPU6050という加速度センサモジュールを使用したかったのですが、microPythonにライブラリがなく、そのため言語はPico SDK C/C++を利用しました。Pico SDK C/C++を使用するための簡単なあらましを、以下の記事に昔書きました。参考にして下さい。(大元のラズベリーパイ財団のドキュメントを直接読まれる方が、早く理解できるかもしれません。リンク先を張っておきます)

筆者が過去に記述した記事3本です。
① Raspberry Pi Pico SDK C/C++の構築 - Tanuki_Bayashin’s diary

② pico-sdk C/C++ での開発の進め方 - Tanuki_Bayashin’s diary

③ ラズパイPicoで使うPico‐SDK C/C++ に関する覚え書き - Tanuki_Bayashin’s diary

ラズベリーパイ財団 ”Getting started with Raspberry Pi Pico”(英語版)の日本語バージョンです。
④ https://datasheets.raspberrypi.com/pico/getting-started-with-pico-JP.pdf

全体の構成のあらましを図1に示します。

図1 全体構成図

a. プログラム冒頭部

プログラムの冒頭部です。
ファイルのインクルード、マクロの定義、グローバル変数の定義を行っています。
グローバル変数を多用したりしています。各部の関数の働きなどを見るときに必要かと思うので、一応乗せておきます。)

リストa-1

//=========================================================
// File Name: Servo_Pendulum.c
// MPU board: Raspberry Pi Pico
// Accelerometer + Gyro sensor: MPU6050
// UART Device   : FT231X(Akizuki)
// 
// 2023/10/10  written by @Tanuki_Bayashin
// 
// This Program is to Control the Inverted Pendulum System.
// version 1.0 (as a Servo_Pendulum.c)
// 
// Apply Mr.DHA Electronic Hobby Study Class
// for Raspberry Pi Pico.
//=========================================================

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include "pico/stdlib.h"
#include "hardware/uart.h"

#include "pico/binary_info.h"
#include "hardware/gpio.h"
#include "hardware/pwm.h"
#include "hardware/adc.h"
#include "hardware/i2c.h"
#include "hardware/timer.h"

//=========================================================
// Port Setting
// DigitalOut;
// I2C i2c(PB_9, PB_8);   //Gyro + Accelerometer (SDA, SCLK)
// Serial uart_usb(USBTX, USBRX); //UART (over USB)
//=========================================================
//UARTに関する部分です
#define UART_ID uart0
#define BAUD_RATE 115200

#define UART_TX_PIN 12    //(GPIO12 #16)
#define UART_RX_PIN 13    //(GPIO13 #17)

// MPU6050を使用するのに必要な部分です。
#define PICO_I2C_SDA_PIN 8 //(GPIO8 #11)
#define PICO_I2C_SCL_PIN 9 //(GPIO9 #12)

#define PI 3.1415926
#define ONE_G 16383  // 2^14 - 1 = 16383

// PWMに関する部分です。サーボモータの制御に必要になってきます。
#define SERVO_1 0 //(GPIO0 #1Pin PWM_A[0])
#define SERVO_2 3 //(GPIO3 #5Pin PWM_B[1])

//パイロットランプ(Pico上のLED)用です。
#define LED_25 25           // LED_25

//=========================================================
//Accelerometer and gyro offset
//=========================================================
//加速度センサ(ジャイロセンサを含む)の読み取り値のオフセットに関する部分です。
// variables for calculate offset
// for offset of accelaration
float acc_Y_offset = 0.0;
float acc_Z_offset = 0.0;
// for offset of angular velocity
float gyro_X_offset = 0.0;

//ラディアンとディグリーの間の換算係数です。
float deg2rad = 0.0174533;  // 3.141593 / 180.
float rad2deg = 57.296;     // 180.0 / 3.141593

// 姿勢のオフセットです。水平の位置からどれだけずれているかを示しています。
float deg_offset = 0.0;    // [deg]
float rad_offset = 0.055;    // [rad]

// 相補フィルタに関する外部変数です。
float angle_int = 0.0, angle_adj = 0.0;
float angle_adj_t, rad_e, rads_e;

//=========================================================
//Accelerometer and gyro data(加速度センサ用です)
//=========================================================
// By default these devices are on bus address 0x68
static int addr = 0x68;

int16_t acceleration[3], gyro[3];
int16_t* temperature;
int sample_num = 1000;

//角度と角速度の計算用です。
float P_angle, P_angle_dot, P_angle_ave, P_angle_dot_ave;
float P_angle2;
float P_angle_data[10] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
float P_angle_dot_data[10] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

//=========================================================
// PWM to Mortor Voltage サーボモータを動かすときに使います。
//=========================================================
uint pwm1_slice_num, pwm2_slice_num;
float volts;
int pwm_value;

//====================================
//Motor control variables 制御に関する部分などです
//====================================
float feedback_rate = 0.02; //sec
struct repeating_timer timer_control, timer_standing, timer_Filter;
bool cancelled_control, cancelled_standing, cancelled_Filter;

static uint32_t t_start = 0, t_end = 0;
static uint32_t t_start_control, t_end_control;
static uint32_t t_start_Filter, t_end_Filter;

float speed_L, speed_R, adj_L, adj_R;
int duty_L, duty_R;

// コントロール・ゲインです
//float k1 = 8.0, k2 = 0.07;
float k1 = 4.5, k2 = 0.013;

//============================

b. UART部

Pico内の変数の値をPC側に送信する部分です。
PC側の受信用のコードは(2-2. PC側のUART受信用ソースコードへのリンク)にあります。(Pythonにて記述)

リストb-1

void uart_initial(){
    // process something important ?
    stdio_init_all();

    // Set up our UART with the required speed.
    uart_init(UART_ID, BAUD_RATE);

    // Set the TX and RX pins by using the function select on the GPIO
    gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
    gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);
    
}

uart_initial():初期設定を行っています。(Pico SDK のサンプル・コードそのままです)

リストb-2 uart_send()

void uart_send(){

    static char s[100];
    snprintf(s, sizeof(s), "%4.3f, %4.3f, %4.3f, %4.3f\n", 
                              rad_e, rads_e, speed_L, speed_R);
    uart_puts(UART_ID, s);
}

実際に変数の値を送信しています。
snprintf()の変数の部分を変えれば、他の変数の値も出力できます。(フォーマット(""ではさまれた部分)も、必要に応じて変更する必要があります)

c. PWM初期設定

PWMによる処理の初期設定を行っています。main()内から呼びだしてます。
サーボモータをPicoで制御する場合、PWM制御にて行います。(詳しい説明は「1.はじめに」でご紹介した動画内にて解説されています。ご参照ください)
20ms周期でPWM信号を発生させています。理論値とだいぶ違いますが、実際にこのリストの値で20msになりました。周期が大きい場合、このような事があるのかもしれないです。
(Pico SDKでのPWMに関する内容は、この章の冒頭部でご紹介した③のリンク先の2.2節をご覧ください)

リストc-1 pwm_setting();

    /* GPIOにPWMを割り当てる */
    gpio_set_function(SERVO_1, GPIO_FUNC_PWM);
    gpio_set_function(SERVO_2, GPIO_FUNC_PWM);

    pwm1_slice_num = pwm_gpio_to_slice_num(SERVO_1);
    pwm2_slice_num = pwm_gpio_to_slice_num(SERVO_2);

    /* clkdiv と wrap を指定 */
    //pwm frequency: 50[Hz]
    //以下の値は理論値とだいぶ違っています。
    pwm_set_clkdiv(pwm1_slice_num, 610.375);
    pwm_set_wrap(pwm1_slice_num, 9764);

    pwm_set_clkdiv(pwm2_slice_num, 610.375);
    pwm_set_wrap(pwm2_slice_num, 9764);

    /* レベル値(デューティカウント値)を設定(ここでは0) */
    pwm_set_chan_level(pwm1_slice_num, PWM_CHAN_A, 732 - 5);
    pwm_set_chan_level(pwm2_slice_num, PWM_CHAN_B, 732 - 5);

    /* pwm0 start */
    pwm_set_enabled(pwm1_slice_num, true);
    pwm_set_enabled(pwm2_slice_num, true);
    /* END of PWM Module Setting */

PWMの初期設定に関する記述です。Pico SDKのサンプル・プログラムにあったものを参考にしました。

d. MPU6050部

 加速度センサ(3自由度)MPU6050を使用したので、これを使用可能にしたり、データを読み取ったりしています。
(ジャイロセンサ(3自由度)も含まれています)

リストd-1 mpu6050_reset()

//=========================================================
// Accelerometer (MPU6050)
// Gyroscope
//========================================================= 
//
#ifdef i2c_default
static void mpu6050_reset() {
    // Two byte reset. First byte register, second byte data
    // There are a load more options to set up the device in different ways
    // that could be added here
    uint8_t buf[] = { 0x6B, 0x00 };
    i2c_write_blocking(i2c_default, addr, buf, 2, false);

    // set P_angle_dot ±500[degree/sec]
    //ジャイロセンサの読みとり値を±500[degree/sec]にしています。
    buf[0] = 0x1B;
    buf[1] = 0x08;  // 0x01 => 0x08 changed(2022/08/11)
    i2c_write_blocking(i2c_default, addr, buf, 2, false);

    // set accelaration ±2G 
    //加速度センサの読みとり値を±2G に設定しています。
    buf[0] = 0x1C;
    buf[1] = 0x00;
    i2c_write_blocking(i2c_default, addr, buf, 2, false);

    // set Digital LPF
    // BW 44Hz(4.9ms delay) for Accel,
    // BW 42Hz(4.8ms delay) for Gyro
    //加速度センサ内部のディジタルフィルタの帯域の設定です。
    buf[0] = 0x1A;
    buf[1] = 0x00;
    i2c_write_blocking(i2c_default, addr, buf, 2, false);

}

#endif

初期設定を行っています。(#ifdef i2c_default などの記述はよく分かっていないです)

リストd-2 get_P_angle()

float get_P_angle() {

    uint8_t buffer[6];
    float sum;
    float x, y;

    // Start reading acceleration registers from register 0x3B for 6 bytes
    uint8_t val = 0x3B;
    // true to keep master control of bus
    i2c_write_blocking(i2c_default, addr, &val, 1, true);
    i2c_read_blocking(i2c_default, addr, buffer, 6, false);

    acceleration[0] = (buffer[0] << 8) | buffer[1];
    acceleration[1] = (buffer[2] << 8) | buffer[3];
    acceleration[2] = (buffer[4] << 8) | buffer[5];

    // 86.03 is the value of compensation for initial posture
    x = (float)acceleration[1] - acc_Y_offset;
    y = (float)acceleration[2] - acc_Z_offset;

    P_angle = - (float)atan2((double)x, (double)y); // [rad]
    //P_angle *= rad2deg; // [deg]

    for (int i = 0; i < 9; i++) {
        P_angle_data[9 - i] = P_angle_data[8 - i];
    }
    P_angle_data[0] = P_angle;

    sum = 0.0;
    for (int i = 0; i < 5; i++) { // 10 -> 5 ** 2023/07/23
        sum += P_angle_data[i];
    }

    P_angle_ave = sum / 5.0; // 10.0 -> 5.0 **

    return P_angle; // [rad]

}

実際に加速度を読み取り、その値から倒立振子の傾斜角を求めています。
まず、加速度を求め、Y軸成分とZ軸成分から傾斜角を求めています。
傾斜角を求める原理は以下の筆者の記事を参考にして下さい。

傾斜計の試作機の製作 - Tanuki_Bayashin’s diary

計算結果の単位はラディアンで、ノイズを減らすために5ステップ分の平均を求めています。

リストd-3 get_P_angle_dot()

float get_P_angle_dot() {

    uint8_t buffer[2];
    float sum;

    // Now gyro data from reg 0x43 for 6 bytes
    uint8_t val = 0x43;
    i2c_write_blocking(i2c_default, addr, &val, 1, true);
    // False - finished with bus
    i2c_read_blocking(i2c_default, addr, buffer, 2, false);

    gyro[0] = (buffer[0] << 8) | buffer[1];

    // 2^15 - 1 = 32767, ±500 [degree/s] for Full range
    // -500を掛けているのは座標系を合わせるため
    P_angle_dot = (gyro[0] - gyro_X_offset) * (-500.0) / 32767.0; // [deg/s]
    P_angle_dot *= deg2rad; // [rad/s]

    for (int i = 0; i < 9; i++) {
        P_angle_dot_data[9 - i] = P_angle_dot_data[8 - i];
    }
    P_angle_dot_data[0] = P_angle_dot;

    sum = 0.0;
    for (int i = 0; i < 10; i++) {
        sum += P_angle_dot_data[i];
    }

    P_angle_dot_ave = sum / 10.0;

    return P_angle_dot;  // [rad/s]
}

倒立振子の傾斜角の時間微分(角加速度)P_angle_dotを求めています。単位は[rad/s]です。
(ジャイロセンサの出力を単位が合うように換算しています。こちらも平均値を求めています)

リストd-4 Calibration()

void Calibration() {

    //// Calculate these variables
    // acc_Y_offset;
    // acc_Z_offset;
    // gyro_X_offset;

    float acc_Y_sum = 0.0;
    float acc_Z_sum = 0.0;
    float gyro_X_sum = 0.0;
    float ONE_G_sin, ONE_G_cos, P_angle_0;

    P_angle_0 = 90.0; // 2023/07/29 measured
    ONE_G_sin = ONE_G * sin(P_angle_0 * PI / 180.0);
    ONE_G_cos = ONE_G * cos(P_angle_0 * PI / 180.0);
    ///////////////////////////////////////////////
    // Calculate offset of acc and gyro raw data //
    // sample_num = 1000                          //
    ///////////////////////////////////////////////
    for (int i = 0; i < sample_num; i++) {
        P_angle = get_P_angle();
        P_angle_dot = get_P_angle_dot();

        // #define ONE_G 16383 = 2^14 - 1
        acc_Y_sum += acceleration[1] - ONE_G_cos;
        acc_Z_sum += acceleration[2] - ONE_G_sin;
        gyro_X_sum += gyro[0];

        // delay 500[μs]
        sleep_us(500);
    }

    // results of calculation of mean values
    acc_Y_offset = acc_Y_sum / sample_num;
    acc_Z_offset = acc_Z_sum / sample_num;
    gyro_X_offset = gyro_X_sum / sample_num;

    return;
}

main()関数内から呼びだしています。加速度センサやジャイロセンサのオフセット値(測定時の一定の誤差)を算出しています。倒立振子を水平にした状謡で電源を入れると、自動で計測します。傾斜角や角速度を計算するときに、利用しています。

リストd-5 mpu6050_initial()

void mpu6050_initial(){

#if !defined(i2c_default) || !defined(PICO_DEFAULT_I2C_SDA_PIN) || !defined(PICO_DEFAULT_I2C_SCL_PIN)
    #warning i2c / mpu6050_i2c example requires a board with I2C pins
    uart_puts(UART_ID, "Default I2C pins were not defined");
#else
    uart_puts(UART_ID, "Hello, MPU6050! Reading raw data from registers...");

    // This example will use I2C0 on the default SDA and SCL pins (4, 5 on a Pico)
    i2c_init(i2c_default, 400 * 1000);
    gpio_set_function(PICO_I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(PICO_I2C_SCL_PIN, GPIO_FUNC_I2C);
    gpio_pull_up(PICO_I2C_SDA_PIN);
    gpio_pull_up(PICO_I2C_SCL_PIN);

    // Make the I2C pins available to picotool
    bi_decl(bi_2pins_with_func(PICO_I2C_SDA_PIN, PICO_I2C_SCL_PIN, GPIO_FUNC_I2C));

    mpu6050_reset();

#endif
    uart_puts(UART_ID, " mpu6050 reset!\n\r");
}

main()関数内から呼びだしています。MPU6050が使えるようにするための初期設定をする関数です。

e. 相補フィルター部

リストe-1 Comple_Filter()

//=========================================================
// Complement Filter Proccess
// (2023/09/15)
//=========================================================
bool Comple_Filter(struct repeating_timer* t)
{
    t_start_Filter = time_us_32();
    
    // 車体の角度と回転速度を計算する(相補フィルタ)

    P_angle = get_P_angle();            // ロボットの角度[rad]
    P_angle_dot = get_P_angle_dot();    // ロボットの角速度[rad/s]

    // 相補フィルタ
    angle_adj_t = angle_adj;
    angle_int += P_angle_dot_ave * 0.002;
    angle_adj = P_angle_ave - angle_int;
    angle_adj = angle_adj_t + 0.002 * (angle_adj - angle_adj_t) * 2.0;

    rad_e = angle_int + angle_adj;
    rads_e = P_angle_dot_ave;

    t_end_Filter = time_us_32() - t_start_Filter;

    return true;
}

「1.はじめに」の動画で紹介されていたフィルタです。傾斜角の値のノイズを除去してくれる処理です。

図2 相補フィルタによるノイズの除去

図2に相補フィルタの動作の様子を示します。赤は傾斜角のデータ、青が相補フィルタの出力です。左の図ではノイズが除去されている様子が見てとれます。また右の図では、ノイズが除去されていると言った感じではないですが、値の変化が緩やかになっているように見えます。

ネットなどで調べてみましたが、その動作原理などは理解できていません。時間のあるときに、詳しく調べたいです。

f. コントローラ部

リストf-1 Control_Pendulum()

//////////////////////////////////////////
// Calculate the velosity of servo motor
// (2023/10/10)
//////////////////////////////////////////
bool Control_Pendulum(struct repeating_timer* t)
{
    //about 400usec passed for this process

    t_start_control = time_us_32();

    // モーターが出す速度の計算
    speed_L = ((rad_e - rad_offset) + rads_e * k2) * k1;

    // speedの最大値・最小値処理
    if (speed_L < -1.0) {
        speed_L = -1.0;
    }
    if (speed_L > 1.0) {
        speed_L = 1.0;
    }
    
    //サーボモータが中性点で停止させるための調整値です。
    adj_L = -5.0;
    adj_R = -5.0;


    //speed_Lの値からspeed_Rを求めています。
    //左右のサーボモータの回転数を合わせるための処理です。
    if (speed_L <= -1.0) {
        speed_R = 1.0;
    }
    else if (speed_L < -0.4) {
        speed_R = -speed_L * 0.826 + 0.00353;

    }
    else if (speed_L < 0.0) {
        speed_R = -speed_L * 0.758 - 0.0299;
    }
    else if (speed_L == 0) {
        speed_R = 0.0;
    }
    else if (speed_L < 0.4) {
        speed_R = -speed_L * 0.8025 - 0.0267;
    }
    else if (speed_L < 1.0) {
        speed_R = -speed_L * 1.182 + 0.158;
    }
    else {
        speed_R = -1.0;
    }

    // モーターのスピードをDuty比に変換しています。
    duty_L = (int)(speed_L * 244.1 + 732.3 + adj_L);
    duty_R = (int)(speed_R * 244.1 + 732.3 + adj_R);

    // MAX:9764 
    /* レベル値(デューティカウント値)を設定 */
    pwm_set_chan_level(pwm1_slice_num, PWM_CHAN_A, duty_L);
    pwm_set_chan_level(pwm2_slice_num, PWM_CHAN_B, duty_R);

    //この処理にかかる時間を計測しています。(およそ400μsecでした)
    t_end_control = time_us_32() - t_start_control;

    return true;
}


倒立振子の傾斜角と角速度のデータから、左右のサーボモータへの指令値を求め、PWMのデューティー比を設定することで、倒立振子のバランスを取るように制御している部分です。

まだバランスがうまく取れていないのですが、いろいろやっている最中に、片方のサーボモータが壊れてしまい、現在手を付けていないです。

g. タイマ割り込み部

タイマ割り込みとは、処理を一定周期で繰り返したいときに使うと便利な機能です。

リストg-1 timer_irq()

void timer_irq(){
    // start main control process (2.0 ms)
    add_repeating_timer_ms(-2, Comple_Filter, NULL, &timer_Filter);
    sleep_ms(1);
    
    // start main control process (20.0 ms)
    add_repeating_timer_ms(-20, Control_Pendulum, NULL, &timer_control);
    sleep_ms(1);
}

相補フィルター部と、コントローラ部を、それぞれ2.0msと20ms周期で繰り返して実行するための処理です。

リストg-2 timer_irq_end()

void timer_irq_end(){

    static char s[100];

    cancelled_control = cancel_repeating_timer(&timer_control);
    uart_puts(UART_ID, "cancelled... _control");
    snprintf(s, sizeof(s), "%d", cancelled_control);
    uart_puts(UART_ID, s);
    uart_puts(UART_ID, "\0");

    sleep_ms(1);

    cancelled_Filter = cancel_repeating_timer(&timer_Filter);
    uart_puts(UART_ID, "cancelled... _Filter");
    snprintf(s, sizeof(s), "%d", cancelled_Filter);
    uart_puts(UART_ID, s);
    uart_puts(UART_ID, "\0");

    sleep_ms(1);

}

タイマ割り込みを終了させるための処理です。(しかしこのプログラムでは、実際に実行されることはまずないです。この上の無限ループから抜け出ることがないためです)

(Pico SDKでのタイマ割り込みに関する内容は、この章の冒頭部でご紹介した③のリンク先の2.4節をご覧ください。正確にはタイマ割り込みとは呼ばないようです)

h. main部

main()関数です。処理はここからスタートし、この処理の中から、上に挙げた関数が呼び出されています。

リストh-1 main()

//=========================================================
// Main
//=========================================================
void main() {

    static char s[100];
    int LED_cnt;

    //rad_offset = deg_offset * deg2rad;
    deg_offset = rad_offset * rad2deg;

    /////////////////////////
    // UART 初期設定       //
    /////////////////////////

    uart_initial();
    sleep_ms(5000);

    uart_puts(UART_ID, " Hello, UART!\n\r");

    /////////////////////////////
    // 加速度センサー 初期設定 //
    /////////////////////////////

    mpu6050_initial();

    /////////////////////////////////////////
    // GPIO(for LED_25) initialize
    /////////////////////////////////////////
    gpio_init(LED_25);
    gpio_set_dir(LED_25, GPIO_OUT);

    ///////////////////////////////////
    // Calibration Start
    ///////////////////////////////////

    //キャリブレーションの処理です
    Calibration();

    snprintf(s, sizeof(s), "%5.3f,%5.3f,%5.3f\n", 
                                acc_Y_offset, acc_Z_offset, gyro_X_offset);
    uart_puts(UART_ID, s);
    sleep_ms(100);

    ///////////////////////////
    // PWM の前処理です      //
    // (Motor_IN1, Motor_IN2)//
    ///////////////////////////

    pwm_setting();

    ///////////////////////////////////////////////
    // 関数 get_P_angle(), get_P_angle_dot() 内で//
    // 平均を求めるときに使用するdata の初期設定   //
    ///////////////////////////////////////////////
    // calculate P_angle [deg] as initial values
    P_angle_data[0] = get_P_angle();
    for (int i = 1; i < 10; i++) {
        P_angle_data[i] = P_angle_data[0];
    }

    // calculate P_angle_dot [deg/s] as initial values
    P_angle_dot_data[0] = get_P_angle_dot();
    for (int i = 1; i < 10; i++) {
        P_angle_dot_data[i] = P_angle_dot_data[0];
    }

    /////////////////////////////////////////////  
    //  Timer
    /////////////////////////////////////////////

    //タイマ割り込みを設定しています
    timer_irq();

    uart_puts(UART_ID, "Start!!\n");
    sleep_ms(1);
 
    //===========================================
    //Main loop
    //it takes 10 msec (calculation)
    //===========================================

    LED_cnt = 0;
    while(1)
    {
        t_start = time_us_32();
        
    //ここでUARTにてデータをPC側に送信しています
        uart_send();

    //Picoの基板上のLEDを点滅させています。
    //Picoの動作が正常であることを示すためでもあります。
        LED_cnt++;
        if (LED_cnt > 50) {
            LED_cnt = 0;
        }
        if (LED_cnt > 25) {
            gpio_put(LED_25, 1); // LED_25 is ON
        }
        else {
            gpio_put(LED_25, 0); // LED_25 is OFF
        }


        sleep_ms(20);

        t_end = time_us_32() - t_start;
    }
    //===========================================
    //Main loop (end)
    //=========================================== 

    //タイマ割り込みを停止しています
    timer_irq_end();

    return;
}

コメント文にておおよその動作を説明しています。今まで上に挙げた関数を呼び出している感じです。

2-2. PC側のUART受信用ソースコード

PC側での受信用コードを示します。(Teratermなどでも見ることはできます。データを保存すれば、グラフに表示することもできます)

リストi-1 PC側のソースコード

#ライブラリを呼びだしています
import csv
import serial
import time

#=================================================
# プログラムの起点
#=================================================
if __name__  == '__main__':

    data = []

    #適切なCOMポートを指定してください
    COM = "COM13"
    bitRate = 115200
     
    #受信用ポートをOpen
    ser = serial.Serial(COM, bitRate, timeout=0.1)

    #受信用ポート番号を確認
    print ('受信用ポート番号:',ser.portstr)
    print('受信テスト')

    data = []
    count = 0
    try:
        while True:
            #データを読み込んでいます。
            #utf-8でコーディング。エラーを非表示にしています
            a=ser.readline()
            data = a.decode('utf-8', errors='ignore').strip()

            count += 1

            #データを受信したとき、PCに表示しています
            if len(data) != 0:
                print(count, data)
            time.sleep(0.005)

    # ctl-C が押されたときの処理です
    except KeyboardInterrupt:
        exit(0)    #強制終了します

4.動画

まず回路に電気を流さない状態で、バランスの具合を見てみました。
なんと、カーペットの上だとバランスが取れてしまいました。(笑)

youtu.be

次は通電した状態での様子です。撮影時のアングルがいまいちですが、バランスは取れています。

youtu.be

しかし、机の上で同じことをすると、徐々に揺れが大きくなって、バランスを崩してしまいました。(汗)

5.まとめ

以上、今回の取り組みを紹介しました。
まだ十分にバランスが取れているとは言えないですが、一応補助輪付きで合格と自分では判断しています。
今後に期待したいです。

お付き合いくださり、ありがとうございました。

ハード編へのリンク先:
サーボモータで倒立振子を作ってみた~ハード編 - Tanuki_Bayashin’s diary