commit 45ba72817e4b4d3d2e4ee34765653935eb0ef6ba
parent 0af956d9bd290eca6860723a0590e3e6296bd34e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 10:54:24 -0700
arm32: SSAT/USAT/QADD/QSUB/QDADD/QDSUB encoders + asm/disasm support
Diffstat:
6 files changed, 202 insertions(+), 0 deletions(-)
diff --git a/src/arch/arm32/asm.c b/src/arch/arm32/asm.c
@@ -571,6 +571,31 @@ static void assemble_one(AsmDriver* d, const Arm32InsnDesc* desc, u32 cond) {
emit_t32(d, arm_bfc(rd, lsb, width));
return;
}
+ case ARM_FMT_SAT: {
+ /* SSAT/USAT rd, #sat, rm (no-shift first cut; mnemonic picks signed vs
+ * unsigned). */
+ u32 rd = parse_reg(d), sat, rm;
+ expect_comma(d);
+ sat = (u32)parse_imm(d);
+ expect_comma(d);
+ rm = parse_reg(d);
+ emit_t32(d, slice_eq_cstr(mn, "usat") ? arm_usat(rd, sat, rm)
+ : arm_ssat(rd, sat, rm));
+ return;
+ }
+ case ARM_FMT_QADD: {
+ /* QADD/QSUB/QDADD/QDSUB rd, rm, rn (ARM operand order). */
+ u32 rd = parse_reg(d), rm, rn;
+ expect_comma(d);
+ rm = parse_reg(d);
+ expect_comma(d);
+ rn = parse_reg(d);
+ emit_t32(d, slice_eq_cstr(mn, "qsub") ? arm_qsub(rd, rm, rn)
+ : slice_eq_cstr(mn, "qdadd") ? arm_qdadd(rd, rm, rn)
+ : slice_eq_cstr(mn, "qdsub") ? arm_qdsub(rd, rm, rn)
+ : arm_qadd(rd, rm, rn));
+ return;
+ }
case ARM_FMT_LDST_T3: {
u32 rt = parse_reg(d), base;
i64 disp;
diff --git a/src/arch/arm32/disasm.c b/src/arch/arm32/disasm.c
@@ -198,6 +198,26 @@ static void arm32_decode_operands(const Arm32InsnDesc* d, u32 w, u64 pc,
arm_decop_pcrel(&out->operands[0], pc, 0);
break;
}
+ case ARM_FMT_SAT: {
+ /* SSAT/USAT rd, #sat, rm: USAT (word[23]=1) encodes sat directly, SSAT
+ * encodes sat-1. Rm is in hw1[3:0]. */
+ u32 sat_imm = w & 0x1fu;
+ u32 is_usat = (w >> 23) & 1u;
+ out->noperands = 3;
+ arm_decop_reg(&out->operands[0], (w >> 8) & 0xfu);
+ arm_decop_imm(&out->operands[1],
+ (i64)(is_usat ? sat_imm : sat_imm + 1u));
+ arm_decop_reg(&out->operands[2], (w >> 16) & 0xfu);
+ break;
+ }
+ case ARM_FMT_QADD: {
+ /* Q{D}ADD/Q{D}SUB rd, rm, rn: Rd=hw2[11:8], Rm=hw2[3:0], Rn=hw1[3:0]. */
+ out->noperands = 3;
+ arm_decop_reg(&out->operands[0], (w >> 8) & 0xfu);
+ arm_decop_reg(&out->operands[1], w & 0xfu);
+ arm_decop_reg(&out->operands[2], (w >> 16) & 0xfu);
+ break;
+ }
default:
break;
}
diff --git a/src/arch/arm32/isa.c b/src/arch/arm32/isa.c
@@ -183,6 +183,25 @@ const Arm32InsnDesc arm32_insn_table[] = {
{MN("ubfx"), (0xf3c00000u), 0xfff08020u, ARM_FMT_BFX, 0, {0}},
/* =================================================================
+ * ARMv7E-M DSP saturating. SSAT/USAT share the 0xF3xx plain-binary-imm
+ * group with the bitfield/movw rows above; the mask pins the LSL/no-shift
+ * forms (word[21]=0, since the ASR and SSAT16/USAT16 forms have word[21]=1)
+ * plus hw2[15]=0 and hw2[5:4]=00, so SSAT16/USAT16/SBFX/UBFX/BFI/MOVW never
+ * collide. The ASR-shifted SSAT/USAT (word[21]=1) is intentionally not
+ * decoded — it falls through to .inst rather than risk misdecoding the
+ * encoding-overlapping SSAT16. QADD/QSUB/QDADD/QDSUB pin hw1[15:4]=0xFA8,
+ * hw2[15:12]=1111, hw2[7:4]=op (collision-free vs CLZ 0xFAB0 / REV/RBIT
+ * 0xFA9x). Verified by round-tripping each match word through llvm-mc
+ * (`--triple=thumbv7em-none-eabi --mattr=+dsp -disassemble`).
+ * ================================================================= */
+ {MN("ssat"), (0xf3000000u), 0xffe08030u, ARM_FMT_SAT, 0, {0}},
+ {MN("usat"), (0xf3800000u), 0xffe08030u, ARM_FMT_SAT, 0, {0}},
+ {MN("qadd"), (0xfa80f080u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}},
+ {MN("qdadd"), (0xfa80f090u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}},
+ {MN("qsub"), (0xfa80f0a0u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}},
+ {MN("qdsub"), (0xfa80f0b0u), 0xfff0f0f0u, ARM_FMT_QADD, 0, {0}},
+
+ /* =================================================================
* Loads / stores — T3 (positive imm12). hw1 selects op+width:
* LDR 0xF8D0 STR 0xF8C0 LDRB 0xF890 STRB 0xF880
* LDRH 0xF8B0 STRH 0xF8A0 LDRSB 0xF990 LDRSH 0xF9B0.
@@ -701,6 +720,37 @@ static void print_bfc(StrBuf* sb, u32 w) {
p_imm(sb, (i64)(msb - lsb + 1u));
}
+/* SSAT/USAT rd, #sat, rm. The U bit (word[23] = hw1[7]) selects USAT, which
+ * encodes the saturation position directly; SSAT encodes sat-1. Rm sits in
+ * hw1[3:0] (f_rn). The (LSL) shift amount lives in imm3:imm2 (hw2[14:12]:[7:6]);
+ * it is appended only when nonzero. */
+static void print_sat(StrBuf* sb, u32 w) {
+ u32 is_usat = (f_hw1(w) >> 7) & 1u;
+ u32 sat_imm = f_hw2(w) & 0x1fu;
+ u32 sat = is_usat ? sat_imm : (sat_imm + 1u);
+ u32 imm3 = (f_hw2(w) >> 12) & 7u, imm2 = (f_hw2(w) >> 6) & 3u;
+ u32 sh = (imm3 << 2) | imm2;
+ p_reg(sb, f_rd(w));
+ p_sep(sb);
+ p_imm(sb, (i64)sat);
+ p_sep(sb);
+ p_reg(sb, f_rn(w)); /* Rm */
+ if (sh) {
+ strbuf_puts(sb, ", lsl ");
+ p_imm(sb, (i64)sh);
+ }
+}
+
+/* QADD/QSUB/QDADD/QDSUB rd, rm, rn. Rd = hw2[11:8], Rm = hw2[3:0], Rn lands in
+ * hw1[3:0] (the ARM operand order is Rd, Rm, Rn). */
+static void print_qadd(StrBuf* sb, u32 w) {
+ p_reg(sb, f_rd(w));
+ p_sep(sb);
+ p_reg(sb, f_rm(w));
+ p_sep(sb);
+ p_reg(sb, f_rn(w));
+}
+
static void print_ldst_t3(StrBuf* sb, u32 w) {
u32 rt = (f_hw2(w) >> 12) & 0xfu;
u32 imm12 = f_hw2(w) & 0xfffu;
@@ -1067,6 +1117,12 @@ void arm32_print_operands(StrBuf* sb, const Arm32InsnDesc* desc, u32 word,
case ARM_FMT_BFC:
print_bfc(sb, word);
break;
+ case ARM_FMT_SAT:
+ print_sat(sb, word);
+ break;
+ case ARM_FMT_QADD:
+ print_qadd(sb, word);
+ break;
case ARM_FMT_LDST_T3:
print_ldst_t3(sb, word);
break;
diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h
@@ -350,6 +350,32 @@ static inline u32 arm_smull(u32 rdlo, u32 rdhi, u32 rn, u32 rm) {
return arm_t32(0xfb80u | rn, (rdlo << 12) | (rdhi << 8) | rm);
}
+/* --------- ARMv7E-M DSP saturating (T1) ---------
+ * SSAT/USAT saturate Rm to a signed/unsigned `sat`-bit value (no-shift form):
+ * SSAT Rd, #sat, Rm : hw1 = 0xF300|Rn(=Rm), hw2 = (Rd<<8) | ((sat-1)&0x1f)
+ * USAT Rd, #sat, Rm : hw1 = 0xF380|Rm, hw2 = (Rd<<8) | (sat&0x1f)
+ * (SSAT encodes sat_imm = sat-1; USAT encodes sat directly. The LSL/ASR shift
+ * operand is omitted in this cut — see isa.c for the decode-mask rationale.)
+ *
+ * QADD/QSUB/QDADD/QDSUB are register saturating add/sub (ARM operand order
+ * Rd, Rm, Rn): hw1 = 0xFA80|Rn, hw2 = 0xF000 | (Rd<<8) | (op<<4) | Rm, with
+ * op QADD=0x8, QDADD=0x9, QSUB=0xA, QDSUB=0xB. Note Rn lands in hw1[3:0] and
+ * Rm in hw2[3:0]. */
+static inline u32 arm_ssat(u32 rd, u32 sat, u32 rm) {
+ return arm_t32(0xf300u | (rm & 0xfu), (rd << 8) | ((sat - 1u) & 0x1fu));
+}
+static inline u32 arm_usat(u32 rd, u32 sat, u32 rm) {
+ return arm_t32(0xf380u | (rm & 0xfu), (rd << 8) | (sat & 0x1fu));
+}
+static inline u32 arm_qop(u32 op, u32 rd, u32 rm, u32 rn) {
+ return arm_t32(0xfa80u | (rn & 0xfu),
+ 0xf000u | (rd << 8) | ((op & 0xfu) << 4) | (rm & 0xfu));
+}
+static inline u32 arm_qadd(u32 rd, u32 rm, u32 rn) { return arm_qop(0x8u, rd, rm, rn); }
+static inline u32 arm_qdadd(u32 rd, u32 rm, u32 rn) { return arm_qop(0x9u, rd, rm, rn); }
+static inline u32 arm_qsub(u32 rd, u32 rm, u32 rn) { return arm_qop(0xau, rd, rm, rn); }
+static inline u32 arm_qdsub(u32 rd, u32 rm, u32 rn) { return arm_qop(0xbu, rd, rm, rn); }
+
static inline u32 arm_dmb(u32 option) { return arm_t32(0xf3bfu, 0x8f50u | (option & 0xfu)); }
static inline u32 arm_dsb(u32 option) { return arm_t32(0xf3bfu, 0x8f40u | (option & 0xfu)); }
static inline u32 arm_isb(u32 option) { return arm_t32(0xf3bfu, 0x8f60u | (option & 0xfu)); }
@@ -412,6 +438,8 @@ typedef enum Arm32Format {
ARM_FMT_BFX, /* SBFX/UBFX rd, rn, #lsb, #width */
ARM_FMT_BFI, /* BFI rd, rn, #lsb, #width */
ARM_FMT_BFC, /* BFC rd, #lsb, #width */
+ ARM_FMT_SAT, /* SSAT/USAT rd, #sat, rm */
+ ARM_FMT_QADD, /* QADD/QSUB/QDADD/QDSUB rd, rm, rn */
ARM_FMT_LDST_T3, /* LDR/STR{B,H,SB,SH} rt, [rn, #imm12] */
ARM_FMT_LDST_T4, /* LDR/STR{B,H,SB,SH} rt, [rn, #+/-imm8] */
ARM_FMT_LDREX, /* LDREX rt, [rn{, #imm}] */
diff --git a/test/arch/arm32_decode_test.c b/test/arch/arm32_decode_test.c
@@ -287,6 +287,65 @@ static void decode_barriers(KitCompiler* pub) {
expect_text(pub, b, 4, 0, 4, "isb", "sy", "isb sy");
}
+/* ARMv7E-M DSP saturating ops: SSAT/USAT (Rd, #sat, Rm) + the QADD family
+ * (Rd, Rm, Rn — note Rn lands in hw1[3:0], Rm in hw2[3:0]). Golden words pinned
+ * to llvm-mc (`--triple=thumbv7em-none-eabi --mattr=+dsp -show-encoding`). */
+static void decode_dsp_sat(KitCompiler* pub) {
+ unsigned char b[4];
+
+ /* Direct encoder -> word equality, pinned to the oracle bytes. */
+ EXPECT(arm_ssat(ARM_R0, 16, ARM_R1) == 0xf301000fu, "ssat encode = %#x",
+ arm_ssat(ARM_R0, 16, ARM_R1));
+ /* llvm-mc bytes [0x06,0xf3,0x1f,0x05] -> hw1=0xf306, hw2=0x051f. (The task
+ * spec quoted 0xf3061f05 with hw2's bytes transposed; the real word is
+ * 0xf306051f, hw2 = (rd<<8)|(sat-1) = (5<<8)|31.) */
+ EXPECT(arm_ssat(ARM_R5, 32, ARM_R6) == 0xf306051fu, "ssat32 encode = %#x",
+ arm_ssat(ARM_R5, 32, ARM_R6));
+ EXPECT(arm_usat(ARM_R0, 15, ARM_R1) == 0xf381000fu, "usat encode = %#x",
+ arm_usat(ARM_R0, 15, ARM_R1));
+ EXPECT(arm_qadd(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f381u, "qadd encode = %#x",
+ arm_qadd(ARM_R3, ARM_R1, ARM_R2));
+ EXPECT(arm_qadd(ARM_R0, ARM_R2, ARM_R1) == 0xfa81f082u, "qadd2 encode = %#x",
+ arm_qadd(ARM_R0, ARM_R2, ARM_R1));
+ EXPECT(arm_qsub(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f3a1u, "qsub encode = %#x",
+ arm_qsub(ARM_R3, ARM_R1, ARM_R2));
+ EXPECT(arm_qdadd(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f391u, "qdadd encode = %#x",
+ arm_qdadd(ARM_R3, ARM_R1, ARM_R2));
+ EXPECT(arm_qdsub(ARM_R3, ARM_R1, ARM_R2) == 0xfa82f3b1u, "qdsub encode = %#x",
+ arm_qdsub(ARM_R3, ARM_R1, ARM_R2));
+
+ /* Disassembly round-trips. */
+ put_t32(b, 0, arm_ssat(ARM_R0, 16, ARM_R1));
+ expect_text(pub, b, 4, 0, 4, "ssat", "r0, #16, r1", "ssat");
+ put_t32(b, 0, arm_usat(ARM_R0, 15, ARM_R1));
+ expect_text(pub, b, 4, 0, 4, "usat", "r0, #15, r1", "usat");
+ put_t32(b, 0, arm_qadd(ARM_R3, ARM_R1, ARM_R2));
+ expect_text(pub, b, 4, 0, 4, "qadd", "r3, r1, r2", "qadd");
+ put_t32(b, 0, arm_qsub(ARM_R3, ARM_R1, ARM_R2));
+ expect_text(pub, b, 4, 0, 4, "qsub", "r3, r1, r2", "qsub");
+ put_t32(b, 0, arm_qdadd(ARM_R3, ARM_R1, ARM_R2));
+ expect_text(pub, b, 4, 0, 4, "qdadd", "r3, r1, r2", "qdadd");
+ put_t32(b, 0, arm_qdsub(ARM_R3, ARM_R1, ARM_R2));
+ expect_text(pub, b, 4, 0, 4, "qdsub", "r3, r1, r2", "qdsub");
+
+ /* Structured-operand check: SSAT decodes to (reg, imm=16, reg). */
+ {
+ Compiler* c = (Compiler*)pub;
+ KitDecodedInsn insn;
+ put_t32(b, 0, arm_ssat(ARM_R0, 16, ARM_R1));
+ memset(&insn, 0, sizeof insn);
+ EXPECT(arch_decode_one(c, b, 4, 0, &insn) == KIT_OK, "ssat decode");
+ EXPECT(insn.noperands == 3, "ssat noperands = %u",
+ (unsigned)insn.noperands);
+ EXPECT(insn.operands[0].kind == KIT_DECOP_REG && insn.operands[0].reg == 0,
+ "ssat rd");
+ EXPECT(insn.operands[1].kind == KIT_DECOP_IMM && insn.operands[1].imm == 16,
+ "ssat sat = %lld", (long long)insn.operands[1].imm);
+ EXPECT(insn.operands[2].kind == KIT_DECOP_REG && insn.operands[2].reg == 1,
+ "ssat rm");
+ }
+}
+
static void decode_ldrex_strex(KitCompiler* pub) {
unsigned char b[4];
put_t32(b, 0, arm_ldrex(ARM_R0, ARM_R1, 0));
@@ -382,6 +441,7 @@ int main(void) {
decode_misc16(c);
decode_barriers(c);
decode_ldrex_strex(c);
+ decode_dsp_sat(c);
decode_it_block(c);
decode_block_terminates(c);
decode_unknown_fallback(c);
diff --git a/test/arch/arm32_inline_test.c b/test/arch/arm32_inline_test.c
@@ -12,6 +12,9 @@ static void arm32_body(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) {
it_inline_asm(c, cg, "add.w r0, r0, #7", NULL, 0, NULL, 0, NULL, 0);
it_inline_asm(c, cg, "movw r1, #0x1234", NULL, 0, NULL, 0, NULL, 0);
it_inline_asm(c, cg, "bkpt #0xab", NULL, 0, NULL, 0, NULL, 0);
+ /* ARMv7E-M DSP saturating (no dsp-feature gate; assembler is permissive). */
+ it_inline_asm(c, cg, "ssat r0, #16, r1", NULL, 0, NULL, 0, NULL, 0);
+ it_inline_asm(c, cg, "qadd r3, r1, r2", NULL, 0, NULL, 0, NULL, 0);
}
static void arm32_bad_operand(KitCompiler* c, KitCg* cg, KitCgTypeId i64_ty) {
@@ -156,6 +159,8 @@ int main(void) {
static const uint8_t add_w_r0_7[] = {0x00u, 0xf1u, 0x07u, 0x00u};
static const uint8_t movw_r1_1234[] = {0x41u, 0xf2u, 0x34u, 0x21u};
static const uint8_t bkpt_ab[] = {0xabu, 0xbeu};
+ static const uint8_t ssat_r0_16_r1[] = {0x01u, 0xf3u, 0x0fu, 0x00u};
+ static const uint8_t qadd_r3_r1_r2[] = {0x82u, 0xfau, 0x81u, 0xf3u};
InlineTestEnv env;
InlineText text;
@@ -176,6 +181,14 @@ int main(void) {
"missing movw r1, #0x1234 encoding");
IT_EXPECT(&env, it_contains(text.data, text.len, bkpt_ab, sizeof bkpt_ab),
"missing bkpt #0xab encoding");
+ IT_EXPECT(&env,
+ it_contains(text.data, text.len, ssat_r0_16_r1,
+ sizeof ssat_r0_16_r1),
+ "missing ssat r0, #16, r1 encoding");
+ IT_EXPECT(&env,
+ it_contains(text.data, text.len, qadd_r3_r1_r2,
+ sizeof qadd_r3_r1_r2),
+ "missing qadd r3, r1, r2 encoding");
}
it_text_close(&text);