Bei einigen Anwendungen des pico benötigt man ein Userinterface das Werte anzeigt und Änderungen dieser zu lässt. Eine der günstigsten und auch elegantesten Methoden ist ein OLED-Display SH1106 mit einem Dreh/Druck-Schalter wie z.B KY-040 zur bedienung.
Wie Integriert man nun ein solches Userinterface seine Anwendung? Im Folgendem ist eine 4-Kanal PWM-Geber Anwendung geschrieben die in einer Dauerschleife die Rückmeldung des KY-040 auswertet und das Display aktualisiert. Das Programm umfasst nur wenige Zeilen Code da es auf drei bereits erstellte Bibliotheken (sh_1106, drehgeber, momefilo_flash[das Prog speichert die Werte bei Spannungsausfall]) zurückgreift welche entsprechende Grundfunktionen bereitstellen. Deren Code Im Anschluss zum Verständis ebenfalls gepostet ist
Es gibt auch eine fertige .uf2 Datei sowie ein Repository des pwm_geber und der benötigten Bibliotheken
Anwendungscode pwm_geber
// pwm_geber by momefilo Desing
/* In dieser Header-Datei muessen die Pins und der I2C-Kanal angepasst werden
* Standart sda=4, sdc=5, kanal=0 */
#include "sh_1106.h"
/* In dieser Header-Datei muessen die Pins des KY-040 angepasst werden
* Standart CKL=16, DT=17, SW=18 */
#include "drehgeber.h"
#include "momefilo_flash.h"
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/time.h"
#include "hardware/irq.h"
#include "hardware/pwm.h"
// Die GPIO-Belegung
#define PWM_PIN1 19
#define PWM_PIN2 20
#define PWM_PIN3 21
#define PWM_PIN4 22
/* Der selektierte PWM-Kanal 1-4
* Wird von mark_pwm(bool up) geaendert */
uint8_t Auswahl = 1;
/* Das Werte-Area der Kanaele zur Laufzeit
* wird von chn_wert(bool up) geaendert */
uint32_t Werte[] = {0, 0, 0, 0};
/* Wird mit einem Druck auf den Drehgeber true
* und mit widerholtem Druck false */
bool if_selct = false;
/* Zeichnet das Menu auf das sh1106-Display
* wird einmalig von main() aufgerufen */
void paint_menu(){
// Der Textspeicher der geschrieben wird
char buf[10];
// Vier Kanaele werden geschrieben
for(int i=0; i<4; i++){
// der angezeigte Text pro Kanal
sprintf(buf, "PWM %d %.3i", i+1, Werte[i]);
// die Schreibfunktion des Display erwartet ein uint8_t array {x, y} in Zeichen
uint8_t pos[]={0,i};
// der letze bool-Parameter legt eine invertierte darstellung des Text fest wenn true
sh1106_text12x16(pos, buf, false);
}
}
/* Markiert den selektierte PWM-Kanal im Menu
* wird aus der Dauerschleife in main() aufgerufen */
void mark_pwm(bool up){
char buf[5];
// erst wird der bisher Markierte text demarkiert
sprintf(buf, "PWM %d", Auswahl);
uint8_t pos[]={0,Auswahl - 1};
sh1106_text12x16(pos, buf, false);
// dann die Auswahl ensprechend geaendert
if(up && Auswahl < 4) Auswahl++;
if(!up && Auswahl > 1) Auswahl--;
sprintf(buf, "PWM %d", Auswahl);
// dann der aktuelle Kanal markiert
pos[1] = Auswahl - 1;
sh1106_text12x16(pos, buf, true);
}
/* Markiert den selektierte PWM-Wert im Menu
* und demarkiert den selektierte PWM-Kanal im Menu
* wird aus der Dauerschleife in main() aufgerufen */
void select_pwm(bool select){
char buf[5];
// PWM-Kanal demarkieren
sprintf(buf, "PWM %d", Auswahl);
uint8_t pos[]={0,Auswahl - 1};
sh1106_text12x16(pos, buf, !select);
// PWM-Wert markieren
sprintf(buf, " %.3i", Werte[Auswahl - 1]);
pos[0] = 5;
sh1106_text12x16(pos, buf, select);
}
/* Schreibt den ausgewaehlten PWM-Wert auf den Ausgang
* wird von chn_wert(bool up) aufgerufen */
void set_pwmwert(){
// Hoechster PWM-Wert ist 256*256-1 und wird auf 100% skaliert
float wert = (256*256-1)/100.0;
// Je nach aktueller Auswahl wird der Wert auf den Ausgang geschrieben
if(Auswahl == 1) { pwm_set_gpio_level(PWM_PIN1, wert * Werte[0]); }
else if(Auswahl == 2) { pwm_set_gpio_level(PWM_PIN2, wert * Werte[1]); }
else if(Auswahl == 3) { pwm_set_gpio_level(PWM_PIN3, wert * Werte[2]); }
else if(Auswahl == 4) { pwm_set_gpio_level(PWM_PIN4, wert * Werte[3]); }
}
/* Aendert den ausgewaehlten PWM-Wert
* wird aus der Dauerschleife in main() aufgerufen */
void chn_wert(bool up){
// Aendern des PWM-Wertes entsprechend der aktuellen Auswahl
if(up && Werte[Auswahl - 1] < 100) Werte[Auswahl - 1]++;
if(!up && Werte[Auswahl - 1] > 0) Werte[Auswahl - 1]--;
char buf[5];
// Den geaenderten Wert auf das Display schreiben
sprintf(buf, " %.3i", Werte[Auswahl - 1]);
uint8_t pos[]={5,Auswahl - 1};
sh1106_text12x16(pos, buf, true);
// Den geaenderten Wert in den Ausgang schreiben
set_pwmwert();
}
/* Initialisiert die PWM-Kanaele
* wird einmalig aus main() aufgerufen */
void init_pwm(){
// PWM-Funktion auf den entsprechenden GPIOs aktivieren
gpio_set_function(PWM_PIN1, GPIO_FUNC_PWM);
gpio_set_function(PWM_PIN2, GPIO_FUNC_PWM);
gpio_set_function(PWM_PIN3, GPIO_FUNC_PWM);
gpio_set_function(PWM_PIN4, GPIO_FUNC_PWM);
pwm_set_enabled(pwm_gpio_to_slice_num(PWM_PIN1), true);
pwm_set_enabled(pwm_gpio_to_slice_num(PWM_PIN2), true);
pwm_set_enabled(pwm_gpio_to_slice_num(PWM_PIN3), true);
pwm_set_enabled(pwm_gpio_to_slice_num(PWM_PIN4), true);
// Die gespeicherten Werte in die Ausgaenge schreiben
float wert = (256*256-1)/100.0;
pwm_set_gpio_level(PWM_PIN1, wert * Werte[0]);
pwm_set_gpio_level(PWM_PIN2, wert * Werte[1]);
pwm_set_gpio_level(PWM_PIN3, wert * Werte[2]);
pwm_set_gpio_level(PWM_PIN4, wert * Werte[3]);
}
int main() {
/* Initialisiert den 10ten 4kb Sektor vom oberen Ende des Flash */
flash_init(10);
/* Liest die gespeicherten PWM-Werte aus dem Flash */
uint32_t *flwerte = flash_getData();
/* und speichert diese im Werte-Arrea */
for(int i=0; i<4; i++) Werte[i] = flwerte[i];
init_pwm();// Initialisiert die PWM-Ausgaenge
sh1106_init();// Initialisiert das Display
sh1106_clear_screen();
paint_menu();// Zeichnet das gesamte Menu auf das Display
mark_pwm(false);// Markiert PWM-Kanal 1
while(true){
/* Staendig wird der Drehgeber abgefragt */
uint8_t richtung = drehgeber_get();
if(richtung == IN_LEFT){// ist die Drehung linksherum?
if(if_selct){chn_wert(false);}// Der Knopf ist zum ersten mal gedrueckt
else mark_pwm(false);// Der Knopf ist nicht oder zum zweiten mal gedrueckt
}
else if(richtung == IN_RIGHT){
if(if_selct){chn_wert(true);}// aendere den Wert des ausgewaehlten Kanal
else mark_pwm(true);// markiere den nacsten Kanal
}
else if(richtung == IN_PRESS){// ist der Knopf gedrueckt?
if(!if_selct){//ist der Knopf zum ersten mal gedrueckt
select_pwm(true);// Kanal auswaehlen
if_selct = true;
}
else{//ist der Knopf zum zweiten mal gedrueckt
select_pwm(false);// Kanal abwaehlen
// Speichere PWM-Werte im Flashspeicher
flash_setDataRow(0, 3, Werte);
if_selct = false;
}
}
}
}
Display More
Bibliothekscode drehgeber:
// drehgeber Bibliothek
#include "drehgeber.h"
#include "pico/stdlib.h"
/* Die Mindest-Pulsdauer in ms
* An Hardware anzupassen */
uint8_t SwPulsdauer = 100, DtPulsdauer = 1, CklPulsdauer = 1;
/* Der Mindestabstand zwischen den pulsen in ms
* An Hardware anzupassen */
uint16_t SwPausendauer = 400, DtPausendauer = 4, CklPausendauer = 4;
/* Der Mindestabstnd zwischen den Schaltwechseln
* An Hardware anzupassen */
uint16_t Wechseldauer = 100;
/* Mindestanzahl der Pulse pro Richtung
* An Hardware anzupassen */
uint8_t Tiks = 3;
// Pulszaehler
uint8_t DtTiks = 0, CklTiks = 0;
// Pulsflanken-Speicher
bool CklDown = false, DtDown= false, SwDown = false;
int drehgeber_get(){
// CKL vor DT = RECHTS
if( (! gpio_get(PIN_CKL)) && gpio_get(PIN_DT)){
DtTiks = 0;
DtDown = false;
// Begin des Pulses
if( ! CklDown){
CklDown = true;
sleep_ms(CklPulsdauer);
}// Wenn der Puls laenger als die Pulsdauer anstand wird er erfasst
else if(CklTiks < Tiks){
CklTiks++;
CklDown = false;
sleep_ms(CklPausendauer);
}// Sind vier Pulse erfasst ist der Geber um eine Stelle veraendert
else{
CklTiks = 0;
CklDown = false;
sleep_ms(Wechseldauer);
return IN_RIGHT;
}
}// DT vor CKL = LINKS
else if( (! gpio_get(PIN_DT)) && gpio_get(PIN_CKL)){
CklTiks = 0;
CklDown = false;
// Begin des Pulses
if( ! DtDown){
DtDown = true;
sleep_ms(DtPulsdauer);
}// Wenn der Puls laenger als die Pulsdauer anstand wird er erfasst
else if(DtTiks < Tiks){
DtTiks++;
DtDown = false;
sleep_ms(DtPausendauer);
}// Sind vier Pulse erfasst ist der Geber um eine Stelle veraendert
else{
DtTiks = 0;
DtDown = false;
sleep_ms(Wechseldauer);
return IN_LEFT;
}
}// Ende der Bewegung
else if(gpio_get(PIN_DT) && gpio_get(PIN_CKL)){
CklDown = false;
DtDown = false;
DtTiks = 0;
CklTiks = 0;
if(! gpio_get(PIN_SW)){
if( ! SwDown){
SwDown = true;
sleep_ms(SwPulsdauer);
}
else{
SwDown = false;
sleep_ms(Wechseldauer);
return IN_PRESS;
}
}
}
return 0;
}
void drehgeber_init(){
gpio_init(PIN_CKL);
gpio_set_input_enabled(PIN_CKL, true);
gpio_set_pulls(PIN_CKL, false,false);
gpio_init(PIN_DT);
gpio_set_input_enabled(PIN_DT, true);
gpio_set_pulls(PIN_DT, false,false);
gpio_init(PIN_SW);
gpio_set_input_enabled(PIN_SW, true);
gpio_set_pulls(PIN_SW, false,false);
}
Display More
Bibliothekscode sh_1106:
// sh_1106 Bibliothek
#include "sh_1106.h"
#include "font12x16.h"
#include "font8x8.h"
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "hardware/gpio.h"
#include <stdio.h>
#include <string.h>
void snd_cmd(uint8_t cmd){
uint8_t buf[2] = {0x80, cmd};
//Attempt to write specified number of bytes to address, blocking.
i2c_write_blocking(i2c_get_instance(I2C_BUS), I2C_ADR, buf, 2, false);
}
void snd_data(uint8_t *data, uint16_t len){
uint8_t buf[len +1];
buf[0] = 0x40;
for(int i=1; i<len+1; i++){ buf[i] = data[i-1]; }
i2c_write_blocking(i2c_get_instance(I2C_BUS), I2C_ADR, buf, len + 1, false);
}
void set_startpoit(uint8_t col, uint8_t page){
snd_cmd(0x0F & col); // col low-bits
snd_cmd(0x10 | (0x0F & (col>>4))); // col high-bits
snd_cmd(0xB0 | (0x0F & page)); //page bits
}
void clear_screen(){
uint8_t buf[132];
for(int i=0; i<132; i++){ buf[i] = 0x00; }
for(int i=0; i<8; i++){
set_startpoit(0,i);
snd_data(buf, 132);
}
}
void sh1106_text12x16(uint8_t *pos, uint8_t *text, bool invert){
// Zeichen fuer Zeichen
for(int i=0; i<strlen(text); i++){
//Es werden je 8x12 Bit fuer jede Page geschrieben
for(int page=0; page<2; page++){
uint8_t *array;
if(page<1){// Dies ist die obere Page
set_startpoit(i*12 + pos[0]*12 + 2, pos[1] * 2);
array = FONT_12x16[text[i]];
if(invert)array = FONT_12x16invert[text[i]];
}
else{// Des ist die untere Page
set_startpoit(i*12 + pos[0]*12 + 2, pos[1] * 2 + 1);
array = FONT_12x16[text[i]] + 12;
if(invert)array = FONT_12x16invert[text[i]] + 12;
}
snd_data(array, 12);
}
}
}
void sh1106_text8x8(uint8_t *pos, uint8_t *text, bool invert){
// Zeichen fuer Zeichen
for(int i=0; i<strlen(text); i++){
//Zeichen um 90grad im Uhrzeiersinn drehen
uint8_t *zeichen = FONT_8x8[text[i]];
if(invert)zeichen = FONT_8x8invert[text[i]];
uint8_t zeichen_gedreht[8];
for(int k=0; k<8; k++){
uint8_t col = 0, colmask = 0x01, rowmask = 0x01;
for(int a=0; a<8; a++){
if(zeichen[a] & rowmask << k){
col = col | colmask << a;
}
}
zeichen_gedreht[k] = col;
}
set_startpoit(i*8 + pos[0]*8 + 2, pos[1]);
snd_data(zeichen_gedreht, 8);
}
}
void sh1106_init(){
i2c_init(i2c_get_instance(I2C_BUS) , I2C_BAUDRATE);
gpio_init(PIN_SCL);
gpio_init(PIN_SDA);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SCL);
gpio_pull_up(PIN_SDA);
snd_cmd(0xAE); // off
snd_cmd(0x40); // start_line 0
snd_cmd(0xD3); // ofset double byte
snd_cmd(0x00); // ofset 0
snd_cmd(0xA1); // von re nach li
snd_cmd(0xC8); // von unten nach oben
snd_cmd(0x81); // kontrast double byte
snd_cmd(0xFF); // voll kontrast
snd_cmd(0xA4); // ganzen Display nutzen
snd_cmd(0xA6); // nicht invertiert (A7 = invertiert)
snd_cmd(0xAF); // on
}
Display More
Bibliothekscode momefilo_flash:
// momefilo_flash Bibliothek
#include "momefilo_flash.h"
#include "hardware/flash.h"
#include "hardware/sync.h"
// die Anfangsadesse des Sektors
uint8_t *Flash_Content;
// offset zur berechnung von *Flash_Content
uint32_t Flash_Offset;
/*
* Jeder Sektor ist in 16 Pages mit je 256 Byte unterteilt die einzeln
* beschrieben, und nur der Sektor im ganzen gelöscht werden koennen.
* Pagecount ist die aktuelle der 16 Pages welche das Data[]-Array beinhaltet
* */
uint8_t Pagecount;
// Der zur Verfügung gestellte Speicherbereich
uint32_t Data[63];
/* Liest anhand Pagecount und Flash_content die Daten aus dem Flash
* in das uint32_t Array*/
void get_Flash(){
int addr;
if(Pagecount < 1){
addr = 15 * 256;
}else{ addr = (Pagecount - 1) * 256;}
uint8_t bufcount = 0;
for(int i=0; i<63; i++){
for(int k=0; k<4; k++){
Data[i] |= (Flash_Content[addr + 2 + bufcount]) << (24 - 8*k);
bufcount++;
}
}
}
/* Speichert die Anfangsadresse des Sektors in Flash_Content
* Setzt den Pagecount
* Ermittelt im Sktor vom Ende herab die aktuelle 256 byte umfassende
* Page deren erste vier Byte 0xABBA als erkennungszeichen enthalten
* die von unten beginnend genuntz sind.
* */
void flash_init(uint8_t sektor){
Flash_Offset = (PICO_FLASH_SIZE_BYTES - (sektor + 1) * FLASH_SECTOR_SIZE);
Flash_Content = (uint8_t *) (XIP_BASE + Flash_Offset);
for(uint8_t i=0; i<63; i++){ Data[i] = 0; }
bool found = false;
Pagecount = 15;
for(int pc=Pagecount; pc>=0; pc--){
uint16_t addr = pc * 256;
if( (Flash_Content[addr] == 0xAB) && \
(Flash_Content[addr+1] == 0xBA)){
found = true;
Pagecount = pc + 1;
get_Flash();
return;
}
}
if(! found) Pagecount = 16;
}
/* Pueft anhand von Pagecount ob die letzte der 16 Pages erreicht ist
* und loescht in diesem Falle den gesamten 4096Byte-Sektor bevor die
* neue Page 0 geschrieben wird*/
void write_Flash(){
uint32_t flags = save_and_disable_interrupts();
uint8_t buf[FLASH_PAGE_SIZE];
if(Pagecount > 15){
flash_range_erase(Flash_Offset, FLASH_SECTOR_SIZE);
Pagecount = 0;
}
buf[0] = 0xAB;
buf[1] = 0xBA;
uint8_t bufcount = 0;
for(int i=0; i<63; i++){
uint32_t mask = 0xFF000000;
for(int k=0; k<4; k++){
buf[2 + bufcount] = (Data[i] & mask) >> (24 - 8*k);
mask = mask >> 8;
bufcount++;
}
}
flash_range_program(Flash_Offset + Pagecount * FLASH_PAGE_SIZE, buf, FLASH_PAGE_SIZE);
restore_interrupts(flags);
Pagecount++;
}
/* Gibt die Anfangsadresse des uint32_t Arrays im Flash zurueck*/
uint32_t *flash_getData(){
return Data;
}
void flash_setData(uint8_t id, uint32_t data){
Data[id] = data;
write_Flash();
}
void flash_setDataRow(uint8_t start, uint8_t end, uint32_t *data){
for(uint8_t i=start; i<=end; i++){
Data[i] = data[i - start];
}
write_Flash();
}
Display More