|
| 1 | +BLE Servo Project |
| 2 | +-------------------------- |
| 3 | +This tutorial will give you an insight on how ``bleservo`` sample application works. |
| 4 | + |
| 5 | +.. contents:: |
| 6 | + :local: |
| 7 | + :depth: 2 |
| 8 | + |
| 9 | +Prerequisites |
| 10 | +~~~~~~~~~~~~~ |
| 11 | +- Complete other tutorials like: :doc:`Project Blinky <../blinky/blinky>` and :doc:`BLE peripheral project tutorial <bleprph/bleprph>` to famliarize yourself with MyNewt and BLE |
| 12 | +- Nordic nRF52 Development board - PCA 10056 |
| 13 | +- Any simple servomotor to test the application |
| 14 | + |
| 15 | +GATT Server |
| 16 | +~~~~~~~~~~~ |
| 17 | + |
| 18 | +GATT in this application contains only one custom service, which contains two characteristics with read, write and notify permissions. |
| 19 | +Both characteristics contain information about PWM pulse width, however one of them stores this information in the form of servo's tilt angle [degrees] and the other one in pulse duration [microseconds]. |
| 20 | +This allows the user to control servo in two ways, either with angle or with PWM pulse duration. |
| 21 | + |
| 22 | +Notifications are used to update user's information on both values if one of them has changed. The other corresponding value is calculated before sending a notification, what will be described below, in section PWM Configuration. |
| 23 | + |
| 24 | +.. code:: c |
| 25 | +
|
| 26 | + static const struct ble_gatt_svc_def gatt_svr_svcs[] = { |
| 27 | + { |
| 28 | + /* Service: Servo */ |
| 29 | + .type = BLE_GATT_SVC_TYPE_PRIMARY, |
| 30 | + .uuid = &gatt_svr_svc_servo_uuid.u, |
| 31 | + .characteristics = (struct ble_gatt_chr_def[]) { { |
| 32 | + /* Characteristic: servo angle*/ |
| 33 | + .uuid = &gatt_svr_chr_servo_angle_uuid.u, |
| 34 | + .val_handle = &gatt_angle_val_handle, |
| 35 | + .access_cb = gatt_svr_chr_access_servo_angle, |
| 36 | + .flags = BLE_GATT_CHR_F_READ | |
| 37 | + BLE_GATT_CHR_F_WRITE | |
| 38 | + BLE_GATT_CHR_F_NOTIFY |
| 39 | + }, { |
| 40 | + /* Characteristic: servo PWM pulse duration*/ |
| 41 | + .uuid = &gatt_svr_chr_servo_pulse_duration_uuid.u, |
| 42 | + .val_handle = &gatt_pulse_duration_val_handle, |
| 43 | + .access_cb = gatt_svr_chr_access_servo_pulse_duration, |
| 44 | + .flags = BLE_GATT_CHR_F_READ | |
| 45 | + BLE_GATT_CHR_F_WRITE | |
| 46 | + BLE_GATT_CHR_F_NOTIFY |
| 47 | + }, { |
| 48 | + 0, /* No more characteristics in this service */ |
| 49 | + }, } |
| 50 | + }, |
| 51 | +
|
| 52 | + { |
| 53 | + 0, /* No more services */ |
| 54 | + }, |
| 55 | + }; |
| 56 | +
|
| 57 | + static int |
| 58 | + gatt_svr_chr_access_servo_angle(uint16_t conn_handle, uint16_t attr_handle, |
| 59 | + struct ble_gatt_access_ctxt *ctxt, void *arg) |
| 60 | + { |
| 61 | + int rc; |
| 62 | + switch (ctxt->op) { |
| 63 | + case BLE_GATT_ACCESS_OP_READ_CHR: |
| 64 | + gatt_angle_val = get_servo_angle(); |
| 65 | + rc = os_mbuf_append(ctxt->om, &gatt_angle_val, |
| 66 | + sizeof gatt_angle_val); |
| 67 | + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; |
| 68 | +
|
| 69 | + case BLE_GATT_ACCESS_OP_WRITE_CHR: |
| 70 | + rc = gatt_svr_chr_write(ctxt->om, |
| 71 | + sizeof gatt_angle_val, |
| 72 | + sizeof gatt_angle_val, |
| 73 | + &gatt_angle_val, NULL); |
| 74 | + servo_angle_setter(gatt_angle_val); |
| 75 | +
|
| 76 | + return rc; |
| 77 | + default: |
| 78 | + assert(0); |
| 79 | + return BLE_ATT_ERR_UNLIKELY; |
| 80 | + } |
| 81 | + } |
| 82 | +
|
| 83 | + static int |
| 84 | + gatt_svr_chr_access_servo_pulse_duration(uint16_t conn_handle, uint16_t attr_handle, |
| 85 | + struct ble_gatt_access_ctxt *ctxt, void *arg) |
| 86 | + { |
| 87 | + int rc; |
| 88 | + switch (ctxt->op) { |
| 89 | + case BLE_GATT_ACCESS_OP_READ_CHR: |
| 90 | + gatt_pulse_duration_val = get_servo_pwm_pulse_duration(); |
| 91 | + rc = os_mbuf_append(ctxt->om, &gatt_pulse_duration_val, |
| 92 | + sizeof gatt_pulse_duration_val); |
| 93 | + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; |
| 94 | +
|
| 95 | + case BLE_GATT_ACCESS_OP_WRITE_CHR: |
| 96 | + rc = gatt_svr_chr_write(ctxt->om, |
| 97 | + sizeof gatt_pulse_duration_val, |
| 98 | + sizeof gatt_pulse_duration_val, |
| 99 | + &gatt_pulse_duration_val, NULL); |
| 100 | + servo_pwm_pulse_duration_setter(gatt_pulse_duration_val); |
| 101 | +
|
| 102 | + return rc; |
| 103 | + default: |
| 104 | + assert(0); |
| 105 | + return BLE_ATT_ERR_UNLIKELY; |
| 106 | + } |
| 107 | + } |
| 108 | + [...] |
| 109 | +
|
| 110 | +
|
| 111 | +Write callbacks of each characteristic call angle or pulse duration setter function. Both files ``gatt_svr.c`` and ``main.c`` need the access to this variables and thanks to this solution, the extern keyword wasn't used in the application. In addition, the boundary conditions of the updated value are checked in this functions and in the end, PWM fracture update event is put to the queue. ``was_angle_passed`` variable is used to keep track on which of the variables has been passed and changed. |
| 112 | + |
| 113 | +.. code:: c |
| 114 | +
|
| 115 | + /* Servo angle setter. Used in gatt_svr.c after receiving new angle value. */ |
| 116 | + void |
| 117 | + servo_angle_setter(uint16_t gatt_value) |
| 118 | + { |
| 119 | + if (gatt_value > SERVO_MAX_ANGLE_VAL) { |
| 120 | + servo_angle = SERVO_MAX_ANGLE_VAL; |
| 121 | + } else if (gatt_value < SERVO_MIN_ANGLE_VAL) { |
| 122 | + servo_angle = SERVO_MIN_ANGLE_VAL; |
| 123 | + } else { |
| 124 | + servo_angle = gatt_value; |
| 125 | + } |
| 126 | +
|
| 127 | + was_angle_passed = 1; |
| 128 | +
|
| 129 | + /* After changing servo angle value an event to update PWM fraction is put to the queue. */ |
| 130 | + os_eventq_put(os_eventq_dflt_get(), &pwm_frac_update_ev); |
| 131 | + } |
| 132 | +
|
| 133 | + /* Servo PWM pulse duration setter. Called in gatt_svr.c after receiving new pulse duration value. */ |
| 134 | + void |
| 135 | + servo_pwm_pulse_duration_setter(uint16_t gatt_value) |
| 136 | + { |
| 137 | + if (gatt_value > SERVO_MAX_PULSE_DURATION_US) { |
| 138 | + servo_pwm_pulse_duration = SERVO_MAX_PULSE_DURATION_US; |
| 139 | + } else if (gatt_value < SERVO_MIN_PULSE_DURATION_US) { |
| 140 | + servo_pwm_pulse_duration = SERVO_MIN_PULSE_DURATION_US; |
| 141 | + } else { |
| 142 | + servo_pwm_pulse_duration = gatt_value; |
| 143 | + } |
| 144 | +
|
| 145 | + was_angle_passed = 0; |
| 146 | +
|
| 147 | + /* After changing servo PWM pulse duration value an event to update PWM fraction is put to the queue. */ |
| 148 | + os_eventq_put(os_eventq_dflt_get(), &pwm_frac_update_ev); |
| 149 | + } |
| 150 | +
|
| 151 | +PWM configuration |
| 152 | +~~~~~~~~~~~~~~~~~ |
| 153 | + |
| 154 | +PWM is configured using two structures: ``pwm_chan_cfg`` and ``pwm_dev_cfg``. Fields initialized with ``NULL`` are related to additional PWM features, which we won't use. |
| 155 | + |
| 156 | +In MyNewt PWM fracture setting is based on number of clock cycles, but the application's characteristics contain variables expressed in more human-friendly units: tilt angle [degrees] or pulse duration [us]. |
| 157 | +The conversion of this variables is necessary, so the application provides four conversion functions. Each of them transforms one variable into the corresponding value of other variable. |
| 158 | +In the ``servo_pwm_init()`` function, after configuration of the PWM, top value of mentioned clock cycles is being assigned to the variable ``pwm_top_val``. |
| 159 | +This variable is later used as reference in calculating corresponding values of variables in different units. |
| 160 | + |
| 161 | +.. code:: c |
| 162 | +
|
| 163 | + int |
| 164 | + servo_pwm_init(void) |
| 165 | + { |
| 166 | + struct pwm_chan_cfg chan_conf = { |
| 167 | + .pin = PWM_CH_CFG_PIN, |
| 168 | + .inverted = PWM_CH_CFG_INV, |
| 169 | + .data = NULL, |
| 170 | + }; |
| 171 | + struct pwm_dev_cfg dev_conf = { |
| 172 | + .n_cycles = 0, |
| 173 | + .int_prio = PWM_IRQ_PRIO, |
| 174 | + .cycle_handler = NULL, |
| 175 | + .seq_end_handler = NULL, |
| 176 | + .cycle_data = NULL, |
| 177 | + .seq_end_data = NULL, |
| 178 | + .data = NULL |
| 179 | + }; |
| 180 | +
|
| 181 | + int rc; |
| 182 | +
|
| 183 | + servo = (struct pwm_dev *)os_dev_open("pwm0", 0, NULL); |
| 184 | + if (!servo) { |
| 185 | + console_printf("Device pwm0 not available\n"); |
| 186 | + return 0; |
| 187 | + } |
| 188 | +
|
| 189 | + pwm_configure_device(servo, &dev_conf); |
| 190 | +
|
| 191 | + rc = pwm_set_frequency(servo, SERVO_PWM_FREQ); |
| 192 | + assert(rc > 0); |
| 193 | + rc = pwm_configure_channel(servo, PWM_CH_NUM, &chan_conf); |
| 194 | + assert(rc == 0); |
| 195 | +
|
| 196 | + /* Calculate minimum fracture value */ |
| 197 | + pwm_top_val = (uint16_t) pwm_get_top_value(servo); |
| 198 | + servo_frac_max_val = us_to_frac(SERVO_MAX_PULSE_DURATION_US); |
| 199 | + servo_frac_min_val = us_to_frac(SERVO_MIN_PULSE_DURATION_US); |
| 200 | + frac = servo_frac_min_val; |
| 201 | +
|
| 202 | + /* At the beginning of working of the app PWM fracture is set to minimum */ |
| 203 | + rc = pwm_set_duty_cycle(servo, PWM_CH_NUM, frac); |
| 204 | + rc = pwm_enable(servo); |
| 205 | + assert(rc == 0); |
| 206 | +
|
| 207 | + return rc; |
| 208 | + } |
| 209 | + |
| 210 | + [...] |
| 211 | + |
| 212 | + /* Conversion functions */ |
| 213 | + uint16_t |
| 214 | + angle_to_frac(uint16_t angle) |
| 215 | + { |
| 216 | + return servo_frac_min_val + (angle * (servo_frac_max_val - servo_frac_min_val) / SERVO_MAX_ANGLE_VAL); |
| 217 | + } |
| 218 | +
|
| 219 | + uint16_t |
| 220 | + us_to_frac(uint16_t us) |
| 221 | + { |
| 222 | + return (us * pwm_top_val) / SERVO_PWM_FULL_CYCLE_DURATION; |
| 223 | + } |
| 224 | +
|
| 225 | + uint16_t |
| 226 | + frac_to_angle(uint16_t frac_) |
| 227 | + { |
| 228 | + return (SERVO_MAX_ANGLE_VAL * frac_ - SERVO_MAX_ANGLE_VAL * servo_frac_min_val) / |
| 229 | + (servo_frac_max_val - servo_frac_min_val); |
| 230 | + } |
| 231 | +
|
| 232 | + uint16_t |
| 233 | + frac_to_us(uint16_t frac_) |
| 234 | + { |
| 235 | + return (SERVO_PWM_FULL_CYCLE_DURATION * frac_) / pwm_top_val; |
| 236 | + } |
| 237 | +
|
| 238 | +Servo wiring |
| 239 | +~~~~~~~~~~~~ |
| 240 | + |
| 241 | +Servo's PWM signal wire should be connected to the P0.31 pin of the nRF52840 board. You should also remember about providing the common ground for the nRF52 board and servo's power source. |
| 242 | + |
| 243 | +.. figure:: ../pics/servo_conn.jpg |
| 244 | + :alt: Servo wiring |
| 245 | + |
0 commit comments