i2cバスに、4桁の7セグメントLEDをつなぎました。コントローラはht16k33で、LEDマトリクスを、少ない配線数で利用できるデバイスです。
PCのキーボードも、スイッチがマトリクス状に接続されていますね。4ビット・マイコンの8048が原形だったと思いますが、ほとんどの人が専用LSIだと思っているかもしれません。
ht16k33のデータシートには、レジスタの説明が全部掲載されていないので、Gihubにある、複数の人のコードからレジスタのアドレスなどを得ています。いつもありがとうございます。
環境
- Raspberry Pi 5 8GB
- 追加ボード;NVMe Base for Raspberry Pi 5 (NVMe Base by Pimoroni)
- Crucial クルーシャル P2シリーズ 500GB 3D NAND NVMe PCIe M.2 SSD CT500P2SSD8
- Ubuntu Desktop 24.04LTS(64-bit)
接続
接続を確認します。
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: 70 71 -- -- -- -- -- --
0x44は湿度センサsht45です。4桁の7セグメントLEDは2個つないでおり、1個はAdafruitで購入した白色の、もう一つはアマゾンで購入した赤色LEDです。Adafruitのほうを基板のアドレス設定のジャンパをショートして0x71に変更しています。
Adafruit/Sparkfanの規格?である4ピン・コネクタStemma QT/Qwiicで、sht45とは数珠つなぎにしています。
i2cのアクセスの一部を関数化した
sht45のプログラムで、softReset()とinit()は、1バイトの書き込みをしているだけですが、まったく同じ内容のコードです。そこで、今回は、systemO()、displayOn()、brightnessSet()は、1バイト書き込み関数one_byte_write()を呼ぶ形にしました。
clearDisplay()とdisplay()は、複数バイトの書き込みをしていますが、とりあえず、最初はべたにコーディングしています。最終的には、block_write()関数にできればと思います。
clearDisplay()は、マトリクスの接続形態がどのような形であっても、レジスタ0に、0x00を10個書き込んだら消灯したというプログラムです。
display()のsegment変数は、桁数の指定で、左から、0,2,6,8で指定できます。7セグメントLEDのどのエレメントを光らせば数字にできるかは解析できているので、ブランクのコードを送れば、clearDisplay()相当の表示(みんな消灯する)できます。
main()の記述にあるように、7セグメントLEDには、「2547」と表示されています。とりあえず表示のプログラムが動きました。

#include <iostream>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <unistd.h>
#include <string>
#include <iomanip>
#include <cstdint>
#include <bitset>
#define ht16k33Adr 0x71
#define HT16K33_GENERIC_SYSTEM_ON 0x21
#define HT16K33_GENERIC_DISPLAY_ON 0x81
#define HT16K33_GENERIC_CMD_BRIGHTNESS 0xe0
int fd;
uint8_t address, data, segment, resister;
int systemOn();
int displayOn();
int brightnessSet();
int one_byte_write(uint8_t address, uint8_t resisterAdr, uint8_t setData);
int clearDisplay();
int display(uint8_t address, uint8_t dispSegment, uint8_t dispData);
int main() {
std::cout << "start display HT16K33\n";
fd = open("/dev/i2c-1", O_RDWR);
systemOn();
usleep(1000);
displayOn();
usleep(1000);
brightnessSet();
usleep(1000);
clearDisplay();
sleep(1);
segment = 0;
data = 0x5b; // 2
display(ht16k33Adr, segment, data);
segment = 2;
data = 0x6d; // 5
display(ht16k33Adr, segment, data);
segment = 6;
data = 0x66; // 4
display(ht16k33Adr, segment, data);
segment = 8;
data = 0x07; // 7
display(ht16k33Adr, segment, data);
sleep(5);
return 0;
}
int systemOn(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_SYSTEM_ON, 0);
return 0;
}
int displayOn(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_DISPLAY_ON, 0);
return 0;
}
int brightnessSet(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_CMD_BRIGHTNESS, 8);
return 0;
}
int one_byte_write(uint8_t address, uint8_t resisterAdr, uint8_t setData){
struct i2c_msg msg[1]; // /usr/include/linux/i2c.h
struct i2c_rdwr_ioctl_data packets; // /usr/include/linux/i2c-dev.h
uint8_t data[2];
msg[0].addr = address; // address
msg[0].flags = 0; // read、writeやアドレス長の指定
msg[0].len = 1; // bufに指定するdataのサイズ
msg[0].buf = data;
data[0] = resisterAdr;
data[1] = setData;
packets.msgs = msg;
packets.nmsgs = 1; // msgのサイズ指定
int ret = ioctl(fd, I2C_RDWR, &packets);
std::cout << "\nsend: 0x" << std::hex << (int)msg[0].addr
<< " resisterAdr 0x" << std::hex << (int)data[0]
<< " setData 0x" << std::hex << (int)data[1] << "\n";
return 0;
}
int clearDisplay(){
struct i2c_msg msg[1];
struct i2c_rdwr_ioctl_data packets;
uint8_t data[11];
msg[0].addr = ht16k33Adr; // address
msg[0].flags = 0; // read、writeやアドレス長の指定
msg[0].len = 11; // bufに指定するdataのサイズ
msg[0].buf = data;
data[0] = 0;
data[1] = 0x00;
data[2] = 0x00;
data[3] = 0x00;
data[4] = 0x00;
data[5] = 0x00;
data[6] = 0x00;
data[7] = 0x00;
data[8] = 0x00;
data[9] = 0x00;
data[10] = 0x00;
packets.msgs = msg;
packets.nmsgs = 1; // msgのサイズ指定
int retclear = ioctl(fd, I2C_RDWR, &packets);
std::cout << "\nsend: 0x" << std::hex << (int)msg[0].addr
<< " all 0 "<< "\n";
return 0;
}
int display(uint8_t address, uint8_t dispSegment, uint8_t dispData){
struct i2c_msg msgP[1];
struct i2c_rdwr_ioctl_data packetsP;
unsigned char data[2];
msgP[0].addr = address;
msgP[0].flags = 0;
msgP[0].len = 2;
msgP[0].buf = data;
data[0] = dispSegment;
data[1] = dispData;
packetsP.msgs = msgP;
packetsP.nmsgs = 1; // データ書き込みでmsgは1
int retP = ioctl(fd, I2C_RDWR, &packetsP);
return 0;
}
実行しました。
$ g++ ex120.cpp
yoshi@yoshi:~/book$ ./a.out
start display HT16K33
send: 0x71 resisterAdr 0x21 setData 0x0
send: 0x71 resisterAdr 0x81 setData 0x0
send: 0x71 resisterAdr 0xe0 setData 0x8
send: 0x71 all 0
数値を出力している部分の波形です。
まずは数値から文字列へ
温度や湿度を測定して、7セグメントLEDに表示したいのですが、数値のままではハンドリングできません。たぶん。よって文字列にしますが、C++を使うのでstring型にします。
実数->文字列は、どこにでも、to_stringを使うと書かれていますが、罠がありました。
12.3を変換したら、12.300000になるのです。だれもそんなことを書かないよね。
で、一般的な処理を書くのが難しそうなので(私が)、"00"を見つけたら、0以降を捨て去るという処理を書きました。
#include <iostream>
#include <string>
#include <iomanip>
int main(int argc, char* argv[]){
float temp = -12.3;
std::string tempS = std::to_string(temp);
std::cout << "str: " << tempS
<< "\n";
std::cout << "float: " << std::fixed << std::setprecision(1) << temp
<< "\n";
std::cout << "size is " << tempS.size() << "\n";
std::cout << "tempS.find(00) = " << tempS.find("00") << "\n";
std::string tempS0 = tempS.erase(tempS.find("00"));
std::cout << "string: " << tempS0 << "\n";
std::cout << "size is " << tempS0.size() << "\n";
std::cout << "\n ----- \n";
float temp1 = 99.90;
std::cout << "float: " << std::fixed << std::setprecision(1) << temp1
<< "\n";
std::string tempS1 = std::to_string(temp1);
std::string tempS10 =tempS1.erase(tempS1.find("00"));
std::cout << "string: " << tempS10 << "\n";
std::cout << "\n ----- \n";
float temp2 = 0.12;
std::cout << "float: " << std::fixed << std::setprecision(2) << temp2
<< "\n";
std::string tempS2 = std::to_string(temp2);
std::string tempS20 =tempS2.erase(tempS2.find("00"));
std::cout << "string: " << tempS20 << "\n";
std::cout << "\n ----- \n";
float temp3 = -2.34;
std::cout << "float: " << std::fixed << std::setprecision(2) << temp3
<< "\n";
std::string tempS3 = std::to_string(temp3);
std::string tempS30 =tempS3.erase(tempS3.find("00"));
std::cout << "string: " << tempS30 << "\n";
std::cout << "\n ----- \n";
float temp4 = -0.12;
std::cout << "float: " << std::fixed << std::setprecision(2) << temp4
<< "\n";
std::string tempS4 = std::to_string(temp4);
std::string tempS40 =tempS4.erase(tempS4.find("00"));
std::cout << "string: " << tempS40 << "\n";
return 0;
}
実行結果です。いくつかのデータを試しましたが、たぶん、室温の表示には問題はないだろうと思います。白金や熱電対を使う人は、このままではまずいところがでてきそうです!
$ ./a.out
str: -12.300000
float: -12.3
size is 10
tempS.find(00) = 5
string: -12.3
size is 5
-----
float: 99.9
string: 99.9
-----
float: 0.12
string: 0.12
-----
float: -2.34
string: -2.34
-----
float: -0.12
string: -0.12
難関はドット処理
7セグメントLEDにドットの表示があります。が、ドットだけを1桁のところに表示すると、間が抜けてしまいます。そこで、数値にドットを引っ付けて表示することになります。
7セグメントLEDの表示データでは、MSBを'1'にすれば数字にドットが引っ付きます。

map関数、初めて使いますが、測定値の数字(文字)とht16k33へ送るコードの対応、pythonでいうところの辞書機能?です。4桁の一つずつをsegmentという変数にし、その対応もmap関数を用意しました。
segmentは右からデータを用意します。つまり、測定値の文字列を後ろから読み取ってきます。ドットだったら、ループを一つ進めて次の数字を読み取ります。その表示用データの最上位ビットを'1'にします。これで、ドット付き数字が表示できます。
segmentDispData[s]が、7セグメントLEDへ送るデータのarrayです。
#include <iostream>
#include <string>
#include <iomanip>
#include <map>
#include <cstdint>
#include <array>
std::array<uint8_t, 4> segmentDispData = {0, 0, 0, 0};
int main(){
float temp = -12.3;
std::string tempS = std::to_string(temp);
std::string tempS0 =tempS.erase(tempS.find("00"));
std::cout << "string: " << tempS0 << "\n";
int size = tempS0.size();
std::map<unsigned char, uint8_t> figData{
{'0', 0x3f}, {'1', 0x06}, {'2', 0x5b}, {'3', 0x4f}, {'4', 0x66}, {'5', 0x6d},
{'6', 0x7d}, {'7', 0x07}, {'8', 0x7f}, {'9', 0x67}, {'-', 0x40} ,{'.', 0x80}};
std::map<uint8_t, uint8_t> segmentData{{1, 0}, {2, 2}, {3, 6}, {4, 8}};
int s = 4;
for (int i = 1; i < size + 1; i++){
if (tempS0[size-i] == '.'){
i++;
segmentDispData[s] = figData.at(tempS0[size-i]) | 0x80;
}else{
segmentDispData[s] = figData.at(tempS0[size-i]);
}
std::cout << "segment" << s << ": " << tempS0[size-i]
<< " 0x" << std::hex << (int)segmentDispData[s] << "\n";
s--;
}
return 0;
}
実行します。
$ g++ ex125.cpp
$ ./a.out
string: -12.3
segment4: 3 0x4f
segment3: 2 0xdb
segment2: 1 0x6
segment1: - 0x40
変換したデータは、arrayを使った配列?に収納します。固定長が特徴だそうです。
二つのプログラムを合体させます。
実数を7セグメントLEDに表示するプログラム
最初に、上記のプログラムを、7セグメントLEDへ送るデータ処理部分を関数に分離しました。
#include <iostream>
#include <string>
#include <iomanip>
#include <map>
#include <cstdint>
#include <array>
int s, i;
int convertDispData(float measurementData);
std::array<uint8_t, 4> segmentDispData = {0, 0, 0, 0};
int main(){
float temp = -12.3;
convertDispData(temp);
return 0;
}
int convertDispData(float measurementData){
std::array<uint8_t, 4> segmentDispData;
std::string measurementDataString = std::to_string(measurementData);
std::string measurementDataStringSlim = measurementDataString.erase(measurementDataString.find("00"));
std::cout << "input measurementData(string): " << measurementDataStringSlim << "\n";
int size = measurementDataStringSlim.size();
std::map<unsigned char, uint8_t> figData{
{'0', 0x3f}, {'1', 0x06}, {'2', 0x5b}, {'3', 0x4f}, {'4', 0x66}, {'5', 0x6d},
{'6', 0x7d}, {'7', 0x07}, {'8', 0x7f}, {'9', 0x67}, {'-', 0x40} ,{'.', 0x80}};
std::map<uint8_t, uint8_t> segmentData{{1, 0}, {2, 2}, {3, 6}, {4, 8}};
s = 4;
for (i = 1; i < size + 1; i++){
if (measurementDataStringSlim[size-i] == '.'){
i++;
segmentDispData[s] = figData.at(measurementDataStringSlim[size-i]) | 0x80;
}else{
segmentDispData[s] = figData.at(measurementDataStringSlim[size-i]);
}
std::cout << "segment" << s << ": " << measurementDataString[size-i]
<< " 0x" << std::hex << (int)segmentDispData[s] << "\n";
s--;
}
return 0;
}
合体させたプログラムです。実数を表示用のデータに変換するconvertDispData()の最後で、7セグメントLEDへ表示を行うdisplay()を呼んでいます。
いったんmainに戻ってからと思ってチャレンジしたのですが、arrayを値渡しとか参照渡しをする記述が、検索して見つかった方法だと全部動かなかったのであきらめました。vectorなら値渡しができるというのを見たのですが、試していません。
#include <iostream>
#include <string>
#include <iomanip>
#include <map>
#include <cstdint>
#include <array>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <unistd.h>
#include <string>
#define ht16k33Adr 0x71
#define HT16K33_GENERIC_SYSTEM_ON 0x21
#define HT16K33_GENERIC_DISPLAY_ON 0x81
#define HT16K33_GENERIC_CMD_BRIGHTNESS 0xe0
int fd;
uint8_t address, data, segment, resister;
int systemOn();
int displayOn();
int brightnessSet();
int one_byte_write(uint8_t address, uint8_t resisterAdr, uint8_t setData);
int clearDisplay();
int display(uint8_t address, uint8_t dispSegment, uint8_t dispData);
int s, i;
int convertDispData(float measurementData);
std::array<uint8_t, 4> segmentDispData = {0, 0, 0, 0};
int main(){
std::cout << "start display HT16K33\n";
fd = open("/dev/i2c-1", O_RDWR);
systemOn();
usleep(1000);
displayOn();
usleep(1000);
brightnessSet();
usleep(1000);
clearDisplay();
usleep(1000);
float temp = -25.6;
convertDispData(temp);
return 0;
}
int convertDispData(float measurementData){
std::array<uint8_t, 4> segmentDispData;
std::string measurementDataString = std::to_string(measurementData);
std::string measurementDataStringSlim = measurementDataString.erase(measurementDataString.find("00"));
std::cout << "input measurementData(string): " << measurementDataStringSlim << "\n";
int size = measurementDataStringSlim.size();
std::map<unsigned char, uint8_t> figData{
{'0', 0x3f}, {'1', 0x06}, {'2', 0x5b}, {'3', 0x4f}, {'4', 0x66}, {'5', 0x6d},
{'6', 0x7d}, {'7', 0x07}, {'8', 0x7f}, {'9', 0x67}, {'-', 0x40} ,{'.', 0x80}};
std::map<uint8_t, uint8_t> segmentData{{1, 0}, {2, 2}, {3, 6}, {4, 8}};
s = 4;
for (i = 1; i < size + 1; i++){
if (measurementDataStringSlim[size-i] == '.'){
i++;
segmentDispData[s] = figData.at(measurementDataStringSlim[size-i]) | 0x80;
}else{
segmentDispData[s] = figData.at(measurementDataStringSlim[size-i]);
}
std::cout << "segment" << s << ": " << measurementDataString[size-i]
<< " 0x" << std::hex << (int)segmentDispData[s] << "\n";
display(ht16k33Adr, segmentData[s], (int)segmentDispData[s]);
s--;
}
return 0;
}
int systemOn(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_SYSTEM_ON, 0);
return 0;
}
int displayOn(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_DISPLAY_ON, 0);
return 0;
}
int brightnessSet(){
one_byte_write(ht16k33Adr, HT16K33_GENERIC_CMD_BRIGHTNESS, 8);
return 0;
}
int one_byte_write(uint8_t address, uint8_t resisterAdr, uint8_t setData){
struct i2c_msg msg[1]; // /usr/include/linux/i2c.h
struct i2c_rdwr_ioctl_data packets; // /usr/include/linux/i2c-dev.h
uint8_t data[2];
msg[0].addr = address; // address
msg[0].flags = 0; // read、writeやアドレス長の指定
msg[0].len = 1; // bufに指定するdataのサイズ
msg[0].buf = data;
data[0] = resisterAdr;
data[1] = setData;
packets.msgs = msg;
packets.nmsgs = 1; // msgのサイズ指定
int ret = ioctl(fd, I2C_RDWR, &packets);
// std::cout << "\nsend: 0x" << std::hex << (int)msg[0].addr
// << " resisterAdr 0x" << std::hex << (int)data[0]
// << " setData 0x" << std::hex << (int)data[1] << "\n";
return 0;
}
int clearDisplay(){
struct i2c_msg msg[1];
struct i2c_rdwr_ioctl_data packets;
uint8_t data[11];
msg[0].addr = ht16k33Adr; // address
msg[0].flags = 0; // read、writeやアドレス長の指定
msg[0].len = 11; // bufに指定するdataのサイズ
msg[0].buf = data;
data[0] = 0;
data[1] = 0x00;
data[2] = 0x00;
data[3] = 0x00;
data[4] = 0x00;
data[5] = 0x00;
data[6] = 0x00;
data[7] = 0x00;
data[8] = 0x00;
data[9] = 0x00;
data[10] = 0x00;
packets.msgs = msg;
packets.nmsgs = 1; // msgのサイズ指定
int retclear = ioctl(fd, I2C_RDWR, &packets);
// std::cout << "\nsend: 0x" << std::hex << (int)msg[0].addr
// << " all 0 "<< "\n";
return 0;
}
int display(uint8_t address, uint8_t dispSegment, uint8_t dispData){
struct i2c_msg msgP[1];
struct i2c_rdwr_ioctl_data packetsP;
unsigned char data[2];
msgP[0].addr = address;
msgP[0].flags = 0;
msgP[0].len = 2;
msgP[0].buf = data;
data[0] = dispSegment;
data[1] = dispData;
packetsP.msgs = msgP;
packetsP.nmsgs = 1; // データ書き込みでmsgは1
int retP = ioctl(fd, I2C_RDWR, &packetsP);
return 0;
}
実行します。
$ g++ ex127.cpp
$ ./a.out
start display HT16K33
input measurementData(string): -25.6
segment4: 6 0x7d
segment3: 5 0xed
segment2: 2 0x5b
segment1: - 0x40

(※)アマゾンで購入した7セグメントはStemma QT/Qwiicがついていないので、Adafruitとi2cの4ピンを相互に接続してあります。